@writedocs/generator 0.1.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.
- package/LICENSE +15 -0
- package/README.md +17 -0
- package/astro.config.mjs +419 -0
- package/bin/writedocs.js +73 -0
- package/package.json +79 -0
- package/src/assets/wd_watermark.png +0 -0
- package/src/assets/wd_watermark_dark.png +0 -0
- package/src/cli/build-auth.js +53 -0
- package/src/cli/build.js +40 -0
- package/src/cli/dev.js +12 -0
- package/src/cli/generate-api-pages.js +359 -0
- package/src/cli/init.js +81 -0
- package/src/cli/preflight.js +40 -0
- package/src/cli/run-astro.js +57 -0
- package/src/cli/run-pagefind.js +66 -0
- package/src/cli/write-redirects-file.js +80 -0
- package/src/components/Accordion.astro +164 -0
- package/src/components/AccordionGroup.astro +40 -0
- package/src/components/ApiLangSelect.astro +168 -0
- package/src/components/ApiPlayground.astro +281 -0
- package/src/components/ApiReferencePanel.astro +1754 -0
- package/src/components/ApiSchemaField.astro +54 -0
- package/src/components/AppIcon.astro +32 -0
- package/src/components/Badge.astro +128 -0
- package/src/components/Callout.astro +168 -0
- package/src/components/Card.astro +136 -0
- package/src/components/CardGroup.astro +20 -0
- package/src/components/CodeGroup.astro +184 -0
- package/src/components/CopyPageMenu.astro +246 -0
- package/src/components/Danger.astro +12 -0
- package/src/components/Expandable.astro +126 -0
- package/src/components/Frame.astro +102 -0
- package/src/components/Hint.astro +99 -0
- package/src/components/Icon.astro +70 -0
- package/src/components/Image.astro +147 -0
- package/src/components/Info.astro +12 -0
- package/src/components/Note.astro +12 -0
- package/src/components/Parameter.astro +119 -0
- package/src/components/RequestExample.astro +33 -0
- package/src/components/ResponseExample.astro +19 -0
- package/src/components/Searchbar.astro +117 -0
- package/src/components/Step.astro +10 -0
- package/src/components/Steps.astro +32 -0
- package/src/components/Tab.astro +9 -0
- package/src/components/Tabs.astro +52 -0
- package/src/components/Tip.astro +12 -0
- package/src/components/Video.astro +135 -0
- package/src/components/Warning.astro +12 -0
- package/src/components/index.ts +48 -0
- package/src/content.config.ts +223 -0
- package/src/layout/BaseLayout.astro +750 -0
- package/src/layout/components/AnalyticsScripts.astro +77 -0
- package/src/layout/components/AskAiWidget.astro +37 -0
- package/src/layout/components/Breadcrumbs.astro +97 -0
- package/src/layout/components/ImageZoom.astro +19 -0
- package/src/layout/components/MobileMenu.astro +200 -0
- package/src/layout/components/NavTree.astro +351 -0
- package/src/layout/components/SearchModal.astro +42 -0
- package/src/layout/components/Sidebar.astro +122 -0
- package/src/layout/components/SiteFooter.astro +85 -0
- package/src/layout/components/TableOfContents.astro +117 -0
- package/src/layout/components/TopBar.astro +311 -0
- package/src/layout/styles/banner.css +44 -0
- package/src/layout/styles/base.css +234 -0
- package/src/layout/styles/dropdown.css +133 -0
- package/src/layout/styles/footer.css +108 -0
- package/src/layout/styles/image-zoom.css +50 -0
- package/src/layout/styles/mobile-menu.css +258 -0
- package/src/layout/styles/search-modal.css +122 -0
- package/src/layout/styles/topbar.css +437 -0
- package/src/lib/config.ts +2131 -0
- package/src/lib/mdx-auto-hydrate.js +70 -0
- package/src/lib/mdx-inject-builtins.js +87 -0
- package/src/lib/mdx-substitute-variables.js +66 -0
- package/src/lib/mdx-title-anchor-ids.js +84 -0
- package/src/lib/mermaid-rehype.js +72 -0
- package/src/lib/openapi-render.ts +479 -0
- package/src/lib/shiki-code-block.js +102 -0
- package/src/lib/shiki-copy-button.js +45 -0
- package/src/lib/styles-asset-integration.js +210 -0
- package/src/lib/writedocs-temp-dir.js +93 -0
- package/src/pages/404.astro +62 -0
- package/src/pages/[...slug].astro +1270 -0
- package/src/pages/[...slug].md.ts +78 -0
- package/src/pages/llms-full.txt.ts +71 -0
- package/src/pages/llms.txt.ts +141 -0
- package/src/scripts/banner.ts +20 -0
- package/src/scripts/dropdowns.ts +61 -0
- package/src/scripts/image-zoom.ts +66 -0
- package/src/scripts/mobile-menu.ts +55 -0
- package/src/scripts/search.ts +155 -0
- package/src/scripts/sidebar-scroll.ts +65 -0
- package/src/scripts/theme-toggle.ts +35 -0
- package/src/scripts/topbar-offset.ts +141 -0
- package/src/styles/global.css +18 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Lets a static asset referenced by a root-relative URL - a writedocs.json
|
|
2
|
+
// field (styles.favicon, styles.logo, footer.logo, styles.background.
|
|
3
|
+
// images.{light,dark}, seo.ogImage, styles.fonts.*.source - the exact set
|
|
4
|
+
// collectConfiguredAssetPaths() in lib/config.ts resolves) *or* a plain
|
|
5
|
+
// image written directly into a page's own MDX/Markdown body
|
|
6
|
+
// (``, a raw `<img src="/images/foo.png">`, the
|
|
7
|
+
// `<Image src="...">` component, `<Card img="...">`, ...) - resolve from
|
|
8
|
+
// *anywhere* in the project, not just public/. "Any place to store
|
|
9
|
+
// assets" is the whole point: a site author shouldn't have to know Astro
|
|
10
|
+
// has a public/ convention at all, let alone reorganize their project
|
|
11
|
+
// around it, just to reference an image from a doc page.
|
|
12
|
+
//
|
|
13
|
+
// Deliberately NOT implemented as "copy everything into public/ (or a
|
|
14
|
+
// merged scratch dir) before Astro starts, then leave Astro's own
|
|
15
|
+
// publicDir alone": that would either (a) write into the user's own
|
|
16
|
+
// public/ folder as a side effect - a real file appearing in their
|
|
17
|
+
// project tree that they didn't create, which writedocsTempDir()'s own
|
|
18
|
+
// doc comment already argues against doing anywhere in this codebase, or
|
|
19
|
+
// (b) point publicDir at a one-shot copied scratch dir instead of the
|
|
20
|
+
// real public/, which would regress `writedocs dev`'s live file-reload
|
|
21
|
+
// for the *existing*, common case of a file genuinely living under
|
|
22
|
+
// public/ - Vite's own dev server watches and serves publicDir straight
|
|
23
|
+
// off disk per-request with no copy step involved at all today; a byte
|
|
24
|
+
// copy taken once at startup wouldn't pick up a live edit to that file
|
|
25
|
+
// without a full dev-server restart.
|
|
26
|
+
//
|
|
27
|
+
// Instead, this is a small Astro integration with two hooks, each
|
|
28
|
+
// resolving fresh rather than relying on any one-shot copy:
|
|
29
|
+
// - astro:server:setup (dev): adds Vite dev-server middleware that
|
|
30
|
+
// intercepts any request whose extension looks like a static asset
|
|
31
|
+
// (looksLikeAssetPath() below - images/fonts, the same set
|
|
32
|
+
// MIME_BY_EXTENSION already knows how to serve) and - only if it
|
|
33
|
+
// doesn't already exist under public/ (that stays Vite's own job,
|
|
34
|
+
// unshadowed) - streams it straight from wherever it lives in the
|
|
35
|
+
// project. Not scoped to writedocs.json's own configured fields specifically
|
|
36
|
+
// (an earlier version of this file was) - a page's own MDX can
|
|
37
|
+
// reference an image path this integration has no way to know about
|
|
38
|
+
// ahead of time short of parsing every page's content on every
|
|
39
|
+
// request, so instead of trying to enumerate what *should* be
|
|
40
|
+
// reachable, it just resolves whatever *is* actually requested,
|
|
41
|
+
// directly, the same way Vite's own publicDir serving already does.
|
|
42
|
+
// - astro:build:done: a one-time pass after Astro's own build finishes.
|
|
43
|
+
// First, the same writedocs.json-field-driven copy as before (still needed
|
|
44
|
+
// as its own pass - background images/font sources render into CSS
|
|
45
|
+
// `url(...)` inside a `<style>` block, and seo.ogImage into a `<meta
|
|
46
|
+
// content="...">` tag, neither of which the second pass below would
|
|
47
|
+
// ever see). Second, copyReferencedContentAssets() below scans every
|
|
48
|
+
// built .html page for `src="/..."` references (an `<img>`, mainly -
|
|
49
|
+
// see its own comment) not already present in the output, and resolves
|
|
50
|
+
// + copies each the same way - this is what actually covers a plain
|
|
51
|
+
// content image, since there's no fixed field name to read it from up
|
|
52
|
+
// front the way there is for writedocs.json's own fields. A real static
|
|
53
|
+
// build has no "later" for a live-reload concern to apply to, so a
|
|
54
|
+
// single pass of each here is sufficient.
|
|
55
|
+
import fs from 'node:fs';
|
|
56
|
+
import path from 'node:path';
|
|
57
|
+
import { fileURLToPath } from 'node:url';
|
|
58
|
+
import { loadDocsConfig, collectConfiguredAssetPaths } from './config.ts';
|
|
59
|
+
|
|
60
|
+
const MIME_BY_EXTENSION = {
|
|
61
|
+
'.svg': 'image/svg+xml',
|
|
62
|
+
'.png': 'image/png',
|
|
63
|
+
'.jpg': 'image/jpeg',
|
|
64
|
+
'.jpeg': 'image/jpeg',
|
|
65
|
+
'.gif': 'image/gif',
|
|
66
|
+
'.webp': 'image/webp',
|
|
67
|
+
'.avif': 'image/avif',
|
|
68
|
+
'.ico': 'image/x-icon',
|
|
69
|
+
'.woff': 'font/woff',
|
|
70
|
+
'.woff2': 'font/woff2',
|
|
71
|
+
'.ttf': 'font/ttf',
|
|
72
|
+
'.otf': 'font/otf',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function mimeFor(filePath) {
|
|
76
|
+
return MIME_BY_EXTENSION[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Resolves a root-relative `urlPath` (e.g. "/images/hero.svg") against
|
|
80
|
+
* `contentDir` directly - not `<contentDir>/public` - the fallback
|
|
81
|
+
* location for one of the styles/footer/seo asset fields
|
|
82
|
+
* (collectConfiguredAssetPaths()) when it isn't sitting under public/.
|
|
83
|
+
* Segment-by-segment path-traversal guard (no
|
|
84
|
+
* `..`/`.` segments) since `urlPath` ultimately comes from an incoming
|
|
85
|
+
* request URL in the dev-server case, not just trusted writedocs.json
|
|
86
|
+
* content. Returns an absolute path, or null if nothing real is there. */
|
|
87
|
+
function resolveOutsidePublic(urlPath, contentDir) {
|
|
88
|
+
const segments = urlPath.replace(/^\/+/, '').split('/');
|
|
89
|
+
if (segments.some((segment) => segment === '..' || segment === '.' || segment === '')) return null;
|
|
90
|
+
const absolute = path.join(contentDir, ...segments);
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(absolute).isFile() ? absolute : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function publicPathFor(urlPath, contentDir) {
|
|
99
|
+
const segments = urlPath.replace(/^\/+/, '').split('/');
|
|
100
|
+
return path.join(contentDir, 'public', ...segments);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Root-relative, recognized-extension paths only - the same "does this
|
|
104
|
+
* look like a static asset at all" question mimeFor()'s own map already
|
|
105
|
+
* answers, reused here as the dev-middleware/build-scan gate instead of
|
|
106
|
+
* a fixed list of writedocs.json field names. Deliberately permissive: a page
|
|
107
|
+
* or config field asking for a path this returns true for gets a real
|
|
108
|
+
* attempt to resolve it from anywhere in the project; anything else
|
|
109
|
+
* (an actual API route, a `.html` page request, ...) is left alone,
|
|
110
|
+
* same as before this existed. */
|
|
111
|
+
function looksLikeAssetPath(urlPath) {
|
|
112
|
+
return Object.prototype.hasOwnProperty.call(MIME_BY_EXTENSION, path.extname(urlPath).toLowerCase());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Recursively lists every `.html` file under `dir`. Astro's own build
|
|
116
|
+
* output is already a flat-ish tree of one .html per route, so this is
|
|
117
|
+
* just a plain walk - no globbing dependency needed for something this
|
|
118
|
+
* small. */
|
|
119
|
+
function listHtmlFiles(dir) {
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
122
|
+
const full = path.join(dir, entry.name);
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
results.push(...listHtmlFiles(full));
|
|
125
|
+
} else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
126
|
+
results.push(full);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Scans every built page for root-relative, recognized-extension `src="/..."`
|
|
133
|
+
* references (`<img src="...">`, mainly - what `Image.astro`, `Frame.astro`,
|
|
134
|
+
* and a raw `![]()`/`<img>` in MDX all ultimately render down to; a
|
|
135
|
+
* `srcset="..."` attribute is checked too, since it's the same kind of
|
|
136
|
+
* reference and costs nothing extra to also cover) that aren't already
|
|
137
|
+
* present in the build output, and copies each one in from wherever it
|
|
138
|
+
* resolves in the project via resolveOutsidePublic() - the same
|
|
139
|
+
* fallback the dev middleware below already uses, just applied once,
|
|
140
|
+
* after the fact, to the rendered HTML instead of to live requests.
|
|
141
|
+
* This is what actually covers a plain content-body image: unlike a
|
|
142
|
+
* writedocs.json field, there's no fixed set of field names to read a
|
|
143
|
+
* content image's path from ahead of time, so instead this reads
|
|
144
|
+
* whatever `src` a page's own rendered markup ends up containing. */
|
|
145
|
+
function copyReferencedContentAssets(outDir, contentDir) {
|
|
146
|
+
const attrPattern = /\b(?:src|srcset)="([^"]+)"/g;
|
|
147
|
+
for (const htmlFile of listHtmlFiles(outDir)) {
|
|
148
|
+
const html = fs.readFileSync(htmlFile, 'utf8');
|
|
149
|
+
let match;
|
|
150
|
+
while ((match = attrPattern.exec(html))) {
|
|
151
|
+
// srcset can hold a comma-separated list of "url descriptor" pairs -
|
|
152
|
+
// split it out; a plain src is already just the one URL.
|
|
153
|
+
const candidates = match[1].split(',').map((part) => part.trim().split(/\s+/)[0]).filter(Boolean);
|
|
154
|
+
for (const urlPath of candidates) {
|
|
155
|
+
if (!urlPath.startsWith('/') || urlPath.startsWith('//')) continue; // only root-relative - not external, not protocol-relative
|
|
156
|
+
if (!looksLikeAssetPath(urlPath)) continue;
|
|
157
|
+
const segments = urlPath.replace(/^\/+/, '').split('/');
|
|
158
|
+
const outPath = path.join(outDir, ...segments);
|
|
159
|
+
if (fs.existsSync(outPath)) continue; // already present - via public/'s passthrough copy, or a previous iteration of this same loop
|
|
160
|
+
const resolved = resolveOutsidePublic(urlPath, contentDir);
|
|
161
|
+
if (!resolved) continue; // doesn't resolve to a real file anywhere in the project either - leave the broken reference as-is, same as today
|
|
162
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
163
|
+
fs.copyFileSync(resolved, outPath);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function stylesAssetFallback(contentDir) {
|
|
170
|
+
return {
|
|
171
|
+
name: 'writedocs-styles-asset-fallback',
|
|
172
|
+
hooks: {
|
|
173
|
+
'astro:server:setup': ({ server }) => {
|
|
174
|
+
server.middlewares.use((req, res, next) => {
|
|
175
|
+
if (!req.url) return next();
|
|
176
|
+
const urlPath = req.url.split('?')[0];
|
|
177
|
+
if (!looksLikeAssetPath(urlPath)) return next();
|
|
178
|
+
if (fs.existsSync(publicPathFor(urlPath, contentDir))) return next(); // real public/ file - Vite's own static serving already handles it, don't shadow
|
|
179
|
+
const resolved = resolveOutsidePublic(urlPath, contentDir);
|
|
180
|
+
if (!resolved) return next();
|
|
181
|
+
res.setHeader('Content-Type', mimeFor(resolved));
|
|
182
|
+
fs.createReadStream(resolved).pipe(res);
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
'astro:build:done': async ({ dir }) => {
|
|
186
|
+
const outDir = fileURLToPath(dir);
|
|
187
|
+
const config = loadDocsConfig(contentDir);
|
|
188
|
+
// Pass 1: writedocs.json's own configured fields. Kept as its own,
|
|
189
|
+
// field-name-driven pass rather than folded into pass 2 below -
|
|
190
|
+
// background images/font sources render into CSS `url(...)`
|
|
191
|
+
// inside a `<style>` block, and seo.ogImage into a `<meta
|
|
192
|
+
// content="...">` tag, none of which pass 2's src="..."/
|
|
193
|
+
// srcset="..." attribute scan would ever see.
|
|
194
|
+
for (const urlPath of collectConfiguredAssetPaths(config)) {
|
|
195
|
+
const segments = urlPath.replace(/^\/+/, '').split('/');
|
|
196
|
+
const outPath = path.join(outDir, ...segments);
|
|
197
|
+
if (fs.existsSync(outPath)) continue; // already present via public/'s normal passthrough copy
|
|
198
|
+
const resolved = resolveOutsidePublic(urlPath, contentDir);
|
|
199
|
+
if (!resolved) continue; // not configured to a real file anywhere - same as today, just renders a broken <link>/<img>
|
|
200
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
201
|
+
fs.copyFileSync(resolved, outPath);
|
|
202
|
+
}
|
|
203
|
+
// Pass 2: arbitrary content-body assets - anything a page's own
|
|
204
|
+
// rendered HTML references by src/srcset that pass 1 didn't
|
|
205
|
+
// already account for.
|
|
206
|
+
copyReferencedContentAssets(outDir, contentDir);
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
/** Where writedocs' own generated/cached artifacts for a given content
|
|
6
|
+
* directory live: Astro's content-layer cache (`cacheDir`), the
|
|
7
|
+
* auto-generated OpenAPI stub pages the `generatedDocs` collection reads
|
|
8
|
+
* (see content.config.ts), and the resolved-operation JSON
|
|
9
|
+
* ApiPlayground.astro/ApiReferencePanel.astro read at render time (see
|
|
10
|
+
* lib/openapi-render.ts). All three used to live under
|
|
11
|
+
* `<contentDir>/.writedocs/` - moved out entirely (into the OS temp
|
|
12
|
+
* directory instead) so `writedocs dev`/`build` never writes anything
|
|
13
|
+
* into a person's own project folder for them to notice, gitignore, or
|
|
14
|
+
* accidentally commit.
|
|
15
|
+
*
|
|
16
|
+
* Namespaced by a short hash of contentDir's own resolved absolute path,
|
|
17
|
+
* not a fixed name, for two reasons: different content directories
|
|
18
|
+
* (this repo's own docs.json-examples/* fixtures, or two unrelated
|
|
19
|
+
* projects on the same machine using the same globally-installed
|
|
20
|
+
* writedocs) must never collide with each other on disk - see the
|
|
21
|
+
* "Content-layer cache collision" bug note in
|
|
22
|
+
* docs/dev/docs/architecture.mdx for what happens when they do - while
|
|
23
|
+
* repeated runs *of the same project* should keep reusing the same
|
|
24
|
+
* directory (Astro's content-layer cache is only useful if it survives
|
|
25
|
+
* across dev/build runs). The human-readable prefix is only there to
|
|
26
|
+
* make a real temp directory recognizable while debugging; nothing
|
|
27
|
+
* parses it back out.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately a plain .js module, not .ts: src/cli/generate-api-pages.js
|
|
30
|
+
* runs via plain Node before Astro/Vite's TS-aware pipeline exists (see
|
|
31
|
+
* that file's own comment on why it otherwise avoids importing from
|
|
32
|
+
* lib/config.ts), so this is the one shared path-computation helper it
|
|
33
|
+
* can still import directly. Every other call site (astro.config.mjs,
|
|
34
|
+
* content.config.ts, src/pages/[...slug].astro, src/pages/[...slug].md.ts,
|
|
35
|
+
* lib/config.ts, lib/openapi-render.ts) imports this exact same file too
|
|
36
|
+
* - all of them need to agree on the identical path, since one writes
|
|
37
|
+
* what another reads. */
|
|
38
|
+
function namespaceLabel(contentDir) {
|
|
39
|
+
const resolved = path.resolve(contentDir);
|
|
40
|
+
const hash = crypto.createHash('sha1').update(resolved).digest('hex').slice(0, 12);
|
|
41
|
+
const label = path.basename(resolved) || 'root';
|
|
42
|
+
return `${label}-${hash}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function writedocsTempDir(contentDir) {
|
|
46
|
+
return path.join(os.tmpdir(), 'writedocs', namespaceLabel(contentDir));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Where `astro build` actually writes its output during the build itself
|
|
50
|
+
* - deliberately *inside* the installed writedocs package (packageRoot),
|
|
51
|
+
* never contentDir/dist directly, and never OS temp either (unlike
|
|
52
|
+
* writedocsTempDir() above). build.js copies the finished result from
|
|
53
|
+
* here into the real `<contentDir>/dist` as an explicit final step once
|
|
54
|
+
* Astro's own build has fully finished - see that file's own comment for
|
|
55
|
+
* why a plain recursive copy, not Astro's default in-place output.
|
|
56
|
+
*
|
|
57
|
+
* The short version: Astro's static build stages its prerendered output
|
|
58
|
+
* in `<outDir>/.prerender/` and then does a plain `fs.rename()` into the
|
|
59
|
+
* real outDir (astro/dist/prerender/utils.js) - which throws `EXDEV:
|
|
60
|
+
* cross-device link not permitted` the moment outDir and that staging
|
|
61
|
+
* location land on different filesystems. Astro decides *where* to stage
|
|
62
|
+
* by checking whether outDir starts with `process.cwd()`
|
|
63
|
+
* (getOutDirWithinCwd) - pointing outDir at contentDir/dist directly (the
|
|
64
|
+
* obvious-looking fix) makes that check pass, but doesn't just move
|
|
65
|
+
* *where* staging happens, it moves *where Astro's own SSR/prerender
|
|
66
|
+
* bundle physically executes from* - and that bundle isn't fully
|
|
67
|
+
* self-contained: some of Astro's own dependencies (Tailwind's Vite
|
|
68
|
+
* plugin registering a loader hook that reaches for a small terminal-
|
|
69
|
+
* color-formatting package, observed directly while diagnosing this) do
|
|
70
|
+
* a bare-specifier `import` Node has to resolve the normal way - walking
|
|
71
|
+
* up from wherever that code physically executes looking for a
|
|
72
|
+
* `node_modules` containing it. contentDir has no `node_modules` at all
|
|
73
|
+
* by design (see docs/dev/docs/architecture.mdx) - a real consumer's
|
|
74
|
+
* project is just `writedocs.json` + a content folder - so once the SSR
|
|
75
|
+
* bundle executes from inside contentDir, *that* resolution fails
|
|
76
|
+
* instead, trading one crash for a less predictable one that depends on
|
|
77
|
+
* which of Astro's own runtime code paths a given site's content happens
|
|
78
|
+
* to trigger.
|
|
79
|
+
*
|
|
80
|
+
* Staying inside packageRoot keeps every one of those bare specifiers
|
|
81
|
+
* resolving exactly the way it already does today (this is where
|
|
82
|
+
* `node_modules` actually lives), while a plain `fs.cp()` - which copies
|
|
83
|
+
* across filesystem boundaries just fine, unlike `rename()` - handles the
|
|
84
|
+
* one genuinely cross-device step explicitly, once, after Astro is
|
|
85
|
+
* completely done needing any of its own dependencies to still resolve.
|
|
86
|
+
*
|
|
87
|
+
* Namespaced by the same contentDir-hash convention as writedocsTempDir()
|
|
88
|
+
* above, so two different content directories building against the same
|
|
89
|
+
* globally-installed writedocs (or two concurrent builds) never collide
|
|
90
|
+
* here either. */
|
|
91
|
+
export function writedocsBuildStagingDir(packageRoot, contentDir) {
|
|
92
|
+
return path.join(packageRoot, '.writedocs-build', namespaceLabel(contentDir));
|
|
93
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Astro's own file-based convention: a static build (`output: 'static'`,
|
|
3
|
+
// this package's only supported mode - see astro.config.mjs) emits this
|
|
4
|
+
// file's output as a literal `404.html` at the site root, and most static
|
|
5
|
+
// hosts (Netlify, Vercel, GitHub Pages, Cloudflare Pages, ...) serve it
|
|
6
|
+
// automatically for any unmatched path. No params, no getStaticPaths -
|
|
7
|
+
// this is a single fixed route, same category as llms.txt.ts/
|
|
8
|
+
// llms-full.txt.ts (see their own comments on Astro auto-prerendering a
|
|
9
|
+
// non-dynamic page at its literal path).
|
|
10
|
+
import { loadDocsConfig } from '../lib/config';
|
|
11
|
+
import BaseLayout from '../layout/BaseLayout.astro';
|
|
12
|
+
|
|
13
|
+
const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
14
|
+
const config = loadDocsConfig(contentDir);
|
|
15
|
+
|
|
16
|
+
// writedocs.json's `notFound.{title,description}` (notFoundSchema in
|
|
17
|
+
// lib/config.ts) - both optional, with the hardcoded fallbacks below, so
|
|
18
|
+
// a site gets a reasonably branded 404 page without having to configure
|
|
19
|
+
// anything.
|
|
20
|
+
const title = config.notFound.title ?? 'Page not found';
|
|
21
|
+
const description = config.notFound.description ?? "The page you're looking for doesn't exist or has moved.";
|
|
22
|
+
---
|
|
23
|
+
<BaseLayout config={config} title={title} description={description} path="/404/" seo={{ noindex: true }} mode="frame">
|
|
24
|
+
<div class="wd-404">
|
|
25
|
+
<p class="wd-404-eyebrow">404</p>
|
|
26
|
+
<h1>{title}</h1>
|
|
27
|
+
<p class="wd-404-description">{description}</p>
|
|
28
|
+
<a class="wd-404-home" href="/">← Back to home</a>
|
|
29
|
+
</div>
|
|
30
|
+
</BaseLayout>
|
|
31
|
+
|
|
32
|
+
<style>
|
|
33
|
+
.wd-404 {
|
|
34
|
+
max-width: 560px;
|
|
35
|
+
margin: 0 auto;
|
|
36
|
+
padding: 5rem 1.5rem 6rem;
|
|
37
|
+
text-align: center;
|
|
38
|
+
}
|
|
39
|
+
.wd-404-eyebrow {
|
|
40
|
+
margin: 0 0 0.5rem;
|
|
41
|
+
font-size: 0.85rem;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
letter-spacing: 0.08em;
|
|
44
|
+
color: var(--wd-primary);
|
|
45
|
+
}
|
|
46
|
+
.wd-404 h1 {
|
|
47
|
+
margin: 0 0 0.75rem;
|
|
48
|
+
font-size: 1.75rem;
|
|
49
|
+
}
|
|
50
|
+
.wd-404-description {
|
|
51
|
+
margin: 0 0 2rem;
|
|
52
|
+
color: var(--wd-text-muted);
|
|
53
|
+
}
|
|
54
|
+
.wd-404-home {
|
|
55
|
+
color: var(--wd-primary);
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
}
|
|
59
|
+
.wd-404-home:hover {
|
|
60
|
+
text-decoration: underline;
|
|
61
|
+
}
|
|
62
|
+
</style>
|