@kamod-ch/preactpress 1.0.3 → 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 (33) hide show
  1. package/dist/client/app.d.ts.map +1 -1
  2. package/dist/client/app.js +34 -4
  3. package/dist/client/app.js.map +1 -1
  4. package/dist/client/mermaid.d.ts +2 -0
  5. package/dist/client/mermaid.d.ts.map +1 -0
  6. package/dist/client/mermaid.js +68 -0
  7. package/dist/client/mermaid.js.map +1 -0
  8. package/dist/client/theme-default/Layout.d.ts.map +1 -1
  9. package/dist/client/theme-default/Layout.js +43 -0
  10. package/dist/client/theme-default/Layout.js.map +1 -1
  11. package/dist/node/markdown.d.ts.map +1 -1
  12. package/dist/node/markdown.js +24 -6
  13. package/dist/node/markdown.js.map +1 -1
  14. package/dist/shared/scrollRestoration.d.ts +2 -1
  15. package/dist/shared/scrollRestoration.d.ts.map +1 -1
  16. package/dist/shared/scrollRestoration.js +36 -14
  17. package/dist/shared/scrollRestoration.js.map +1 -1
  18. package/package.json +2 -1
  19. package/src/client/app.tsx +33 -3
  20. package/src/client/mermaid.ts +74 -0
  21. package/src/client/theme-default/Layout.tsx +43 -0
  22. package/src/client/theme-default/styles.css +181 -5
  23. package/src/shared/scrollRestoration.ts +45 -14
  24. package/templates/docs/.preactpress/config.ts +5 -0
  25. package/templates/docs/examples/cloudflare-pages.md +117 -0
  26. package/templates/docs/examples/custom-theme.md +266 -0
  27. package/templates/docs/examples/mermaid.md +53 -0
  28. package/templates/docs/examples/preact-signals.md +102 -0
  29. package/templates/docs/examples/rss.md +104 -0
  30. package/templates/docs/guide/custom-themes.md +1 -1
  31. package/templates/docs/guide/deploy.md +2 -0
  32. package/templates/docs/guide/markdown-and-mdx.md +1 -1
  33. package/templates/docs/markdown-examples.md +10 -0
