|
1 | 1 | --- |
2 | | -title: About |
3 | | -description: About this starter project |
| 2 | +title: Content Pages |
| 3 | +description: This page is rendered from a Markdown file using the Analog.js content feature — here's how it works. |
4 | 4 | --- |
5 | 5 |
|
6 | | -## About This Starter |
| 6 | +## How This Page Is Built |
7 | 7 |
|
8 | | -This is a full-stack starter template demonstrating modern Angular and .NET development practices. |
| 8 | +This page is rendered by an Angular component that loads a Markdown file from `src/content/about.md` using `injectContent()` from `@analogjs/content`. |
9 | 9 |
|
10 | | -### Tech Stack |
| 10 | +```typescript |
| 11 | +import { MarkdownComponent, injectContent } from '@analogjs/content'; |
| 12 | +import { toSignal } from '@angular/core/rxjs-interop'; |
11 | 13 |
|
12 | | -**Frontend** |
| 14 | +export class About { |
| 15 | + readonly content = toSignal(injectContent<AboutAttributes>({ customFilename: 'about' })); |
| 16 | +} |
| 17 | +``` |
| 18 | + |
| 19 | +The `customFilename` option loads a specific file from `src/content/` without needing a slug route parameter. The `injectContent()` function returns an Observable of a `ContentFile` that includes the rendered HTML, typed frontmatter attributes, and a table of contents extracted from the headings. |
| 20 | + |
| 21 | +## Frontmatter |
| 22 | + |
| 23 | +Each content file can include a YAML frontmatter block. This page's frontmatter looks like: |
| 24 | + |
| 25 | +```yaml |
| 26 | +--- |
| 27 | +title: Content Pages |
| 28 | +description: This page is rendered from a Markdown file... |
| 29 | +--- |
| 30 | +``` |
| 31 | + |
| 32 | +The frontmatter is available as strongly-typed attributes on the `ContentFile` object. In the component template, this page's `title` and `description` are read directly from `content.attributes` to render the hero section above — no duplication between the page title and the markdown body. |
13 | 33 |
|
14 | | -- [Angular 21](https://angular.dev) — Zoneless, signals-first |
15 | | -- [NgRx Signal Store](https://ngrx.io/guide/signals/signal-store) — Reactive state management |
16 | | -- [Angular Material](https://material.angular.io) — UI component library |
17 | | -- [Analog.js](https://analogjs.org) — Vite-native Angular meta-framework |
18 | | -- [Tailwind CSS](https://tailwindcss.com) — Utility-first styling |
| 34 | +## Table of Contents |
19 | 35 |
|
20 | | -**Backend** |
| 36 | +The `content.toc` array is automatically generated from the headings in this Markdown file. Each entry has an `id`, `level`, and `text`: |
21 | 37 |
|
22 | | -- [.NET 10](https://dotnet.microsoft.com) — Web API |
| 38 | +```typescript |
| 39 | +type TableOfContentItem = { |
| 40 | + id: string; // the anchor id, e.g. "how-this-page-is-built" |
| 41 | + level: number; // heading level: 2, 3, 4... |
| 42 | + text: string; // heading text |
| 43 | +}; |
| 44 | +``` |
23 | 45 |
|
24 | | -**Tooling** |
| 46 | +The "On this page" panel above this content is built from `content.toc` in the component template — it links directly to each heading's auto-generated anchor id. |
25 | 47 |
|
26 | | -- [Nx](https://nx.dev) — Monorepo build system |
27 | | -- [Vite](https://vitejs.dev) — Development server and bundler |
28 | | -- [Vitest](https://vitest.dev) — Unit testing framework |
29 | | -- [pnpm](https://pnpm.io) — Fast, disk-efficient package manager |
| 48 | +## Syntax Highlighting |
30 | 49 |
|
31 | | -### Features |
| 50 | +Code blocks are highlighted at build time using [Shiki](https://shiki.style/), configured in `vite.config.ts`: |
32 | 51 |
|
33 | | -- Zoneless Angular with `provideZonelessChangeDetection` |
34 | | -- Signal-based state with NgRx Signal Store |
35 | | -- Markdown content pages via Analog.js |
36 | | -- Progressive Web App (PWA) support |
37 | | -- JWT authentication flow |
38 | | -- Lazy-loaded feature modules |
39 | | -- Full CI/CD pipeline with GitHub Actions |
| 52 | +```typescript |
| 53 | +analog({ |
| 54 | + content: { |
| 55 | + highlighter: 'shiki', |
| 56 | + shikiOptions: { |
| 57 | + highlighter: { |
| 58 | + additionalLangs: ['bash', 'shell', 'yaml'], |
| 59 | + }, |
| 60 | + }, |
| 61 | + }, |
| 62 | +}); |
| 63 | +``` |
40 | 64 |
|
41 | | -### Getting Started |
| 65 | +Shiki uses the same grammar files as VS Code, so you get accurate highlighting for TypeScript, Angular templates, YAML, shell, and more — all resolved at build time with no client-side runtime overhead. |
42 | 66 |
|
43 | | -Clone the repository and install dependencies: |
| 67 | +## Listing All Content Files |
44 | 68 |
|
45 | | -```bash |
46 | | -git clone https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter |
47 | | -cd angular-cli-netcore-ngrx-starter |
48 | | -pnpm install |
| 69 | +`injectContentFiles()` returns metadata for every file in `src/content/` — synchronously, resolved at build time. The panel below the content on this page is built from it: |
| 70 | + |
| 71 | +```typescript |
| 72 | +readonly contentFiles = injectContentFiles<AboutAttributes>(); |
49 | 73 | ``` |
50 | 74 |
|
51 | | -Start the full stack in one command: |
| 75 | +Note that `injectContentFiles()` returns **metadata only** — `filename`, `slug`, and `attributes`. The content body is not included. Use `injectContent()` separately to load and render an individual file's body. |
| 76 | + |
| 77 | +## Mermaid Diagrams |
| 78 | + |
| 79 | +Mermaid diagrams are supported via the `loadMermaid` option on `withMarkdownRenderer()`: |
52 | 80 |
|
53 | | -```bash |
54 | | -pnpm nx serve web-app |
| 81 | +```typescript |
| 82 | +provideContent( |
| 83 | + withMarkdownRenderer({ |
| 84 | + loadMermaid: () => import('mermaid'), |
| 85 | + }), |
| 86 | +); |
55 | 87 | ``` |
| 88 | + |
| 89 | +Mermaid blocks in Markdown are rendered client-side as SVGs. Here's how the content pipeline for this page works: |
| 90 | + |
| 91 | +```mermaid |
| 92 | +flowchart LR |
| 93 | + A["about.md\n(src/content/)"] -->|parsed at build time| B["ContentFile\n{ attributes, toc, content }"] |
| 94 | + B -->|injectContent()| C["About component"] |
| 95 | + C -->|analog-markdown| D["Rendered page"] |
| 96 | +``` |
| 97 | + |
| 98 | +And the two ways to load content: |
| 99 | + |
| 100 | +```mermaid |
| 101 | +flowchart TD |
| 102 | + subgraph Fixed["Fixed filename"] |
| 103 | + A["injectContent({ customFilename: 'about' })"] --> B["src/content/about.md"] |
| 104 | + end |
| 105 | + subgraph Slug["Slug-based routing"] |
| 106 | + C["injectContent()"] --> D["src/content/[slug].md"] |
| 107 | + end |
| 108 | +``` |
| 109 | + |
| 110 | +For blog-style content, Analog supports resolving files by a route slug parameter instead of a fixed filename. A route like `src/app/pages/blog/posts.[slug].page.ts` can load the matching file from `src/content/posts/`: |
| 111 | + |
| 112 | +```typescript |
| 113 | +// resolves src/content/posts/<slug>.md from the active route |
| 114 | +readonly post$ = injectContent<PostAttributes>(); |
| 115 | +``` |
| 116 | + |
| 117 | +Subdirectory filtering lets you scope `injectContentFiles()` to a specific folder: |
| 118 | + |
| 119 | +```typescript |
| 120 | +readonly posts = injectContentFiles<PostAttributes>( |
| 121 | + (file) => file.filename.includes('/src/content/posts/'), |
| 122 | +); |
| 123 | +``` |
| 124 | + |
| 125 | +This makes it straightforward to build a blog index that lists all posts with their frontmatter, then renders each post on a dedicated route. |
0 commit comments