Skip to content

Commit ad5cde9

Browse files
feat: update about page to demo Analog content feature, add nav link (closes #150)
- Rewrite about.md to explain and demonstrate the Analog.js content feature (injectContent, frontmatter, TOC, Shiki highlighting, injectContentFiles, slug routing, Mermaid diagrams) - Update About component to use toSignal, display frontmatter attributes as a hero header, render content.toc as an 'On this page' nav, and show injectContentFiles() results in a footer panel - Install mermaid and enable it via withMarkdownRenderer({ loadMermaid }) - Configure Shiki to skipLangs: ['mermaid'] so Mermaid blocks use the runtime SVG renderer - Add About link to NAV_LINKS (toolbar + sidenav) - Add Analog.js to README/home.md features and tech stack sections - Update e2e specs to match new about page structure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 18d5162 commit ad5cde9

11 files changed

Lines changed: 365 additions & 286 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://
99
- **Authentication** — register, login, and logout with JWT bearer tokens backed by ASP.NET Core Identity
1010
- **Notification center** — persistent notification panel with unread count, mark-as-read, dismiss, and action support (e.g. one-click reload on SW update)
1111
- **PWA / service worker** — offline support; notifies users when a new app version is available with an in-app prompt to reload
12+
- **Markdown content pages**[Analog.js](https://analogjs.org) content feature renders pages from Markdown files with frontmatter support (see the [About](/about) page for a live demo)
1213
- **Debug page** (`/debug`) — trigger test notifications and inspect service worker update state during development
1314
- **PR preview deployments** — every pull request gets a live preview URL via Azure Static Web Apps
1415

@@ -19,6 +20,7 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://
1920
- [Angular 21](https://angular.dev) — zoneless change detection, standalone components, signals
2021
- [NgRx Signal Store](https://ngrx.io/guide/signals) — reactive state management
2122
- [Angular Material](https://material.angular.io) — UI component library
23+
- [Analog.js](https://analogjs.org) — Vite-native Angular meta-framework; used for file-based Markdown content pages
2224
- [Tailwind CSS v4](https://tailwindcss.com) — utility-first styling
2325
- [Angular PWA](https://angular.dev/ecosystem/service-workers) — service worker & offline support
2426

apps/web-app-e2e/src/about.e2e.spec.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,25 @@ test.describe('About page', () => {
77
await expect(page.getByTestId('app-about')).toBeVisible();
88
});
99

10-
test('should render the about heading', async ({ page }) => {
10+
test('should render the page title from frontmatter', async ({ page }) => {
1111
await page.goto('/about');
1212

1313
await expect(
14-
page.getByRole('heading', { name: /about this starter/i }),
14+
page.getByRole('heading', { name: /content pages/i }),
1515
).toBeVisible();
1616
});
1717

18-
test('should display the tech stack section', async ({ page }) => {
18+
test('should display the table of contents', async ({ page }) => {
1919
await page.goto('/about');
2020

2121
await expect(
22-
page.getByRole('heading', { name: /tech stack/i }),
22+
page.getByRole('navigation', { name: /on this page/i }),
2323
).toBeVisible();
2424
});
25+
26+
test('should display the content files panel', async ({ page }) => {
27+
await page.goto('/about');
28+
29+
await expect(page.getByText(/content files in this app/i)).toBeVisible();
30+
});
2531
});

apps/web-app/src/app/about/about.ts

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,105 @@
1-
import { AsyncPipe } from '@angular/common';
21
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
3-
import { MarkdownComponent, injectContent } from '@analogjs/content';
2+
import { toSignal } from '@angular/core/rxjs-interop';
3+
import {
4+
MarkdownComponent,
5+
injectContent,
6+
injectContentFiles,
7+
} from '@analogjs/content';
48
import { LayoutStore } from '@myorg/shared';
59

10+
interface AboutAttributes {
11+
title: string;
12+
description: string;
13+
}
14+
615
@Component({
7-
imports: [AsyncPipe, MarkdownComponent],
16+
imports: [MarkdownComponent],
817
selector: 'app-about',
918
template: `
10-
<div class="max-w-3xl mx-auto px-8 py-8">
11-
<div class="doc-prose prose max-w-none">
12-
@if (content$ | async; as content) {
19+
@if (content(); as content) {
20+
<!-- Hero from frontmatter -->
21+
<div
22+
class="border-b border-outline-variant/30 bg-surface-container-low dark:bg-surface-container"
23+
>
24+
<div class="max-w-3xl mx-auto px-8 py-10">
25+
<p
26+
class="mb-3 text-xs font-semibold uppercase tracking-widest text-primary"
27+
>
28+
Analog.js Content Feature
29+
</p>
30+
<h1
31+
class="mb-4 font-display text-4xl font-bold leading-snug text-on-surface md:text-5xl"
32+
>
33+
{{ content.attributes.title }}
34+
</h1>
35+
<p class="max-w-xl text-base leading-relaxed text-on-surface-variant">
36+
{{ content.attributes.description }}
37+
</p>
38+
</div>
39+
</div>
40+
41+
<div class="max-w-3xl mx-auto px-8 py-8">
42+
<!-- Table of Contents from content.toc -->
43+
@if (content.toc && content.toc.length > 0) {
44+
<nav
45+
class="mb-8 rounded-lg border border-outline-variant/40 bg-surface-container-low p-4"
46+
aria-label="On this page"
47+
>
48+
<p class="mb-2 text-sm font-semibold text-on-surface">
49+
On this page
50+
</p>
51+
<ul class="space-y-1">
52+
@for (item of content.toc; track item.id) {
53+
<li
54+
[class.pl-4]="item.level === 3"
55+
[class.pl-8]="item.level === 4"
56+
>
57+
<a
58+
[href]="'#' + item.id"
59+
class="text-sm text-on-surface-variant hover:text-primary transition-colors no-underline"
60+
>{{ item.text }}</a
61+
>
62+
</li>
63+
}
64+
</ul>
65+
</nav>
66+
}
67+
68+
<!-- Rendered markdown -->
69+
<div class="doc-prose prose max-w-none">
1370
<analog-markdown [content]="content.content" />
71+
</div>
72+
73+
<!-- injectContentFiles() demo -->
74+
@if (contentFiles.length > 0) {
75+
<div
76+
class="mt-10 rounded-lg border border-outline-variant/40 bg-surface-container-low p-4"
77+
>
78+
<p class="mb-1 text-sm font-semibold text-on-surface">
79+
Content files in this app
80+
<span class="ml-1 font-normal text-on-surface-variant"
81+
>(via <code class="text-xs">injectContentFiles()</code>)</span
82+
>
83+
</p>
84+
<p class="mb-3 text-xs text-on-surface-variant">
85+
Resolved at build time — no API call needed.
86+
</p>
87+
<ul class="space-y-1">
88+
@for (file of contentFiles; track file.slug) {
89+
<li class="text-sm">
90+
<code class="text-primary">{{ file.filename }}</code>
91+
@if (file.attributes.title) {
92+
<span class="text-on-surface-variant">
93+
— {{ file.attributes.title }}</span
94+
>
95+
}
96+
</li>
97+
}
98+
</ul>
99+
</div>
14100
}
15101
</div>
16-
</div>
102+
}
17103
`,
18104
host: {
19105
class: 'block',
@@ -23,7 +109,11 @@ import { LayoutStore } from '@myorg/shared';
23109
})
24110
export class About {
25111
private readonly layoutStore = inject(LayoutStore);
26-
readonly content$ = injectContent({ customFilename: 'about' });
112+
113+
readonly content = toSignal(
114+
injectContent<AboutAttributes>({ customFilename: 'about' }),
115+
);
116+
readonly contentFiles = injectContentFiles<AboutAttributes>();
27117

28118
constructor() {
29119
this.layoutStore.setTitle('About');

apps/web-app/src/app/app.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export const appConfig: ApplicationConfig = {
3535
withPreloading(PreloadAllModules),
3636
),
3737
provideAnimations(),
38-
provideContent(withMarkdownRenderer()),
38+
provideContent(
39+
withMarkdownRenderer({
40+
loadMermaid: () => import('mermaid'),
41+
}),
42+
),
3943
],
4044
};

apps/web-app/src/assets/home.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://
77
- **Authentication** — register, login, and logout with JWT bearer tokens backed by ASP.NET Core Identity
88
- **Notification center** — persistent notification panel with unread count, mark-as-read, dismiss, and action support (e.g. one-click reload on SW update)
99
- **PWA / service worker** — offline support; notifies users when a new app version is available with an in-app prompt to reload
10+
- **Markdown content pages**[Analog.js](https://analogjs.org) content feature renders pages from Markdown files with frontmatter support (see the [About](/about) page for a live demo)
1011
- **Debug page** (`/debug`) — trigger test notifications and inspect service worker update state during development
1112
- **PR preview deployments** — every pull request gets a live preview URL via Azure Static Web Apps
1213

@@ -17,6 +18,7 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://
1718
- [Angular 21](https://angular.dev) — zoneless change detection, standalone components, signals
1819
- [NgRx Signal Store](https://ngrx.io/guide/signals) — reactive state management
1920
- [Angular Material](https://material.angular.io) — UI component library
21+
- [Analog.js](https://analogjs.org) — Vite-native Angular meta-framework; used for file-based Markdown content pages
2022
- [Tailwind CSS v4](https://tailwindcss.com) — utility-first styling
2123
- [Angular PWA](https://angular.dev/ecosystem/service-workers) — service worker & offline support
2224

apps/web-app/src/content/about.md

Lines changed: 105 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,125 @@
11
---
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.
44
---
55

6-
## About This Starter
6+
## How This Page Is Built
77

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`.
99

10-
### Tech Stack
10+
```typescript
11+
import { MarkdownComponent, injectContent } from '@analogjs/content';
12+
import { toSignal } from '@angular/core/rxjs-interop';
1113

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.
1333

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
1935

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`:
2137

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+
```
2345

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.
2547

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
3049

31-
### Features
50+
Code blocks are highlighted at build time using [Shiki](https://shiki.style/), configured in `vite.config.ts`:
3251

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+
```
4064

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.
4266

43-
Clone the repository and install dependencies:
67+
## Listing All Content Files
4468

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>();
4973
```
5074

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()`:
5280

53-
```bash
54-
pnpm nx serve web-app
81+
```typescript
82+
provideContent(
83+
withMarkdownRenderer({
84+
loadMermaid: () => import('mermaid'),
85+
}),
86+
);
5587
```
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.

apps/web-app/src/content/home.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://
77
- **Authentication** — register, login, and logout with JWT bearer tokens backed by ASP.NET Core Identity
88
- **Notification center** — persistent notification panel with unread count, mark-as-read, dismiss, and action support (e.g. one-click reload on SW update)
99
- **PWA / service worker** — offline support; notifies users when a new app version is available with an in-app prompt to reload
10+
- **Markdown content pages**[Analog.js](https://analogjs.org) content feature renders pages from Markdown files with frontmatter support (see the [About](/about) page for a live demo)
1011
- **Debug page** (`/debug`) — trigger test notifications and inspect service worker update state during development
1112
- **PR preview deployments** — every pull request gets a live preview URL via Azure Static Web Apps
1213

@@ -17,6 +18,7 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://
1718
- [Angular 21](https://angular.dev) — zoneless change detection, standalone components, signals
1819
- [NgRx Signal Store](https://ngrx.io/guide/signals) — reactive state management
1920
- [Angular Material](https://material.angular.io) — UI component library
21+
- [Analog.js](https://analogjs.org) — Vite-native Angular meta-framework; used for file-based Markdown content pages
2022
- [Tailwind CSS v4](https://tailwindcss.com) — utility-first styling
2123
- [Angular PWA](https://angular.dev/ecosystem/service-workers) — service worker & offline support
2224

apps/web-app/vite.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export default defineConfig(({ mode }) => {
3131
highlighter: 'shiki',
3232
shikiOptions: {
3333
highlighter: {
34-
additionalLangs: ['bash', 'shell', 'yaml'],
34+
additionalLangs: ['bash', 'shell', 'yaml', 'mermaid'],
35+
skipLangs: ['mermaid'],
3536
},
3637
},
3738
},

libs/shared/src/lib/components/nav-links.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,10 @@ export const NAV_LINKS: NavLink[] = [
2424
hint: 'Lazy Loaded Feature',
2525
label: 'Counter',
2626
},
27+
{
28+
routerLink: '/about',
29+
icon: 'info',
30+
hint: 'About',
31+
label: 'About',
32+
},
2733
];

0 commit comments

Comments
 (0)