@kamod-ch/preactpress 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/client/app.d.ts.map +1 -1
  2. package/dist/client/app.js +54 -7
  3. package/dist/client/app.js.map +1 -1
  4. package/dist/client/loadPage.d.ts +1 -0
  5. package/dist/client/loadPage.d.ts.map +1 -1
  6. package/dist/client/loadPage.js +3 -0
  7. package/dist/client/loadPage.js.map +1 -1
  8. package/dist/client/mermaid.d.ts +2 -0
  9. package/dist/client/mermaid.d.ts.map +1 -0
  10. package/dist/client/mermaid.js +68 -0
  11. package/dist/client/mermaid.js.map +1 -0
  12. package/dist/client/theme-default/Layout.d.ts.map +1 -1
  13. package/dist/client/theme-default/Layout.js +43 -0
  14. package/dist/client/theme-default/Layout.js.map +1 -1
  15. package/dist/node/markdown.d.ts.map +1 -1
  16. package/dist/node/markdown.js +24 -6
  17. package/dist/node/markdown.js.map +1 -1
  18. package/dist/shared/scrollRestoration.d.ts +11 -0
  19. package/dist/shared/scrollRestoration.d.ts.map +1 -0
  20. package/dist/shared/scrollRestoration.js +88 -0
  21. package/dist/shared/scrollRestoration.js.map +1 -0
  22. package/dist/shared/search.d.ts.map +1 -1
  23. package/dist/shared/search.js +4 -1
  24. package/dist/shared/search.js.map +1 -1
  25. package/package.json +2 -1
  26. package/src/client/app.tsx +59 -7
  27. package/src/client/loadPage.ts +4 -0
  28. package/src/client/mermaid.ts +74 -0
  29. package/src/client/theme-default/Layout.tsx +43 -0
  30. package/src/client/theme-default/styles.css +181 -5
  31. package/src/shared/scrollRestoration.ts +101 -0
  32. package/src/shared/search.ts +5 -1
  33. package/templates/docs/.preactpress/config.ts +5 -0
  34. package/templates/docs/examples/cloudflare-pages.md +117 -0
  35. package/templates/docs/examples/custom-theme.md +266 -0
  36. package/templates/docs/examples/mermaid.md +53 -0
  37. package/templates/docs/examples/preact-signals.md +102 -0
  38. package/templates/docs/examples/rss.md +104 -0
  39. package/templates/docs/guide/custom-themes.md +1 -1
  40. package/templates/docs/guide/deploy.md +2 -0
  41. package/templates/docs/guide/markdown-and-mdx.md +1 -1
  42. package/templates/docs/markdown-examples.md +10 -0
