Skip to content

Commit f2242f7

Browse files
committed
fix: support static export on Cloudflare Pages
1 parent a931856 commit f2242f7

3 files changed

Lines changed: 166 additions & 38 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"build": "echo 'Starting TinaCMS build...' && tinacms build && echo 'TinaCMS build completed. Starting Next.js build...' && next build",
99
"postbuild": "npx pagefind --site .next --output-path .next/static/pagefind && next-sitemap",
1010
"build-local-pagefind": "tinacms build --local --skip-cloud-checks --skip-search-index -c \"NODE_ENV=production next build && npx pagefind --site .next --output-subdir ../public/pagefind\"",
11-
"export": "tinacms build --local --skip-cloud-checks --skip-search-index -c \"NODE_ENV=production EXPORT_MODE=static UNOPTIMIZED_IMAGES=true next build\"",
12-
"build:cloudflare": "tinacms build --local --skip-cloud-checks --skip-search-index -c \"NODE_ENV=production EXPORT_MODE=static UNOPTIMIZED_IMAGES=true NEXT_PUBLIC_PAGEFIND_PATH=/pagefind next build && npx pagefind --site out --output-subdir pagefind\"",
11+
"export": "node scripts/run-static-export.js export",
12+
"build:cloudflare": "node scripts/run-static-export.js cloudflare",
1313
"start": "tinacms build && next start",
1414
"lint": "biome check src/ tina/",
1515
"lint:fix": "biome check src/ tina/ --fix",

scripts/run-static-export.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require("node:fs/promises");
4+
const path = require("node:path");
5+
const { spawn } = require("node:child_process");
6+
7+
const mode = process.argv[2];
8+
9+
const commands = {
10+
export:
11+
'tinacms build --local --skip-cloud-checks --skip-search-index -c "next build"',
12+
cloudflare:
13+
'tinacms build --local --skip-cloud-checks --skip-search-index -c "next build && npx pagefind --site out --output-subdir pagefind"',
14+
};
15+
16+
const command = commands[mode];
17+
18+
if (!command) {
19+
console.error(
20+
`Unknown static export mode "${mode}". Use one of: ${Object.keys(commands).join(", ")}.`
21+
);
22+
process.exit(1);
23+
}
24+
25+
const projectRoot = process.cwd();
26+
const appApiDir = path.join(projectRoot, "src", "app", "api");
27+
const backupRoot = path.join(projectRoot, ".static-export-route-backup");
28+
29+
async function pathExists(target) {
30+
try {
31+
await fs.access(target);
32+
return true;
33+
} catch {
34+
return false;
35+
}
36+
}
37+
38+
async function collectRouteFiles(dir) {
39+
const files = [];
40+
const entries = await fs.readdir(dir, { withFileTypes: true });
41+
42+
for (const entry of entries) {
43+
const fullPath = path.join(dir, entry.name);
44+
if (entry.isDirectory()) {
45+
files.push(...(await collectRouteFiles(fullPath)));
46+
continue;
47+
}
48+
49+
if (entry.isFile() && entry.name === "route.ts") {
50+
files.push(fullPath);
51+
}
52+
}
53+
54+
return files;
55+
}
56+
57+
async function moveRouteFilesOut() {
58+
if (!(await pathExists(appApiDir))) return [];
59+
60+
const routeFiles = await collectRouteFiles(appApiDir);
61+
62+
await fs.rm(backupRoot, { recursive: true, force: true });
63+
64+
for (const routeFile of routeFiles) {
65+
const relativePath = path.relative(projectRoot, routeFile);
66+
const backupPath = path.join(backupRoot, relativePath);
67+
await fs.mkdir(path.dirname(backupPath), { recursive: true });
68+
await fs.rename(routeFile, backupPath);
69+
}
70+
71+
return routeFiles.map((routeFile) => path.relative(projectRoot, routeFile));
72+
}
73+
74+
async function restoreRouteFiles(relativePaths) {
75+
for (const relativePath of relativePaths) {
76+
const backupPath = path.join(backupRoot, relativePath);
77+
const targetPath = path.join(projectRoot, relativePath);
78+
79+
if (!(await pathExists(backupPath))) continue;
80+
81+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
82+
await fs.rename(backupPath, targetPath);
83+
}
84+
85+
await fs.rm(backupRoot, { recursive: true, force: true });
86+
}
87+
88+
async function run() {
89+
const movedRouteFiles = await moveRouteFilesOut();
90+
let exitCode = 1;
91+
92+
try {
93+
const child = spawn(command, {
94+
cwd: projectRoot,
95+
env: {
96+
...process.env,
97+
NODE_ENV: "production",
98+
EXPORT_MODE: "static",
99+
UNOPTIMIZED_IMAGES: "true",
100+
NEXT_PUBLIC_STATIC_EXPORT: "true",
101+
...(mode === "cloudflare"
102+
? { NEXT_PUBLIC_PAGEFIND_PATH: "/pagefind" }
103+
: {}),
104+
},
105+
shell: true,
106+
stdio: "inherit",
107+
});
108+
109+
exitCode = await new Promise((resolve, reject) => {
110+
child.on("error", reject);
111+
child.on("exit", resolve);
112+
});
113+
} finally {
114+
await restoreRouteFiles(movedRouteFiles);
115+
}
116+
117+
process.exit(exitCode ?? 1);
118+
}
119+
120+
run().catch((error) => {
121+
console.error(error);
122+
process.exit(1);
123+
});