@@ -1 +1 @@
1
- {"version":3,"file":"scrollRestoration.js","sourceRoot":"","sources":["../../src/shared/scrollRestoration.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scrollRestoration.js","sourceRoot":"","sources":["../../src/shared/scrollRestoration.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kamod-ch/preactpress",
3
- "version": "1.0.3",
3
+ "version": "2.0.0",
4
4
  "description": "Vite and Preact powered static site generator",
5
5
  "keywords": [
6
6
  "markdown",
@@ -103,6 +103,7 @@
103
103
  "markdown-it": "^14.2.0",
104
104
  "markdown-it-emoji": "^3.0.0",
105
105
  "markdown-it-mathjax3": "^5.2.0",
106
+ "mermaid": "^11.16.0",
106
107
  "minimist": "^1.2.8",
107
108
  "node-html-parser": "^7.1.0",
108
109
  "picocolors": "^1.1.1",
@@ -10,9 +10,11 @@ import {
10
10
  consumeScrollRestoreOnPopstate,
11
11
  restoreScrollPositionAfterLayout,
12
12
  saveScrollPositionBeforeNavigation,
13
+ scrollToTopAfterLayout,
13
14
  setupScrollRestoration,
14
15
  } from "../shared/scrollRestoration.js";
15
16
  import { getCachedPage, loadPage, prefetchPage, seedPage } from "./loadPage.js";
17
+ import { renderMermaidDiagrams } from "./mermaid.js";
16
18
  import { setupViewportPrefetch } from "./prefetchLinks.js";
17
19
  import {
18
20
  localeFromRoute,
@@ -62,6 +64,9 @@ export function App({ routePath, initialPage }: { routePath: string; initialPage
62
64
  const [currentRoute, setCurrentRoute] = useState(() => normalizeRoute(routePath));
63
65
  const [page, setPage] = useState<PageView>(() => initialPage ?? loadingPage(routePath));
64
66
  const pendingScrollRestore = useRef(false);
67
+ const pendingScrollTop = useRef(false);
68
+ const [scrollRestoreNonce, setScrollRestoreNonce] = useState(0);
69
+ const [scrollTopNonce, setScrollTopNonce] = useState(0);
65
70
  const availableRoutes = new Set(routes);
66
71
  const activeLocale = localeFromRoute(currentRoute, i18n);
67
72
  const activeSite = siteForRoute(site, currentRoute, i18n);
@@ -81,7 +86,10 @@ export function App({ routePath, initialPage }: { routePath: string; initialPage
81
86
  setPage(loaded);
82
87
  if (pendingScrollRestore.current) {
83
88
  pendingScrollRestore.current = false;
84
- restoreScrollPositionAfterLayout();
89
+ setScrollRestoreNonce((nonce) => nonce + 1);
90
+ } else if (pendingScrollTop.current) {
91
+ pendingScrollTop.current = false;
92
+ setScrollTopNonce((nonce) => nonce + 1);
85
93
  }
86
94
  }
87
95
  })
@@ -95,6 +103,13 @@ export function App({ routePath, initialPage }: { routePath: string; initialPage
95
103
  meta: {},
96
104
  headings: [],
97
105
  });
106
+ if (pendingScrollRestore.current) {
107
+ pendingScrollRestore.current = false;
108
+ setScrollRestoreNonce((nonce) => nonce + 1);
109
+ } else if (pendingScrollTop.current) {
110
+ pendingScrollTop.current = false;
111
+ setScrollTopNonce((nonce) => nonce + 1);
112
+ }
98
113
  }
99
114
  });
100
115
  return () => {
@@ -102,6 +117,16 @@ export function App({ routePath, initialPage }: { routePath: string; initialPage
102
117
  };
103
118
  }, [activeSite.description, currentRoute]);
104
119
 
120
+ useEffect(() => {
121
+ if (scrollRestoreNonce === 0) return;
122
+ void restoreScrollPositionAfterLayout();
123
+ }, [scrollRestoreNonce]);
124
+
125
+ useEffect(() => {
126
+ if (scrollTopNonce === 0) return;
127
+ void scrollToTopAfterLayout();
128
+ }, [scrollTopNonce]);
129
+
105
130
  useEffect(() => {
106
131
  const prefetch = (route: string) => prefetchPage(route, site.base);
107
132
  const stopViewportPrefetch = setupViewportPrefetch(routeFromHref, prefetch);
@@ -138,9 +163,10 @@ export function App({ routePath, initialPage }: { routePath: string; initialPage
138
163
  }
139
164
  event.preventDefault();
140
165
  saveScrollPositionBeforeNavigation();
166
+ pendingScrollTop.current = true;
141
167
  window.history.pushState({}, "", url);
142
168
  setCurrentRoute(route);
143
- window.scrollTo({ top: 0, behavior: "auto" });
169
+ window.scrollTo({ top: 0, left: 0, behavior: "auto" });
144
170
  };
145
171
  const onMouseEnter = (event: MouseEvent) => {
146
172
  const link = anchorFromEvent(event);
@@ -160,9 +186,13 @@ export function App({ routePath, initialPage }: { routePath: string; initialPage
160
186
  };
161
187
  }, [currentRoute, mpa]);
162
188
 
189
+ useEffect(() => {
190
+ void renderMermaidDiagrams();
191
+ }, [currentRoute, page]);
192
+
163
193
  useEffect(() => {
164
194
  if (currentRoute !== normalizeRoute(routePath)) {
165
- document.getElementById("content")?.focus();
195
+ document.getElementById("content")?.focus({ preventScroll: true });
166
196
  }
167
197
  }, [currentRoute, routePath]);
168
198
 
@@ -0,0 +1,74 @@
1
+ let renderRun = 0;
2
+
3
+ function isDarkTheme(): boolean {
4
+ if (typeof document === "undefined" || typeof window === "undefined") return false;
5
+ const explicit = document.documentElement.dataset.theme;
6
+ if (explicit === "dark") return true;
7
+ if (explicit === "light") return false;
8
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
9
+ }
10
+
11
+ function mermaidSourceFromElement(element: HTMLElement): string {
12
+ if (element.dataset.mermaidSource) return element.dataset.mermaidSource;
13
+ return element.textContent ?? "";
14
+ }
15
+
16
+ function normalizeMermaidBlocks(): HTMLElement[] {
17
+ const blocks = Array.from(document.querySelectorAll<HTMLElement>(".pp-mermaid"));
18
+
19
+ for (const code of document.querySelectorAll<HTMLElement>(
20
+ 'pre > code.language-mermaid, pre > code[class*="language-mermaid"]',
21
+ )) {
22
+ const pre = code.parentElement;
23
+ if (!pre || pre.dataset.mermaidNormalized === "true") continue;
24
+
25
+ const block = document.createElement("div");
26
+ block.className = "pp-mermaid";
27
+ block.dataset.mermaidSource = code.textContent ?? "";
28
+ block.textContent = code.textContent ?? "";
29
+ pre.replaceWith(block);
30
+ pre.dataset.mermaidNormalized = "true";
31
+ blocks.push(block);
32
+ }
33
+
34
+ return blocks;
35
+ }
36
+
37
+ export async function renderMermaidDiagrams(): Promise<void> {
38
+ if (typeof document === "undefined") return;
39
+
40
+ const blocks = normalizeMermaidBlocks().filter(
41
+ (block) => block.dataset.mermaidRendered !== "true",
42
+ );
43
+ if (blocks.length === 0) return;
44
+
45
+ const run = ++renderRun;
46
+ const { default: mermaid } = await import("mermaid");
47
+ if (run !== renderRun) return;
48
+
49
+ mermaid.initialize({
50
+ startOnLoad: false,
51
+ securityLevel: "strict",
52
+ theme: isDarkTheme() ? "dark" : "default",
53
+ });
54
+
55
+ await Promise.all(
56
+ blocks.map(async (block, index) => {
57
+ const source = mermaidSourceFromElement(block).trim();
58
+ if (!source) return;
59
+
60
+ try {
61
+ const id = `pp-mermaid-${run}-${index}`;
62
+ const { svg } = await mermaid.render(id, source);
63
+ if (run !== renderRun) return;
64
+ block.innerHTML = svg;
65
+ block.dataset.mermaidRendered = "true";
66
+ block.dataset.mermaidSource = source;
67
+ } catch (error) {
68
+ block.dataset.mermaidError = "true";
69
+ block.textContent = source;
70
+ console.warn("Failed to render Mermaid diagram", error);
71
+ }
72
+ }),
73
+ );
74
+ }
@@ -150,6 +150,49 @@ const Layout: FunctionalComponent<LayoutProps> = ({
150
150
  setMobileMenuOpen(false);
151
151
  }, [routePath]);
152
152
 
153
+ useEffect(() => {
154
+ const copyText = async (text: string) => {
155
+ if (navigator.clipboard?.writeText) {
156
+ await navigator.clipboard.writeText(text);
157
+ return;
158
+ }
159
+ const textarea = document.createElement("textarea");
160
+ textarea.value = text;
161
+ textarea.setAttribute("readonly", "");
162
+ textarea.style.position = "fixed";
163
+ textarea.style.top = "-9999px";
164
+ document.body.append(textarea);
165
+ textarea.select();
166
+ document.execCommand("copy");
167
+ textarea.remove();
168
+ };
169
+
170
+ const onClick = (event: MouseEvent) => {
171
+ const target = event.target;
172
+ const candidate = target instanceof Element ? target.closest(".pp-code-copy") : null;
173
+ if (!(candidate instanceof HTMLButtonElement)) return;
174
+ const button = candidate;
175
+ const code = button.closest(".pp-code-block")?.querySelector("code")?.textContent ?? "";
176
+ if (!code) return;
177
+
178
+ event.preventDefault();
179
+ void copyText(code).then(() => {
180
+ const icon = button.querySelector(".pp-code-copy-icon");
181
+ if (icon && button.dataset.checkIcon) icon.innerHTML = button.dataset.checkIcon;
182
+ button.classList.add("copied");
183
+ button.setAttribute("aria-label", button.dataset.copiedLabel ?? "Copied");
184
+ window.setTimeout(() => {
185
+ if (icon && button.dataset.copyIcon) icon.innerHTML = button.dataset.copyIcon;
186
+ button.classList.remove("copied");
187
+ button.setAttribute("aria-label", "Copy code");
188
+ }, 2000);
189
+ });
190
+ };
191
+
192
+ document.addEventListener("click", onClick);
193
+ return () => document.removeEventListener("click", onClick);
194
+ }, []);
195
+
153
196
  useEffect(() => {
154
197
  if (!mobileMenuOpen) return;
155
198
 
@@ -759,14 +759,188 @@ body::selection {
759
759
  margin: 16px 0;
760
760
  }
761
761
 
762
+ .pp-doc-content .pp-mermaid {
763
+ margin: 1.25rem 0;
764
+ padding: 1.25rem;
765
+ overflow: auto;
766
+ border: 1px solid color-mix(in srgb, var(--pp-border) 82%, transparent);
767
+ border-radius: 14px;
768
+ background: var(--pp-code-bg);
769
+ color: var(--pp-code-text);
770
+ text-align: center;
771
+ }
772
+
773
+ .pp-doc-content .pp-mermaid[data-mermaid-error="true"] {
774
+ text-align: left;
775
+ white-space: pre;
776
+ }
777
+
778
+ .pp-doc-content .pp-mermaid svg {
779
+ max-width: 100%;
780
+ height: auto;
781
+ }
782
+
783
+ .pp-doc-content .pp-code-block {
784
+ position: relative;
785
+ margin: 1.25rem 0;
786
+ border: 1px solid color-mix(in srgb, var(--pp-border) 82%, transparent);
787
+ border-radius: 14px;
788
+ background: var(--pp-code-bg);
789
+ box-shadow:
790
+ 0 14px 34px color-mix(in srgb, #000 8%, transparent),
791
+ inset 0 1px 0 color-mix(in srgb, #fff 45%, transparent);
792
+ overflow: hidden;
793
+ }
794
+
762
795
  .pp-doc-content pre {
763
- padding: 16px;
764
- border-radius: 8px;
796
+ padding: 16px 18px;
797
+ border-radius: 0;
765
798
  overflow: auto;
766
- border: 1px solid var(--pp-border);
799
+ border: 0;
767
800
  background: var(--pp-code-bg);
768
801
  color: var(--pp-code-text);
769
- line-height: 1.55;
802
+ line-height: 1.38;
803
+ }
804
+
805
+ .pp-doc-content .pp-code-block pre {
806
+ margin: 0;
807
+ max-height: min(68vh, 720px);
808
+ padding-top: 2.7rem;
809
+ padding-right: 3.75rem;
810
+ line-height: 0;
811
+ scrollbar-width: thin;
812
+ scrollbar-color: color-mix(in srgb, var(--pp-muted) 40%, transparent) transparent;
813
+ }
814
+
815
+ .pp-doc-content .pp-code-block .shiki.shiki-themes.vitesse-light.vitesse-dark {
816
+ padding-bottom: 1rem;
817
+ }
818
+
819
+ .pp-doc-content .pp-code-block pre code {
820
+ display: block;
821
+ min-width: max-content;
822
+ font-size: 0.9rem;
823
+ line-height: 1.32;
824
+ tab-size: 2;
825
+ }
826
+
827
+ .pp-doc-content .pp-code-block .line {
828
+ display: block;
829
+ line-height: 0;
830
+ counter-increment: pp-code-line;
831
+ }
832
+
833
+ .pp-doc-content .pp-code-block code {
834
+ counter-reset: pp-code-line;
835
+ }
836
+
837
+ .pp-doc-content .pp-code-block .line::before {
838
+ content: counter(pp-code-line);
839
+ display: inline-block;
840
+ width: 2rem;
841
+ margin-right: 1rem;
842
+ color: color-mix(in srgb, var(--pp-muted) 42%, transparent);
843
+ font-variant-numeric: tabular-nums;
844
+ text-align: right;
845
+ user-select: none;
846
+ }
847
+
848
+ .pp-doc-content .pp-code-block[data-language="plaintext"] pre {
849
+ padding-top: 18px;
850
+ }
851
+
852
+ .pp-doc-content .pp-code-lang {
853
+ position: absolute;
854
+ top: 12px;
855
+ left: 14px;
856
+ z-index: 2;
857
+ max-width: calc(100% - 4.75rem);
858
+ overflow: hidden;
859
+ border: 1px solid color-mix(in srgb, var(--pp-border) 72%, transparent);
860
+ border-radius: 999px;
861
+ padding: 0.18rem 0.55rem;
862
+ background: color-mix(in srgb, var(--pp-bg) 72%, transparent);
863
+ color: var(--pp-muted);
864
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
865
+ font-size: 0.72rem;
866
+ line-height: 1.35;
867
+ text-overflow: ellipsis;
868
+ text-transform: uppercase;
869
+ letter-spacing: 0.04em;
870
+ backdrop-filter: blur(12px);
871
+ }
872
+
873
+ .pp-doc-content .shiki,
874
+ .pp-doc-content .shiki span {
875
+ color: var(--shiki-light, var(--pp-code-text)) !important;
876
+ background-color: var(--shiki-light-bg, var(--pp-code-bg)) !important;
877
+ }
878
+
879
+ @media (prefers-color-scheme: dark) {
880
+ :root:not([data-theme]) .pp-doc-content .shiki,
881
+ :root:not([data-theme]) .pp-doc-content .shiki span {
882
+ color: var(--shiki-dark, var(--pp-code-text)) !important;
883
+ background-color: var(--shiki-dark-bg, var(--pp-code-bg)) !important;
884
+ }
885
+ }
886
+
887
+ :root[data-theme="dark"] .pp-doc-content .shiki,
888
+ :root[data-theme="dark"] .pp-doc-content .shiki span {
889
+ color: var(--shiki-dark, var(--pp-code-text)) !important;
890
+ background-color: var(--shiki-dark-bg, var(--pp-code-bg)) !important;
891
+ }
892
+
893
+ .pp-doc-content .pp-code-copy {
894
+ position: absolute;
895
+ top: 10px;
896
+ right: 10px;
897
+ z-index: 2;
898
+ display: grid;
899
+ place-items: center;
900
+ width: 34px;
901
+ height: 34px;
902
+ padding: 0;
903
+ border: 1px solid color-mix(in srgb, var(--pp-border) 72%, transparent);
904
+ border-radius: 10px;
905
+ background: color-mix(in srgb, var(--pp-bg) 72%, transparent);
906
+ color: var(--pp-muted);
907
+ opacity: 0;
908
+ transform: translateY(-3px) scale(0.96);
909
+ cursor: pointer;
910
+ backdrop-filter: blur(12px);
911
+ transition:
912
+ opacity 0.18s ease,
913
+ transform 0.18s ease,
914
+ color 0.18s ease,
915
+ border-color 0.18s ease,
916
+ background 0.18s ease;
917
+ }
918
+
919
+ .pp-doc-content .pp-code-block:hover .pp-code-copy,
920
+ .pp-doc-content .pp-code-copy:focus-visible,
921
+ .pp-doc-content .pp-code-copy.copied {
922
+ opacity: 1;
923
+ transform: translateY(0) scale(1);
924
+ }
925
+
926
+ .pp-doc-content .pp-code-copy:hover,
927
+ .pp-doc-content .pp-code-copy.copied {
928
+ color: var(--pp-accent);
929
+ border-color: color-mix(in srgb, var(--pp-accent) 44%, var(--pp-border));
930
+ background: color-mix(in srgb, var(--pp-bg) 88%, var(--pp-accent) 12%);
931
+ }
932
+
933
+ .pp-doc-content .pp-code-copy svg {
934
+ width: 17px;
935
+ height: 17px;
936
+ fill: currentColor;
937
+ }
938
+
939
+ .pp-doc-content.pp-code-wrap .pp-code-block pre code,
940
+ .pp-doc-content .pp-code-block.pp-code-wrap pre code {
941
+ min-width: 0;
942
+ white-space: pre-wrap;
943
+ overflow-wrap: anywhere;
770
944
  }
771
945
 
772
946
  .pp-doc-content :not(pre) > code {
@@ -970,9 +1144,11 @@ body::selection {
970
1144
  display: block;
971
1145
  }
972
1146
 
973
- .pp-doc-content .pp-code-group-block .shiki {
1147
+ .pp-doc-content .pp-code-group-block .pp-code-block {
974
1148
  margin: 0;
1149
+ border: 0;
975
1150
  border-radius: 0;
1151
+ box-shadow: none;
976
1152
  }
977
1153
 
978
1154
  .pp-doc-content table {
@@ -9,15 +9,14 @@ export function setupScrollRestoration(): () => void {
9
9
  window.history.scrollRestoration = "manual";
10
10
  }
11
11
 
12
- let timeout: ReturnType<typeof setTimeout> | undefined;
13
12
  const onScroll = () => {
14
- clearTimeout(timeout);
15
- timeout = setTimeout(persistScrollPosition, 150);
13
+ persistScrollPosition();
16
14
  };
17
15
  window.addEventListener("scroll", onScroll, { passive: true });
16
+ window.addEventListener("pagehide", persistScrollPosition);
18
17
  return () => {
19
18
  window.removeEventListener("scroll", onScroll);
20
- clearTimeout(timeout);
19
+ window.removeEventListener("pagehide", persistScrollPosition);
21
20
  };
22
21
  }
23
22
 
@@ -44,17 +43,49 @@ export function restoreScrollPosition(): void {
44
43
  window.scrollTo({ top: readScrollPositionFromHistory(), left: 0, behavior: "auto" });
45
44
  }
46
45
 
47
- export function restoreScrollPositionAfterLayout(): void {
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> {
48
55
  if (typeof window === "undefined") return;
49
- requestAnimationFrame(() => {
50
- requestAnimationFrame(() => {
51
- const html = document.documentElement;
52
- const previous = html.style.scrollBehavior;
53
- html.style.scrollBehavior = "auto";
54
- restoreScrollPosition();
55
- html.style.scrollBehavior = previous;
56
- });
57
- });
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
+ ]);
58
89
  }
59
90
 
60
91
  export function skipNextScrollRestore(): void {
@@ -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`.