@@ -0,0 +1,101 @@
1
+ export const SCROLL_STATE_KEY = "ppScrollY";
2
+
3
+ let skipScrollRestoreOnce = false;
4
+
5
+ export function setupScrollRestoration(): () => void {
6
+ if (typeof window === "undefined") return () => {};
7
+
8
+ if ("scrollRestoration" in window.history) {
9
+ window.history.scrollRestoration = "manual";
10
+ }
11
+
12
+ const onScroll = () => {
13
+ persistScrollPosition();
14
+ };
15
+ window.addEventListener("scroll", onScroll, { passive: true });
16
+ window.addEventListener("pagehide", persistScrollPosition);
17
+ return () => {
18
+ window.removeEventListener("scroll", onScroll);
19
+ window.removeEventListener("pagehide", persistScrollPosition);
20
+ };
21
+ }
22
+
23
+ export function persistScrollPosition(): void {
24
+ if (typeof window === "undefined") return;
25
+ const scrollY = window.scrollY;
26
+ const state = window.history.state ?? {};
27
+ if (state[SCROLL_STATE_KEY] === scrollY) return;
28
+ window.history.replaceState({ ...state, [SCROLL_STATE_KEY]: scrollY }, "");
29
+ }
30
+
31
+ export function saveScrollPositionBeforeNavigation(): void {
32
+ persistScrollPosition();
33
+ }
34
+
35
+ export function readScrollPositionFromHistory(): number {
36
+ if (typeof window === "undefined") return 0;
37
+ const scrollY = window.history.state?.[SCROLL_STATE_KEY];
38
+ return typeof scrollY === "number" && Number.isFinite(scrollY) ? scrollY : 0;
39
+ }
40
+
41
+ export function restoreScrollPosition(): void {
42
+ if (typeof window === "undefined") return;
43
+ window.scrollTo({ top: readScrollPositionFromHistory(), left: 0, behavior: "auto" });
44
+ }
45
+
46
+ export async function restoreScrollPositionAfterLayout(): Promise<void> {
47
+ await scrollAfterLayout(readScrollPositionFromHistory());
48
+ }
49
+
50
+ export async function scrollToTopAfterLayout(): Promise<void> {
51
+ await scrollAfterLayout(0);
52
+ }
53
+
54
+ async function scrollAfterLayout(top: number): Promise<void> {
55
+ if (typeof window === "undefined") return;
56
+ await nextAnimationFrame();
57
+ await nextAnimationFrame();
58
+ await waitForImagesToSettle();
59
+ await nextAnimationFrame();
60
+
61
+ const html = document.documentElement;
62
+ const previous = html.style.scrollBehavior;
63
+ html.style.scrollBehavior = "auto";
64
+ window.scrollTo({ top, left: 0, behavior: "auto" });
65
+ html.style.scrollBehavior = previous;
66
+ }
67
+
68
+ function nextAnimationFrame(): Promise<void> {
69
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
70
+ }
71
+
72
+ async function waitForImagesToSettle(timeoutMs = 1500): Promise<void> {
73
+ if (typeof document === "undefined") return;
74
+ const images = Array.from(document.images).filter((image) => !image.complete);
75
+ if (images.length === 0) return;
76
+
77
+ await Promise.race([
78
+ Promise.allSettled(
79
+ images.map(
80
+ (image) =>
81
+ new Promise<void>((resolve) => {
82
+ image.addEventListener("load", () => resolve(), { once: true });
83
+ image.addEventListener("error", () => resolve(), { once: true });
84
+ }),
85
+ ),
86
+ ),
87
+ new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
88
+ ]);
89
+ }
90
+
91
+ export function skipNextScrollRestore(): void {
92
+ skipScrollRestoreOnce = true;
93
+ }
94
+
95
+ export function consumeScrollRestoreOnPopstate(): boolean {
96
+ if (skipScrollRestoreOnce) {
97
+ skipScrollRestoreOnce = false;
98
+ return false;
99
+ }
100
+ return true;
101
+ }
@@ -1,3 +1,5 @@
1
+ import { saveScrollPositionBeforeNavigation, skipNextScrollRestore } from "./scrollRestoration.js";
2
+
1
3
  export type SearchProvider = "local" | "algolia";
2
4
 
3
5
  export interface LocalSearchConfig {
@@ -99,7 +101,9 @@ export function navigateDocSearchResult(itemUrl: string, base: string): void {
99
101
  window.location.assign(href);
100
102
  return;
101
103
  }
104
+ saveScrollPositionBeforeNavigation();
102
105
  window.history.pushState({}, "", href);
106
+ skipNextScrollRestore();
103
107
  window.dispatchEvent(new PopStateEvent("popstate"));
104
- window.scrollTo({ top: 0 });
108
+ window.scrollTo({ top: 0, behavior: "auto" });
105
109
  }