src/components/copy-page-dropdown.tsx

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface CopyPageDropdownProps {
2727
export const CopyPageDropdown: React.FC<CopyPageDropdownProps> = ({
2828
title = "Documentation Page",
2929
}) => {
30+
const isStaticExport = process.env.NEXT_PUBLIC_STATIC_EXPORT === "true";
3031
const [copied, setCopied] = useState(false);
3132
const [isOpen, setIsOpen] = useState(false);
3233
const [markdownUrl, setMarkdownUrl] = useState<string | null>(null);
@@ -177,42 +178,46 @@ export const CopyPageDropdown: React.FC<CopyPageDropdownProps> = ({
177178
description: "Copy page as Markdown for LLMs",
178179
onClick: handleCopyPage,
179180
},
180-
{
181-
icon: (
182-
<MdFilePresent className="w-4 h-4 text-neutral-text-secondary" />
183-
),
184-
label: "View as Markdown",
185-
description: "View this page as plain text",
186-
onClick: handleViewMarkdown,
187-
},
188-
{
189-
icon: (
190-
<SiOpenai className="w-4 h-4 text-neutral-text-secondary" />
191-
),
192-
label: "Open in ChatGPT",
193-
description: "Ask questions about this page",
194-
onClick: () =>
195-
openInLLM(
196-
(url) =>
197-
`https://chat.openai.com/?hints=search&q=Read%20from%20${encodeURIComponent(
198-
url
199-
)}%20so%20I%20can%20ask%20questions%20about%20it.`
200-
),
201-
},
202-
{
203-
icon: (
204-
<FaCommentDots className="w-4 h-4 text-neutral-text-secondary" />
205-
),
206-
label: "Open in Claude",
207-
description: "Ask questions about this page",
208-
onClick: () =>
209-
openInLLM(
210-
(url) =>
211-
`https://claude.ai/?q=Read%20from%20${encodeURIComponent(
212-
url
213-
)}%20so%20I%20can%20ask%20questions%20about%20it.`
214-
),
215-
},
181+
...(!isStaticExport
182+
? [
183+
{
184+
icon: (
185+
<MdFilePresent className="w-4 h-4 text-neutral-text-secondary" />
186+
),
187+
label: "View as Markdown",
188+
description: "View this page as plain text",
189+
onClick: handleViewMarkdown,
190+
},
191+
{
192+
icon: (
193+
<SiOpenai className="w-4 h-4 text-neutral-text-secondary" />
194+
),
195+
label: "Open in ChatGPT",
196+
description: "Ask questions about this page",
197+
onClick: () =>
198+
openInLLM(
199+
(url) =>
200+
`https://chat.openai.com/?hints=search&q=Read%20from%20${encodeURIComponent(
201+
url
202+
)}%20so%20I%20can%20ask%20questions%20about%20it.`
203+
),
204+
},
205+
{
206+
icon: (
207+
<FaCommentDots className="w-4 h-4 text-neutral-text-secondary" />
208+
),
209+
label: "Open in Claude",
210+
description: "Ask questions about this page",
211+
onClick: () =>
212+
openInLLM(
213+
(url) =>
214+
`https://claude.ai/?q=Read%20from%20${encodeURIComponent(
215+
url
216+
)}%20so%20I%20can%20ask%20questions%20about%20it.`
217+
),
218+
},
219+
]
220+
: []),
216221
].map(({ icon, label, description, onClick }) => (
217222
<DropdownMenuItem
218223
key={label}

0 commit comments

Comments
 (0)