@@ -77,9 +77,14 @@ export default defineConfig({
77
77
  items: [
78
78
  { text: "Markdown examples", link: "/markdown-examples" },
79
79
  { text: "Interactive MDX", link: "/interactive" },
80
+ { text: "Preact Signals", link: "/examples/preact-signals" },
81
+ { text: "Mermaid diagrams", link: "/examples/mermaid" },
82
+ { text: "RSS / Atom feed", link: "/examples/rss" },
80
83
  { text: "Algolia DocSearch", link: "/examples/algolia-docsearch" },
81
84
  { text: "Content loader", link: "/examples/content-loader" },
82
85
  { text: "Dynamic routes", link: "/examples/dynamic-routes" },
86
+ { text: "Custom theme", link: "/examples/custom-theme" },
87
+ { text: "Cloudflare Pages", link: "/examples/cloudflare-pages" },
83
88
  { text: "Static assets", link: "/examples/static-assets" },
84
89
  ],
85
90
  },
@@ -0,0 +1,117 @@
1
+ ---
2
+ title: Cloudflare Pages deployment
3
+ description: Deploy a PreactPress static site to Cloudflare Pages.
4
+ tags:
5
+ - deploy
6
+ - examples
7
+ ---
8
+
9
+ PreactPress outputs static files, so Cloudflare Pages can serve the generated `dist/` directory without a Node server.
10
+
11
+ ## Prerequisites
12
+
13
+ - A PreactPress site committed to GitHub or GitLab
14
+ - Node.js 20 or newer
15
+ - A package script like:
16
+
17
+ ```json [package.json]
18
+ {
19
+ "scripts": {
20
+ "check": "preactpress check",
21
+ "build": "preactpress build"
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## Configure PreactPress
27
+
28
+ Set the final production URL in `.preactpress/config.ts`:
29
+
30
+ ```ts [.preactpress/config.ts]
31
+ import { defineConfig } from "@kamod-ch/preactpress/config";
32
+
33
+ export default defineConfig({
34
+ site: {
35
+ title: "My Docs",
36
+ description: "Documentation built with PreactPress",
37
+ url: "https://docs.example.com",
38
+ base: "/",
39
+ },
40
+ });
41
+ ```
42
+
43
+ Use `base: "/"` for normal Cloudflare Pages deployments. Only change `base` if you intentionally serve the site from a subpath.
44
+
45
+ ## Deploy with the Cloudflare dashboard
46
+
47
+ 1. Open **Cloudflare Dashboard → Workers & Pages → Create application → Pages**.
48
+ 2. Connect your GitHub or GitLab repository.
49
+ 3. Choose your PreactPress project.
50
+ 4. Use these build settings:
51
+
52
+ | Setting | Value |
53
+ | ------------------- | ---------------- |
54
+ | Framework preset | `None` / custom |
55
+ | Build command | `pnpm run build` |
56
+ | Build output folder | `dist` |
57
+ | Root directory | project root |
58
+
59
+ If your docs live in a monorepo subfolder, set **Root directory** to that folder, for example:
60
+
61
+ ```txt
62
+ packages/docs
63
+ ```
64
+
65
+ ## Environment variables
66
+
67
+ Set the Node version in Cloudflare Pages:
68
+
69
+ | Variable | Value |
70
+ | -------------- | ----- |
71
+ | `NODE_VERSION` | `22` |
72
+
73
+ Cloudflare Pages usually detects pnpm from `pnpm-lock.yaml`. If needed, set:
74
+
75
+ | Variable | Value |
76
+ | -------------- | --------- |
77
+ | `PNPM_VERSION` | `10.12.4` |
78
+
79
+ ## Optional: run checks before build
80
+
81
+ For stricter deployments, use this build command:
82
+
83
+ ```sh
84
+ pnpm run check && pnpm run build
85
+ ```
86
+
87
+ This fails the deployment when routes, nav links, sidebar links, or internal Markdown links are invalid.
88
+
89
+ ## Deploy with Wrangler
90
+
91
+ You can also deploy the built `dist/` directory manually with Wrangler:
92
+
93
+ ```sh
94
+ pnpm run check
95
+ pnpm run build
96
+ pnpm dlx wrangler pages deploy dist --project-name my-preactpress-site
97
+ ```
98
+
99
+ For a monorepo, run those commands from the site package directory or pass the correct output path.
100
+
101
+ ## Custom domains
102
+
103
+ After the first deployment, add your domain in **Pages → Custom domains**. Then update `site.url` to match the production domain:
104
+
105
+ ```ts
106
+ export default defineConfig({
107
+ site: {
108
+ url: "https://docs.example.com",
109
+ },
110
+ });
111
+ ```
112
+
113
+ This keeps canonical URLs, Open Graph metadata, `sitemap.xml`, `robots.txt`, and feeds correct.
114
+
115
+ ## Cache notes
116
+
117
+ Cloudflare can cache hashed files under `assets/` aggressively because their filenames change when content changes. Avoid immutable caching for HTML, `preactpress-search.json`, and `preactpress-content/*.json`.
@@ -0,0 +1,266 @@
1
+ ---
2
+ title: Custom theme example
3
+ description: A minimal copy-pasteable PreactPress theme with navigation, Markdown, MDX, and styling.
4
+ ---
5
+
6
+ This example shows a small custom theme you can copy into your project. For a larger production-style example, scaffold the bundled magazine template:
7
+
8
+ ```sh
9
+ pnpm exec preactpress init my-magazine --template magazine
10
+ ```
11
+
12
+ ## File structure
13
+
14
+ ```txt
15
+ .preactpress/
16
+ config.ts
17
+ theme/
18
+ Layout.tsx
19
+ theme.css
20
+ index.md
21
+ about.md
22
+ ```
23
+
24
+ ## Configure the theme
25
+
26
+ ```ts [.preactpress/config.ts]
27
+ import { defineConfig } from "@kamod-ch/preactpress/config";
28
+
29
+ export default defineConfig({
30
+ site: {
31
+ title: "Acme Docs",
32
+ description: "A custom themed PreactPress site",
33
+ },
34
+
35
+ // Relative to .preactpress/
36
+ theme: "./theme/Layout.tsx",
37
+
38
+ themeConfig: {
39
+ nav: [
40
+ { text: "Home", link: "/" },
41
+ { text: "About", link: "/about" },
42
+ ],
43
+ footer: "Built with PreactPress",
44
+ },
45
+ });
46
+ ```
47
+
48
+ ## Layout component
49
+
50
+ ```tsx [.preactpress/theme/Layout.tsx]
51
+ import type { FunctionalComponent } from "preact";
52
+ import {
53
+ createMdxHeadingComponents,
54
+ isActive,
55
+ withBase,
56
+ type LayoutProps,
57
+ } from "@kamod-ch/preactpress/client";
58
+ import "./theme.css";
59
+
60
+ const Layout: FunctionalComponent<LayoutProps> = ({ site, themeConfig, routePath, page }) => {
61
+ const MdxComponent = page?.kind === "mdx" ? page.Component : undefined;
62
+ const mdxComponents = createMdxHeadingComponents({
63
+ headingClass: "ct-heading",
64
+ anchorClass: "ct-heading-anchor",
65
+ anchorLabel: "Link to this section",
66
+ });
67
+
68
+ return (
69
+ <div class="ct-layout">
70
+ <a class="ct-skip" href="#content">
71
+ Skip to content
72
+ </a>
73
+
74
+ <header class="ct-header">
75
+ <a class="ct-brand" href={withBase(site.base, "/")}>
76
+ {site.title}
77
+ </a>
78
+ <nav class="ct-nav" aria-label="Main navigation">
79
+ {(themeConfig.nav ?? []).map((item) => {
80
+ const active = isActive(routePath, item.link);
81
+ return (
82
+ <a
83
+ key={item.link}
84
+ href={withBase(site.base, item.link)}
85
+ class={active ? "active" : ""}
86
+ aria-current={active ? "page" : undefined}
87
+ >
88
+ {item.text}
89
+ </a>
90
+ );
91
+ })}
92
+ </nav>
93
+ </header>
94
+
95
+ <main id="content" class="ct-main" tabIndex={-1}>
96
+ <article class="ct-article">
97
+ <h1>{page?.title ?? site.title}</h1>
98
+ {page?.description ? <p class="ct-lead">{page.description}</p> : null}
99
+
100
+ {MdxComponent ? (
101
+ <div class="ct-content">
102
+ <MdxComponent components={mdxComponents} />
103
+ </div>
104
+ ) : (
105
+ <div
106
+ class="ct-content"
107
+ dangerouslySetInnerHTML={{ __html: page?.kind === "markdown" ? page.html : "" }}
108
+ />
109
+ )}
110
+ </article>
111
+ </main>
112
+
113
+ {themeConfig.footer ? <footer class="ct-footer">{themeConfig.footer}</footer> : null}
114
+ </div>
115
+ );
116
+ };
117
+
118
+ export default Layout;
119
+ ```
120
+
121
+ ## Styling
122
+
123
+ ```css [.preactpress/theme/theme.css]
124
+ :root {
125
+ color-scheme: light dark;
126
+ --ct-bg: #f7f3ea;
127
+ --ct-panel: #fffaf0;
128
+ --ct-text: #1f2937;
129
+ --ct-muted: #6b7280;
130
+ --ct-border: #eadfca;
131
+ --ct-accent: #b45309;
132
+ }
133
+
134
+ @media (prefers-color-scheme: dark) {
135
+ :root {
136
+ --ct-bg: #15110b;
137
+ --ct-panel: #1f1a12;
138
+ --ct-text: #f8ecd5;
139
+ --ct-muted: #c5bba9;
140
+ --ct-border: #3b3022;
141
+ --ct-accent: #fbbf24;
142
+ }
143
+ }
144
+
145
+ body {
146
+ margin: 0;
147
+ background: var(--ct-bg);
148
+ color: var(--ct-text);
149
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
150
+ }
151
+
152
+ .ct-skip {
153
+ position: absolute;
154
+ left: 1rem;
155
+ top: -4rem;
156
+ z-index: 10;
157
+ background: var(--ct-accent);
158
+ color: #111827;
159
+ padding: 0.5rem 0.75rem;
160
+ border-radius: 999px;
161
+ }
162
+
163
+ .ct-skip:focus {
164
+ top: 1rem;
165
+ }
166
+
167
+ .ct-header {
168
+ max-width: 1040px;
169
+ margin: 0 auto;
170
+ padding: 1.25rem;
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: space-between;
174
+ gap: 1rem;
175
+ }
176
+
177
+ .ct-brand {
178
+ color: inherit;
179
+ font-weight: 800;
180
+ text-decoration: none;
181
+ letter-spacing: -0.04em;
182
+ }
183
+
184
+ .ct-nav {
185
+ display: flex;
186
+ gap: 0.35rem;
187
+ }
188
+
189
+ .ct-nav a {
190
+ color: var(--ct-muted);
191
+ text-decoration: none;
192
+ padding: 0.45rem 0.7rem;
193
+ border-radius: 999px;
194
+ }
195
+
196
+ .ct-nav a:hover,
197
+ .ct-nav a.active {
198
+ background: color-mix(in srgb, var(--ct-accent) 16%, transparent);
199
+ color: var(--ct-text);
200
+ }
201
+
202
+ .ct-main {
203
+ max-width: 1040px;
204
+ margin: 0 auto;
205
+ padding: 1.25rem;
206
+ }
207
+
208
+ .ct-article {
209
+ background: var(--ct-panel);
210
+ border: 1px solid var(--ct-border);
211
+ border-radius: 28px;
212
+ padding: clamp(1.5rem, 4vw, 3rem);
213
+ box-shadow: 0 24px 70px rgb(0 0 0 / 0.08);
214
+ }
215
+
216
+ .ct-article h1 {
217
+ margin: 0;
218
+ font-size: clamp(2.4rem, 8vw, 5rem);
219
+ letter-spacing: -0.07em;
220
+ line-height: 0.95;
221
+ }
222
+
223
+ .ct-lead {
224
+ color: var(--ct-muted);
225
+ font-size: 1.2rem;
226
+ }
227
+
228
+ .ct-content {
229
+ line-height: 1.75;
230
+ }
231
+
232
+ .ct-content a {
233
+ color: var(--ct-accent);
234
+ }
235
+
236
+ .ct-content pre {
237
+ overflow: auto;
238
+ padding: 1rem;
239
+ border-radius: 16px;
240
+ background: #111827;
241
+ color: #f9fafb;
242
+ }
243
+
244
+ .ct-heading-anchor {
245
+ opacity: 0;
246
+ margin-left: 0.4rem;
247
+ color: var(--ct-muted);
248
+ text-decoration: none;
249
+ }
250
+
251
+ .ct-heading:hover .ct-heading-anchor,
252
+ .ct-heading-anchor:focus-visible {
253
+ opacity: 1;
254
+ }
255
+
256
+ .ct-footer {
257
+ max-width: 1040px;
258
+ margin: 0 auto;
259
+ padding: 2rem 1.25rem;
260
+ color: var(--ct-muted);
261
+ }
262
+ ```
263
+
264
+ ## Notes
265
+
266
+ A custom theme receives the same `LayoutProps` as the default theme. Your layout is responsible for rendering Markdown versus MDX pages, navigation, responsive behavior, search UI, focus management, and any theme-specific styling.
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: Mermaid diagrams
3
+ description: Render flowcharts, sequence diagrams, and other Mermaid diagrams in Markdown and MDX.
4
+ tags:
5
+ - examples
6
+ - markdown
7
+ ---
8
+
9
+ PreactPress renders Mermaid diagrams from fenced code blocks. Use the `mermaid` language name:
10
+
11
+ ````md
12
+ ```mermaid
13
+ graph TD
14
+ A[Markdown] --> B[PreactPress]
15
+ B --> C[Static HTML]
16
+ ```
17
+ ````
18
+
19
+ ## Flowchart
20
+
21
+ ```mermaid
22
+ graph TD
23
+ A[Markdown] --> B[PreactPress]
24
+ B --> C[Static HTML]
25
+ C --> D[Fast docs site]
26
+ ```
27
+
28
+ ## Sequence diagram
29
+
30
+ ```mermaid
31
+ sequenceDiagram
32
+ participant User
33
+ participant Site as PreactPress Site
34
+ participant Mermaid
35
+
36
+ User->>Site: Open docs page
37
+ Site->>Mermaid: Render diagram block
38
+ Mermaid-->>Site: SVG diagram
39
+ Site-->>User: Interactive documentation
40
+ ```
41
+
42
+ ## State diagram
43
+
44
+ ```mermaid
45
+ stateDiagram-v2
46
+ [*] --> Draft
47
+ Draft --> Review
48
+ Review --> Published
49
+ Review --> Draft
50
+ Published --> [*]
51
+ ```
52
+
53
+ Mermaid is loaded on the client only when a page contains a diagram.
@@ -0,0 +1,102 @@
1
+ ---
2
+ title: Preact Signals
3
+ description: Use @preact/signals in MDX components and custom PreactPress themes.
4
+ tags:
5
+ - examples
6
+ - mdx
7
+ ---
8
+
9
+ PreactPress is built on Preact, so you can use `@preact/signals` in MDX components and custom themes.
10
+
11
+ Install Signals in your site project:
12
+
13
+ ```sh
14
+ pnpm add @preact/signals
15
+ ```
16
+
17
+ ## Component example
18
+
19
+ Create a normal Preact component:
20
+
21
+ ```tsx [components/SignalCounter.tsx]
22
+ import { signal } from "@preact/signals";
23
+
24
+ const count = signal(0);
25
+
26
+ export default function SignalCounter() {
27
+ return (
28
+ <button type="button" onClick={() => count.value++}>
29
+ Count: {count}
30
+ </button>
31
+ );
32
+ }
33
+ ```
34
+
35
+ Then import it from an MDX page:
36
+
37
+ ```mdx [interactive.mdx]
38
+ import SignalCounter from "./components/SignalCounter";
39
+
40
+ # Signals demo
41
+
42
+ <SignalCounter />
43
+ ```
44
+
45
+ ## Derived values
46
+
47
+ Use `computed` for derived state:
48
+
49
+ ```tsx
50
+ import { computed, signal } from "@preact/signals";
51
+
52
+ const count = signal(0);
53
+ const doubled = computed(() => count.value * 2);
54
+
55
+ export default function DoubledCounter() {
56
+ return (
57
+ <div>
58
+ <button type="button" onClick={() => count.value++}>
59
+ Count: {count}
60
+ </button>
61
+ <p>Doubled: {doubled}</p>
62
+ </div>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## Custom theme usage
68
+
69
+ Signals also work inside a custom theme:
70
+
71
+ ```tsx [.preactpress/theme/Layout.tsx]
72
+ import type { FunctionalComponent } from "preact";
73
+ import { signal } from "@preact/signals";
74
+ import type { LayoutProps } from "@kamod-ch/preactpress/client";
75
+
76
+ const menuOpen = signal(false);
77
+
78
+ const Layout: FunctionalComponent<LayoutProps> = ({ site, page }) => (
79
+ <div>
80
+ <header>
81
+ <a href="/">{site.title}</a>
82
+ <button type="button" onClick={() => (menuOpen.value = !menuOpen.value)}>
83
+ Menu
84
+ </button>
85
+ </header>
86
+
87
+ {menuOpen.value ? <nav>Navigation…</nav> : null}
88
+
89
+ <main id="content">
90
+ {page?.kind === "markdown" ? (
91
+ <article dangerouslySetInnerHTML={{ __html: page.html }} />
92
+ ) : page?.kind === "mdx" ? (
93
+ <page.Component />
94
+ ) : null}
95
+ </main>
96
+ </div>
97
+ );
98
+
99
+ export default Layout;
100
+ ```
101
+
102
+ No special PreactPress configuration is required beyond installing `@preact/signals`.