@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,750 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The site's root HTML shell: <head> meta/SEO tags, the writedocs.json
|
|
3
|
+
// `integrations`/`scripts` passthrough, the color-theme flash-
|
|
4
|
+
// prevention script, the dismissible `banner`, and the three-column
|
|
5
|
+
// .wd-shell (sidebar/main/toc) that every page's own content lands in.
|
|
6
|
+
// Everything else that used to live directly in this file - the topbar,
|
|
7
|
+
// the mobile hamburger drawer, the search modal, the footer, and the
|
|
8
|
+
// analytics/AI-chat integration scripts - is now its own component,
|
|
9
|
+
// composed below. This file had grown to ~2200 lines covering all of
|
|
10
|
+
// those unrelated concerns in one place before that split; each piece is
|
|
11
|
+
// small enough on its own now to read start-to-finish without losing the
|
|
12
|
+
// thread, and a change to (say) the mobile menu no longer means scrolling
|
|
13
|
+
// past a thousand lines of unrelated topbar/search/footer code to find
|
|
14
|
+
// the right spot. This file is the only .astro file that stays directly
|
|
15
|
+
// in src/layout/ - every other component lives in src/layout/components/,
|
|
16
|
+
// and every plain .css file (base.css, banner.css, and the rest each
|
|
17
|
+
// component below imports for itself) lives in src/layout/styles/ - this
|
|
18
|
+
// file is the layout's single entry point/orchestrator, everything else
|
|
19
|
+
// is a piece it composes.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
// Named `nodePath`, not the more natural `path` - this component already
|
|
22
|
+
// destructures a prop called `path` (the current page's own URL path,
|
|
23
|
+
// see Props below) from Astro.props, which would otherwise shadow a
|
|
24
|
+
// module-level `path` import inside this component's own scope, silently
|
|
25
|
+
// turning every `path.join(...)` call into a TypeError at render time
|
|
26
|
+
// ("path.join is not a function") rather than a compile-time error, since
|
|
27
|
+
// both are just plain identifiers to the bundler. Hit and fixed while
|
|
28
|
+
// wiring up findRootAssets() below - real bug, not a hypothetical.
|
|
29
|
+
import nodePath from 'node:path';
|
|
30
|
+
import {
|
|
31
|
+
resolveSiteUrl,
|
|
32
|
+
resolveAbsoluteUrl,
|
|
33
|
+
findRootAssets,
|
|
34
|
+
resolveNavbarColor,
|
|
35
|
+
contrastTextColor,
|
|
36
|
+
resolveFonts,
|
|
37
|
+
googleFontsHref,
|
|
38
|
+
fontFaceRule,
|
|
39
|
+
type DocsConfig,
|
|
40
|
+
type Selector,
|
|
41
|
+
type GlobalDropdownView,
|
|
42
|
+
type SeoFields,
|
|
43
|
+
} from '../lib/config';
|
|
44
|
+
import '../styles/global.css';
|
|
45
|
+
// rehypeKatex (astro.config.mjs) renders every $inline$/$$block$$ math
|
|
46
|
+
// span into real KaTeX markup at build time - this is the one runtime
|
|
47
|
+
// dependency that output needs: layout/glyph CSS for the .katex class
|
|
48
|
+
// tree, self-hosted (bundled by Vite, fonts included via its own url()
|
|
49
|
+
// asset resolution) rather than a KaTeX CDN link. Loaded site-wide, same
|
|
50
|
+
// as global.css above - a page with no math in it pays for one small,
|
|
51
|
+
// cached stylesheet and nothing else (no fonts are actually fetched
|
|
52
|
+
// unless a page's rendered KaTeX markup uses their glyphs). KaTeX's own
|
|
53
|
+
// output deliberately sets no explicit text color, inheriting `color`
|
|
54
|
+
// from whatever wraps it - so it already follows this site's light/dark
|
|
55
|
+
// theme with no extra CSS needed here.
|
|
56
|
+
import 'katex/dist/katex.min.css';
|
|
57
|
+
import './styles/base.css';
|
|
58
|
+
import './styles/banner.css';
|
|
59
|
+
// Astro's client-side router - swaps <body> content between same-origin
|
|
60
|
+
// navigations via the native View Transition API instead of a hard
|
|
61
|
+
// browser reload, so the topbar/sidebar/footer don't visibly flash/
|
|
62
|
+
// "twitch" on every click the way a full document reload does. Every
|
|
63
|
+
// interactive component's own <script> (TopBar's dropdowns, search,
|
|
64
|
+
// TableOfContents, Mermaid, the copy-page menu, ...) already calls its
|
|
65
|
+
// init function once directly *and* re-registers it on the
|
|
66
|
+
// `astro:page-load` event (see e.g. TableOfContents.astro's own
|
|
67
|
+
// `initToc(document); document.addEventListener('astro:page-load', ...)`
|
|
68
|
+
// pair) - that event only ever fires when this router is active, so this
|
|
69
|
+
// was already the intended architecture, just missing this one piece
|
|
70
|
+
// that actually turns it on.
|
|
71
|
+
import { ClientRouter } from 'astro:transitions';
|
|
72
|
+
import TopBar from './components/TopBar.astro';
|
|
73
|
+
import MobileMenu from './components/MobileMenu.astro';
|
|
74
|
+
import SearchModal from './components/SearchModal.astro';
|
|
75
|
+
import ImageZoom from './components/ImageZoom.astro';
|
|
76
|
+
import SiteFooter from './components/SiteFooter.astro';
|
|
77
|
+
import AnalyticsScripts from './components/AnalyticsScripts.astro';
|
|
78
|
+
import AskAiWidget from './components/AskAiWidget.astro';
|
|
79
|
+
|
|
80
|
+
interface Props {
|
|
81
|
+
config: DocsConfig;
|
|
82
|
+
title: string;
|
|
83
|
+
// Falls back to config.description when a page has none of its own -
|
|
84
|
+
// see [...slug].astro, which passes entry.data.description straight
|
|
85
|
+
// through without resolving the fallback itself.
|
|
86
|
+
description?: string;
|
|
87
|
+
// The current page's own path (e.g. "/" or "/guides/foo/"), used to
|
|
88
|
+
// build the canonical <link> and og:url/twitter:url - [...slug].astro
|
|
89
|
+
// already computes this via hrefForSlug(currentFileId) for its own
|
|
90
|
+
// prev/next links, so it's passed through rather than recomputed here.
|
|
91
|
+
path: string;
|
|
92
|
+
// Site-wide seo (writedocs.json) merged with this page's own frontmatter
|
|
93
|
+
// seo override - see mergeSeo() in lib/config.ts. [...slug].astro does
|
|
94
|
+
// the merging; this component just renders the result.
|
|
95
|
+
seo?: SeoFields;
|
|
96
|
+
// One Selector per level of the active page's navigation path (tabs,
|
|
97
|
+
// versions, languages, products, or a dropdown used as a nested level)
|
|
98
|
+
// - see resolveSections()/buildSelectors() in lib/config.ts for how
|
|
99
|
+
// this is computed from however the site's writedocs.json nests things.
|
|
100
|
+
// Passed straight through to TopBar.astro and MobileMenu.astro, which
|
|
101
|
+
// each derive their own view of it (TopBar's switcherSelectors/
|
|
102
|
+
// tabSelectors vs. MobileMenu's per-level accordion rows).
|
|
103
|
+
selectors?: Selector[];
|
|
104
|
+
// The always-visible topbar dropdown list from navigation.global.dropdowns
|
|
105
|
+
// - independent of the active selectors, shown on every page. Also
|
|
106
|
+
// passed straight through to TopBar.astro and MobileMenu.astro.
|
|
107
|
+
globalDropdowns?: GlobalDropdownView[];
|
|
108
|
+
// Per-page frontmatter `mode` (content.config.ts) - controls how much
|
|
109
|
+
// site chrome wraps this page. 'blank' drops the topbar and the whole
|
|
110
|
+
// .wd-shell (sidebar/toc columns) entirely, rendering only <slot />
|
|
111
|
+
// directly in <body> - see the template below. Every other mode
|
|
112
|
+
// (including 'custom') still gets the topbar; [...slug].astro is what
|
|
113
|
+
// decides whether to actually pass sidebar/toc slot content for
|
|
114
|
+
// 'wide'/'frame'/'custom' (BaseLayout has no sidebar/toc content of its
|
|
115
|
+
// own to conditionally omit - an unfilled named slot in Astro already
|
|
116
|
+
// renders nothing, so this component only needs to know whether to
|
|
117
|
+
// render the *column wrapper divs* at all).
|
|
118
|
+
mode?: 'default' | 'wide' | 'frame' | 'custom' | 'blank';
|
|
119
|
+
}
|
|
120
|
+
const {
|
|
121
|
+
config,
|
|
122
|
+
title,
|
|
123
|
+
description,
|
|
124
|
+
path,
|
|
125
|
+
seo = {},
|
|
126
|
+
selectors = [],
|
|
127
|
+
globalDropdowns = [],
|
|
128
|
+
mode = 'default',
|
|
129
|
+
} = Astro.props as Props;
|
|
130
|
+
const showSidebarCol = mode === 'default' || mode === 'wide';
|
|
131
|
+
const showTocCol = mode === 'default';
|
|
132
|
+
// Meta tag resolution - all derived once here rather than inline in the
|
|
133
|
+
// template below, so the JSX stays a straight list of conditional <meta>
|
|
134
|
+
// tags instead of repeating this logic at each call site.
|
|
135
|
+
const siteUrl = resolveSiteUrl(config);
|
|
136
|
+
const effectiveDescription = description ?? config.description;
|
|
137
|
+
const canonicalUrl = siteUrl ? `${siteUrl}${path}` : null;
|
|
138
|
+
const ogType = seo.ogType ?? 'website';
|
|
139
|
+
const absoluteOgImage = seo.ogImage ? resolveAbsoluteUrl(siteUrl, seo.ogImage) : undefined;
|
|
140
|
+
// summary_large_image needs a real image to make sense as a card size;
|
|
141
|
+
// falls back to the smaller "summary" card when no image is set, rather
|
|
142
|
+
// than always defaulting to the large-image card regardless.
|
|
143
|
+
const twitterCard = seo.twitterCard ?? (absoluteOgImage ? 'summary_large_image' : 'summary');
|
|
144
|
+
// Light-mode values come straight from styles.colors/styles.background
|
|
145
|
+
// (with defaults); dark-mode values fall back to their light counterpart
|
|
146
|
+
// when the site's own dark override doesn't set them, so a site only has
|
|
147
|
+
// to specify what actually changes for dark mode. Both sets are always
|
|
148
|
+
// computed and handed to the CSS below (light-mode ones as sensible
|
|
149
|
+
// starting defaults, but the .wd-* class hides/shows appropriately) - see
|
|
150
|
+
// the `[data-theme="dark"]` override rule below, which is what actually
|
|
151
|
+
// switches which set is live. Unlike every other bit of CSS this file
|
|
152
|
+
// used to own directly, this specific pair of rules can't move into a
|
|
153
|
+
// plain external .css file the way base.css/banner.css did - `define:vars`
|
|
154
|
+
// (right below) is what lets these raw hex/URL values get interpolated
|
|
155
|
+
// into custom-property assignments at all, and that directive only works
|
|
156
|
+
// on a <style> tag physically inside an .astro file's own template.
|
|
157
|
+
const lightPrimary = config.styles.colors.primary;
|
|
158
|
+
const lightText = config.styles.colors.text ?? '#0f172a';
|
|
159
|
+
const darkPrimary = config.styles.colors.dark?.primary ?? lightPrimary;
|
|
160
|
+
const darkText = config.styles.colors.dark?.text ?? '#e2e8f0';
|
|
161
|
+
// styles.background.colors is now the single source for any background
|
|
162
|
+
// color - both the flat --wd-background surface (dropdowns/modals/kbd
|
|
163
|
+
// chips/footer/topbar-fallback/etc., every other `var(--wd-background)`
|
|
164
|
+
// call site) and the <body> canvas paint below. There used to be a
|
|
165
|
+
// separate styles.colors.background/colors.dark.background pair driving
|
|
166
|
+
// just the --wd-background half, independent of this - see config.ts's
|
|
167
|
+
// own comment on why that split was removed; `lightBackground`/
|
|
168
|
+
// `darkBackground` here is what used to read that other field.
|
|
169
|
+
const lightBackground = config.styles.background?.colors?.light ?? '#ffffff';
|
|
170
|
+
const darkBackground = config.styles.background?.colors?.dark ?? '#0b1120';
|
|
171
|
+
// The topbar/mobile-menu-panel background - falls back to the page's own
|
|
172
|
+
// background (above) when a site doesn't set it, so an unconfigured navbar
|
|
173
|
+
// keeps blending into the page exactly as it always has; only sites that
|
|
174
|
+
// explicitly set `styles.navbar.light`/`.dark` get a topbar that looks
|
|
175
|
+
// different from the page behind it. resolveNavbarColor() (lib/config.ts)
|
|
176
|
+
// is what splits the string-or-{background,accent} union `navbar.light`/
|
|
177
|
+
// `.dark` can now be into its two parts - see its own comment.
|
|
178
|
+
const navLight = resolveNavbarColor(config.styles.navbar?.light, lightBackground);
|
|
179
|
+
const navDark = resolveNavbarColor(config.styles.navbar?.dark, darkBackground);
|
|
180
|
+
const lightNavbarBg = navLight.background;
|
|
181
|
+
const darkNavbarBg = navDark.background;
|
|
182
|
+
// Whether this side of `styles.navbar` was configured at all - string or
|
|
183
|
+
// object form, doesn't matter, just "is there a writedocs.json value here" -
|
|
184
|
+
// not merely "does lightNavbarBg differ from the page background", since
|
|
185
|
+
// a site could deliberately set navbar to the *same* color as the page
|
|
186
|
+
// background and this should still count as configured. What this gates:
|
|
187
|
+
// once true, the navbar's own text/icon color stops assuming it's sitting
|
|
188
|
+
// on something close to the page background (--wd-text's own tuning
|
|
189
|
+
// target) and switches to being computed fresh against whatever
|
|
190
|
+
// `background` actually resolved to.
|
|
191
|
+
const lightNavbarConfigured = config.styles.navbar?.light !== undefined;
|
|
192
|
+
const darkNavbarConfigured = config.styles.navbar?.dark !== undefined;
|
|
193
|
+
// --wd-navbar-foreground/-muted: the topbar's own text/icon color, plain
|
|
194
|
+
// (resting) state. Never a writedocs.json value, even indirectly - always
|
|
195
|
+
// contrastTextColor(lightNavbarBg) (lib/config.ts, picks black or white by
|
|
196
|
+
// luminance) the moment this side of `navbar` is configured at all, or the
|
|
197
|
+
// literal --wd-text/--wd-text-muted var references when it isn't, so an
|
|
198
|
+
// unconfigured navbar renders pixel-identical to before this feature
|
|
199
|
+
// existed. An earlier version of this let an object-form field double as
|
|
200
|
+
// both "the navbar's accent color" and "the navbar's text color" - real
|
|
201
|
+
// bug, caught immediately: a site picking a light accent (to stay legible
|
|
202
|
+
// against a dark/saturated background, the whole point of setting it) got
|
|
203
|
+
// that same light color as its *text*, which is exactly backwards. Text
|
|
204
|
+
// legibility isn't a design choice worth exposing at all here - it's
|
|
205
|
+
// always computed, correctly, every time. The muted variant mixes that
|
|
206
|
+
// same resolved color toward transparent (not toward lightNavbarBg, which
|
|
207
|
+
// would drag it back toward the background's own hue and undo the
|
|
208
|
+
// contrast fix) - a plain opacity reduction, so it stays legible
|
|
209
|
+
// regardless of what hue the navbar itself happens to be.
|
|
210
|
+
const lightNavbarFg = lightNavbarConfigured ? contrastTextColor(lightNavbarBg) : 'var(--wd-text)';
|
|
211
|
+
const darkNavbarFg = darkNavbarConfigured ? contrastTextColor(darkNavbarBg) : 'var(--wd-text)';
|
|
212
|
+
const lightNavbarFgMuted = lightNavbarConfigured
|
|
213
|
+
? `color-mix(in srgb, ${lightNavbarFg} 70%, transparent)`
|
|
214
|
+
: 'var(--wd-text-muted)';
|
|
215
|
+
const darkNavbarFgMuted = darkNavbarConfigured
|
|
216
|
+
? `color-mix(in srgb, ${darkNavbarFg} 70%, transparent)`
|
|
217
|
+
: 'var(--wd-text-muted)';
|
|
218
|
+
// --wd-navbar-accent: the tab-hover-underline/active-tab-fill color inside
|
|
219
|
+
// the topbar specifically - `accent` on the object form (a real, legitimate
|
|
220
|
+
// design choice, unlike text color above), falling back to the sitewide
|
|
221
|
+
// --wd-primary (today's exact behavior) when unset - a site that sets a
|
|
222
|
+
// navbar background but no accent keeps its ordinary brand-colored
|
|
223
|
+
// active-tab fill exactly as before.
|
|
224
|
+
const lightNavbarAccent = navLight.accent ?? lightPrimary;
|
|
225
|
+
const darkNavbarAccent = navDark.accent ?? darkPrimary;
|
|
226
|
+
// --wd-navbar-accent-text: the active tab's own text color, painted on top
|
|
227
|
+
// of --wd-navbar-accent above. Hardcoded to white before this feature
|
|
228
|
+
// existed, which only ever looked right because every accent color in
|
|
229
|
+
// practice (always --wd-primary until now) happened to be dark/saturated
|
|
230
|
+
// enough for white text - contrastTextColor() (lib/config.ts) picks
|
|
231
|
+
// black or white by actual luminance instead, so a light `accent` (e.g.
|
|
232
|
+
// a pale one against a dark navbar) still gets legible active-tab text
|
|
233
|
+
// rather than assuming white always works.
|
|
234
|
+
const lightNavbarAccentText = contrastTextColor(lightNavbarAccent);
|
|
235
|
+
const darkNavbarAccentText = contrastTextColor(darkNavbarAccent);
|
|
236
|
+
// --wd-navbar-border: the switcher pills' (version/language/product) own
|
|
237
|
+
// border. Never the flat --wd-border (a fixed neutral gray/slate tuned for
|
|
238
|
+
// sitting on the page background) once navbar is configured - that reads
|
|
239
|
+
// as a harsh, near-white outline against a saturated/colored navbar
|
|
240
|
+
// background, since it's not picked with that background in mind at all.
|
|
241
|
+
// Same mix-toward-transparent technique as the muted foreground above: a
|
|
242
|
+
// low-opacity tint of the already-contrast-correct navbarFg, so it stays
|
|
243
|
+
// soft and legible against any navbar hue instead of a flat, unrelated
|
|
244
|
+
// gray. Unconfigured navbar keeps the literal var(--wd-border) reference,
|
|
245
|
+
// pixel-identical to before this existed.
|
|
246
|
+
const lightNavbarBorder = lightNavbarConfigured
|
|
247
|
+
? `color-mix(in srgb, ${lightNavbarFg} 25%, transparent)`
|
|
248
|
+
: 'var(--wd-border)';
|
|
249
|
+
const darkNavbarBorder = darkNavbarConfigured
|
|
250
|
+
? `color-mix(in srgb, ${darkNavbarFg} 25%, transparent)`
|
|
251
|
+
: 'var(--wd-border)';
|
|
252
|
+
// The <body> canvas's own paint - same color as --wd-background above
|
|
253
|
+
// (there's only one background color to configure now), plus an optional
|
|
254
|
+
// image layered on top. `images.light`/`.dark` are plain public/-relative
|
|
255
|
+
// paths (same convention as styles.logo/favicon) - wrapped in url(...) here
|
|
256
|
+
// (or 'none' when unset) since CSS custom properties can't have url()
|
|
257
|
+
// applied to them after the fact (`url(var(--x))` isn't valid CSS - the
|
|
258
|
+
// value itself has to already be a full `url(...)` token, which is what
|
|
259
|
+
// these two variables hold).
|
|
260
|
+
const lightPageBgColor = lightBackground;
|
|
261
|
+
const darkPageBgColor = darkBackground;
|
|
262
|
+
const lightPageBgImage = config.styles.background?.images?.light
|
|
263
|
+
? `url('${config.styles.background.images.light}')`
|
|
264
|
+
: 'none';
|
|
265
|
+
const darkPageBgImage = config.styles.background?.images?.dark
|
|
266
|
+
? `url('${config.styles.background.images.dark}')`
|
|
267
|
+
: 'none';
|
|
268
|
+
// A logo is either one path used for both modes, or a {light, dark,
|
|
269
|
+
// label} object - normalized to a single light/dark pair here (the one
|
|
270
|
+
// place that needs to know which form writedocs.json used) so TopBar.astro,
|
|
271
|
+
// MobileMenu.astro, and SiteFooter.astro - each of which render their own
|
|
272
|
+
// copy of the brand link - never need to branch on it themselves. The
|
|
273
|
+
// brand link shows the image(s) alone by default - no site name text next
|
|
274
|
+
// to it, since a real logo asset is usually already a wordmark. `label`
|
|
275
|
+
// (only settable on the object form - see config.ts) opts back into
|
|
276
|
+
// showing text next to the image - or, since `light`/`dark` are
|
|
277
|
+
// themselves optional on that form, stands entirely on its own:
|
|
278
|
+
// `styles.logo: { label: "..." }` with no images at all overrides just the
|
|
279
|
+
// brand text without adding any logo image. A site with neither a logo
|
|
280
|
+
// image nor a label falls back to plain config.name text, so the brand
|
|
281
|
+
// link is never left empty.
|
|
282
|
+
const logo = config.styles.logo;
|
|
283
|
+
const logoLight = typeof logo === 'string' ? logo : logo?.light;
|
|
284
|
+
const logoDark = typeof logo === 'string' ? logo : logo?.dark;
|
|
285
|
+
const logoLabel = typeof logo === 'string' ? undefined : logo?.label;
|
|
286
|
+
const hasLogoImage = Boolean(logoLight || logoDark);
|
|
287
|
+
const brandLabel = logoLabel ?? (hasLogoImage ? undefined : config.name);
|
|
288
|
+
|
|
289
|
+
// writedocs.json's `footer.logo` (same {light, dark, label} shape as
|
|
290
|
+
// `styles.logo` above, via config.ts's shared logoSchema) - unset by
|
|
291
|
+
// default, in which case SiteFooter.astro just gets the same
|
|
292
|
+
// logoLight/logoDark/logoLabel as the topbar/mobile menu above. Set, it
|
|
293
|
+
// replaces those entirely (not just fills in whichever side is missing)
|
|
294
|
+
// before ever reaching SiteFooter - a footer wanting a different mark
|
|
295
|
+
// than the topbar (or none at all - `styles.logo` set, `footer.logo`
|
|
296
|
+
// deliberately absent-of-images-but-present, e.g. `{ label: "" }`, isn't
|
|
297
|
+
// a real use case worth solving here) shouldn't still show the sitewide
|
|
298
|
+
// one alongside or underneath it.
|
|
299
|
+
const footerLogoConfig = config.footer.logo;
|
|
300
|
+
const footerLogoLight = footerLogoConfig
|
|
301
|
+
? typeof footerLogoConfig === 'string'
|
|
302
|
+
? footerLogoConfig
|
|
303
|
+
: footerLogoConfig.light
|
|
304
|
+
: logoLight;
|
|
305
|
+
const footerLogoDark = footerLogoConfig
|
|
306
|
+
? typeof footerLogoConfig === 'string'
|
|
307
|
+
? footerLogoConfig
|
|
308
|
+
: footerLogoConfig.dark
|
|
309
|
+
: logoDark;
|
|
310
|
+
|
|
311
|
+
// Zero-config custom CSS/JS - any `.css`/`.js` file anywhere in the
|
|
312
|
+
// project (root, `docs/`, `snippets/`, `public/`, any subfolder -
|
|
313
|
+
// `dist/`/`node_modules/`/etc. excluded, see findRootAssets() in
|
|
314
|
+
// lib/config.ts) is auto-loaded on every page, no writedocs.json entry
|
|
315
|
+
// needed. Read fresh on every render (not cached at module scope) so an
|
|
316
|
+
// edit to one of these files shows up on the next `writedocs dev` page
|
|
317
|
+
// load without a server restart - same freshness `loadDocsConfig(contentDir)`
|
|
318
|
+
// itself already gives writedocs.json edits in [...slug].astro/404.astro.
|
|
319
|
+
// Rendered in addition to, not instead of, writedocs.json's own explicit
|
|
320
|
+
// `scripts` field - that still exists for external/CDN script URLs
|
|
321
|
+
// (`src`) and inline content that should only load under deliberate
|
|
322
|
+
// config, and renders before these so a file living in the project (the
|
|
323
|
+
// more locally-owned override) wins over a URL possibly pointing at
|
|
324
|
+
// someone else's asset.
|
|
325
|
+
const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
326
|
+
const rootAssets = findRootAssets(contentDir);
|
|
327
|
+
// `name` is a POSIX-separated path relative to contentDir (e.g.
|
|
328
|
+
// "docs/theme.css") - split and rejoin with nodePath.join rather than
|
|
329
|
+
// handing the raw string straight to it, so a nested asset resolves
|
|
330
|
+
// correctly on every OS this might run on, not just POSIX ones.
|
|
331
|
+
const rootCssContents = rootAssets.css.map((name) => fs.readFileSync(nodePath.join(contentDir, ...name.split('/')), 'utf-8'));
|
|
332
|
+
const rootJsContents = rootAssets.js.map((name) => fs.readFileSync(nodePath.join(contentDir, ...name.split('/')), 'utf-8'));
|
|
333
|
+
// public/*.css and public/*.js auto-load too, but as <link>/<script src>
|
|
334
|
+
// pointing at their already-public URL (see findRootAssets()'s own
|
|
335
|
+
// comment for why - no point inlining a file that's also independently
|
|
336
|
+
// fetchable at that exact URL). publicJsHrefs is filtered against
|
|
337
|
+
// writedocs.json's own `scripts.head` so a site that hasn't yet removed an
|
|
338
|
+
// explicit `src` entry for the same public/ file doesn't get that href
|
|
339
|
+
// rendered twice - the explicit entry still works, it's just redundant
|
|
340
|
+
// with this now. publicCssHrefs has no equivalent filter to apply -
|
|
341
|
+
// there's no explicit stylesheet-URL config field anymore.
|
|
342
|
+
const explicitScriptSrcs = new Set(config.scripts.head.map((s) => s.src).filter((src): src is string => Boolean(src)));
|
|
343
|
+
const publicCssHrefs = rootAssets.publicCss;
|
|
344
|
+
const publicJsHrefs = rootAssets.publicJs.filter((href) => !explicitScriptSrcs.has(href));
|
|
345
|
+
|
|
346
|
+
// styles.fonts (see fontsSchema/resolveFonts() in lib/config.ts) - resolves
|
|
347
|
+
// to plain Inter, genuinely loaded via Google Fonts (not just named in a
|
|
348
|
+
// fallback stack the way base.css used to), when a site sets no `fonts`
|
|
349
|
+
// field at all. `fontStack()` quotes the family name (safe even for a
|
|
350
|
+
// single-word name, and required for one with spaces like "Playfair
|
|
351
|
+
// Display") and appends the same system-font fallback chain base.css
|
|
352
|
+
// always used, so a reader who somehow loads the page before the web font
|
|
353
|
+
// finishes fetching still sees a reasonable font rather than the browser
|
|
354
|
+
// default serif. `heading`/`body` are left `null` (not defaulted to the
|
|
355
|
+
// same value as `base`) whenever a site hasn't overridden them - base.css
|
|
356
|
+
// falls back to `--wd-font-family` itself via CSS var(), so a `null` here
|
|
357
|
+
// deliberately never gets its own CSS var written into the style block
|
|
358
|
+
// below.
|
|
359
|
+
const resolvedFonts = resolveFonts(config);
|
|
360
|
+
const fontsGoogleHref = googleFontsHref(resolvedFonts);
|
|
361
|
+
function fontStack(family: string): string {
|
|
362
|
+
return `'${family}', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
|
|
363
|
+
}
|
|
364
|
+
// heading/body already fall back to `resolvedFonts.base` right here (not
|
|
365
|
+
// left as a CSS var() fallback chain for :root below to resolve) - same
|
|
366
|
+
// "fully resolve in JS, plain values into define:vars" approach every
|
|
367
|
+
// other themeable value in this file already uses (see lightPrimary/
|
|
368
|
+
// darkPrimary etc. above), rather than introducing a different pattern
|
|
369
|
+
// just for fonts.
|
|
370
|
+
const wdFontFamily = fontStack(resolvedFonts.base.family);
|
|
371
|
+
const wdFontFamilyHeading = resolvedFonts.heading ? fontStack(resolvedFonts.heading.family) : wdFontFamily;
|
|
372
|
+
const wdFontFamilyBody = resolvedFonts.body ? fontStack(resolvedFonts.body.family) : wdFontFamily;
|
|
373
|
+
// One @font-face rule per source-based font actually configured (Google
|
|
374
|
+
// Fonts, covered by fontsGoogleHref above instead, never reach here) - this
|
|
375
|
+
// only ever gets the right *file* loaded (the @font-face `font-weight`
|
|
376
|
+
// descriptor, or the Google Fonts URL's `:wght@` axis inside
|
|
377
|
+
// googleFontsHref() above), not the actual rendered boldness of any
|
|
378
|
+
// element - see wdFontWeightCss right below for that other half.
|
|
379
|
+
const fontFaceCss = [fontFaceRule(resolvedFonts.base), fontFaceRule(resolvedFonts.heading), fontFaceRule(resolvedFonts.body)]
|
|
380
|
+
.filter(Boolean)
|
|
381
|
+
.join('\n');
|
|
382
|
+
// Without this, a configured `weight` only ever changed which font *file*
|
|
383
|
+
// loaded - h1-h6 still rendered at the browser's own default `font-weight:
|
|
384
|
+
// bold` (a user-agent-stylesheet rule, present regardless of which file
|
|
385
|
+
// actually loaded), so a heading weight of e.g. 400 was silently faux-
|
|
386
|
+
// bolded back to looking bold anyway, with no visible effect from
|
|
387
|
+
// changing the config at all. `resolvedFonts.heading?.weight ?? base.weight`
|
|
388
|
+
// falls back the same way family already does (an override with no
|
|
389
|
+
// weight of its own still inherits the top-level one); `undefined` when
|
|
390
|
+
// nothing at any level configured a weight, which deliberately renders no
|
|
391
|
+
// rule at all here - an unconfigured site keeps today's plain browser-
|
|
392
|
+
// default bold headings/normal body text exactly as before this feature
|
|
393
|
+
// existed, not a forced 400/700 nobody asked for.
|
|
394
|
+
const wdFontWeightHeading = resolvedFonts.heading?.weight ?? resolvedFonts.base.weight;
|
|
395
|
+
const wdFontWeightBody = resolvedFonts.body?.weight ?? resolvedFonts.base.weight;
|
|
396
|
+
const fontWeightCss = [
|
|
397
|
+
wdFontWeightHeading !== undefined ? `h1, h2, h3, h4, h5, h6 { font-weight: ${wdFontWeightHeading}; }` : '',
|
|
398
|
+
wdFontWeightBody !== undefined ? `body { font-weight: ${wdFontWeightBody}; }` : '',
|
|
399
|
+
]
|
|
400
|
+
.filter(Boolean)
|
|
401
|
+
.join('\n');
|
|
402
|
+
const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
|
|
403
|
+
---
|
|
404
|
+
<!doctype html>
|
|
405
|
+
<html lang="en">
|
|
406
|
+
<head>
|
|
407
|
+
<meta charset="UTF-8" />
|
|
408
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
409
|
+
<title>{title} · {config.name}</title>
|
|
410
|
+
<ClientRouter />
|
|
411
|
+
{effectiveDescription && <meta name="description" content={effectiveDescription} />}
|
|
412
|
+
{seo.keywords && seo.keywords.length > 0 && <meta name="keywords" content={seo.keywords.join(', ')} />}
|
|
413
|
+
{seo.noindex && <meta name="robots" content="noindex, nofollow" />}
|
|
414
|
+
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
|
|
415
|
+
{config.styles.favicon && <link rel="icon" href={config.styles.favicon} />}
|
|
416
|
+
{/* styles.fonts - see resolveFonts()/googleFontsHref() in lib/config.ts.
|
|
417
|
+
Present (non-null) whenever at least one configured font (or the
|
|
418
|
+
Inter default, when styles.fonts is unset entirely) has no `source`
|
|
419
|
+
- i.e. is a Google Font, auto-loaded by family name. The two
|
|
420
|
+
preconnect hints warm up the connections googleapis.com's own CSS
|
|
421
|
+
response will immediately need (its @font-face rules point at
|
|
422
|
+
gstatic.com for the actual font files) before the browser has even
|
|
423
|
+
parsed that response - Google's own recommended pattern for using
|
|
424
|
+
their fonts. Rendered this early in <head> (right after favicon,
|
|
425
|
+
before Open Graph/Twitter meta) so the font starts loading as soon
|
|
426
|
+
as possible rather than competing with less render-critical tags
|
|
427
|
+
for bandwidth/priority. */}
|
|
428
|
+
{fontsGoogleHref && (
|
|
429
|
+
<>
|
|
430
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
431
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
432
|
+
<link rel="stylesheet" href={fontsGoogleHref} />
|
|
433
|
+
</>
|
|
434
|
+
)}
|
|
435
|
+
<!-- Open Graph -->
|
|
436
|
+
<meta property="og:title" content={title} />
|
|
437
|
+
{effectiveDescription && <meta property="og:description" content={effectiveDescription} />}
|
|
438
|
+
<meta property="og:type" content={ogType} />
|
|
439
|
+
<meta property="og:site_name" content={config.name} />
|
|
440
|
+
{canonicalUrl && <meta property="og:url" content={canonicalUrl} />}
|
|
441
|
+
{absoluteOgImage && <meta property="og:image" content={absoluteOgImage} />}
|
|
442
|
+
<!-- Twitter card -->
|
|
443
|
+
<meta name="twitter:card" content={twitterCard} />
|
|
444
|
+
<meta name="twitter:title" content={title} />
|
|
445
|
+
{effectiveDescription && <meta name="twitter:description" content={effectiveDescription} />}
|
|
446
|
+
{absoluteOgImage && <meta name="twitter:image" content={absoluteOgImage} />}
|
|
447
|
+
{/* writedocs.json's `integrations` (the six analytics providers) - see
|
|
448
|
+
AnalyticsScripts.astro. Rendered as early in <head> as reasonably
|
|
449
|
+
possible (right after the meta tags, before even the theme-
|
|
450
|
+
detection script below) - the usual placement every one of these
|
|
451
|
+
providers' own install instructions ask for, so pageview timing is
|
|
452
|
+
captured as close to actual page load as it can be. */}
|
|
453
|
+
<AnalyticsScripts config={config} />
|
|
454
|
+
<script is:inline>
|
|
455
|
+
// Sets [data-theme] and [data-banner-dismissed] on <html> before
|
|
456
|
+
// first paint, so the page never flashes the wrong mode or an
|
|
457
|
+
// already-dismissed banner. Preference order for theme: an explicit
|
|
458
|
+
// prior choice (localStorage), then the OS-level preference,
|
|
459
|
+
// defaulting to light. The click handlers that update both live in
|
|
460
|
+
// src/scripts/theme-toggle.ts and src/scripts/banner.ts.
|
|
461
|
+
//
|
|
462
|
+
// Also re-run on `astro:after-swap` (ClientRouter, see BaseLayout's
|
|
463
|
+
// own import of it below), not just the initial load - Astro's
|
|
464
|
+
// client-side navigation swaps <html>'s attributes wholesale to
|
|
465
|
+
// match the newly-fetched page's raw server-rendered markup (see
|
|
466
|
+
// swapRootAttributes() in astro/dist/transitions/swap-functions.js),
|
|
467
|
+
// which has neither of these dataset attributes baked in since
|
|
468
|
+
// they're pure client-side runtime state, not something the server
|
|
469
|
+
// ever renders. Without re-running this after every swap, a
|
|
470
|
+
// dismissed banner (or a chosen dark theme) would silently come
|
|
471
|
+
// back on the very next navigation - both attributes get wiped, not
|
|
472
|
+
// just reset to their default, so this has to unconditionally
|
|
473
|
+
// reapply them from localStorage every time, exactly as it does on
|
|
474
|
+
// first load. `astro:after-swap` fires synchronously right after
|
|
475
|
+
// that swap and before the browser's next paint, so this still
|
|
476
|
+
// prevents any visible flash on client-side navigations too.
|
|
477
|
+
function wdApplyStoredPreferences() {
|
|
478
|
+
try {
|
|
479
|
+
var stored = localStorage.getItem('wd-theme');
|
|
480
|
+
var theme = stored === 'light' || stored === 'dark'
|
|
481
|
+
? stored
|
|
482
|
+
: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
483
|
+
document.documentElement.dataset.theme = theme;
|
|
484
|
+
} catch (e) {
|
|
485
|
+
document.documentElement.dataset.theme = 'light';
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
document.documentElement.dataset.bannerDismissed = localStorage.getItem('wd-banner-dismissed') === 'true' ? 'true' : 'false';
|
|
489
|
+
} catch (e) {
|
|
490
|
+
document.documentElement.dataset.bannerDismissed = 'false';
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
wdApplyStoredPreferences();
|
|
494
|
+
document.addEventListener('astro:after-swap', wdApplyStoredPreferences);
|
|
495
|
+
</script>
|
|
496
|
+
<script>
|
|
497
|
+
// Preserves the sidebar's own scroll position across a ClientRouter
|
|
498
|
+
// navigation - see src/scripts/sidebar-scroll.ts for why this is
|
|
499
|
+
// needed at all (the sidebar is re-rendered fresh, server-side, on
|
|
500
|
+
// every page, so it's a brand-new DOM node each time, reset to
|
|
501
|
+
// scrollTop 0 like a real page reload would be). Same before-swap/
|
|
502
|
+
// after-swap pairing as wdApplyStoredPreferences above, and for the
|
|
503
|
+
// same reason: after-swap fires before the next paint, so there's no
|
|
504
|
+
// visible flash of the sidebar sitting at the top before jumping
|
|
505
|
+
// back to where the reader had it scrolled.
|
|
506
|
+
import { saveSidebarScroll, restoreSidebarScroll } from '../scripts/sidebar-scroll';
|
|
507
|
+
|
|
508
|
+
document.addEventListener('astro:before-swap', saveSidebarScroll);
|
|
509
|
+
document.addEventListener('astro:after-swap', restoreSidebarScroll);
|
|
510
|
+
</script>
|
|
511
|
+
<style
|
|
512
|
+
define:vars={{
|
|
513
|
+
wdPrimaryLight: lightPrimary,
|
|
514
|
+
wdBackgroundLight: lightBackground,
|
|
515
|
+
wdTextLight: lightText,
|
|
516
|
+
wdNavbarLight: lightNavbarBg,
|
|
517
|
+
wdNavbarFgLight: lightNavbarFg,
|
|
518
|
+
wdNavbarFgMutedLight: lightNavbarFgMuted,
|
|
519
|
+
wdNavbarAccentLight: lightNavbarAccent,
|
|
520
|
+
wdNavbarAccentTextLight: lightNavbarAccentText,
|
|
521
|
+
wdNavbarBorderLight: lightNavbarBorder,
|
|
522
|
+
wdPageBgColorLight: lightPageBgColor,
|
|
523
|
+
wdPageBgImageLight: lightPageBgImage,
|
|
524
|
+
wdFontFamily,
|
|
525
|
+
wdFontFamilyHeading,
|
|
526
|
+
wdFontFamilyBody,
|
|
527
|
+
wdPrimaryDark: darkPrimary,
|
|
528
|
+
wdBackgroundDark: darkBackground,
|
|
529
|
+
wdTextDark: darkText,
|
|
530
|
+
wdNavbarDark: darkNavbarBg,
|
|
531
|
+
wdNavbarFgDark: darkNavbarFg,
|
|
532
|
+
wdNavbarFgMutedDark: darkNavbarFgMuted,
|
|
533
|
+
wdNavbarAccentDark: darkNavbarAccent,
|
|
534
|
+
wdNavbarAccentTextDark: darkNavbarAccentText,
|
|
535
|
+
wdNavbarBorderDark: darkNavbarBorder,
|
|
536
|
+
wdPageBgColorDark: darkPageBgColor,
|
|
537
|
+
wdPageBgImageDark: darkPageBgImage,
|
|
538
|
+
}}
|
|
539
|
+
>
|
|
540
|
+
:root {
|
|
541
|
+
--wd-primary: var(--wdPrimaryLight);
|
|
542
|
+
--wd-background: var(--wdBackgroundLight);
|
|
543
|
+
--wd-text: var(--wdTextLight);
|
|
544
|
+
--wd-navbar-background: var(--wdNavbarLight);
|
|
545
|
+
--wd-navbar-foreground: var(--wdNavbarFgLight);
|
|
546
|
+
--wd-navbar-foreground-muted: var(--wdNavbarFgMutedLight);
|
|
547
|
+
--wd-navbar-accent: var(--wdNavbarAccentLight);
|
|
548
|
+
--wd-navbar-accent-text: var(--wdNavbarAccentTextLight);
|
|
549
|
+
--wd-navbar-border: var(--wdNavbarBorderLight);
|
|
550
|
+
--wd-page-bg-color: var(--wdPageBgColorLight);
|
|
551
|
+
--wd-page-bg-image: var(--wdPageBgImageLight);
|
|
552
|
+
--wd-text-muted: #64748b;
|
|
553
|
+
--wd-border: #e2e8f0;
|
|
554
|
+
--wd-surface: #f8fafc;
|
|
555
|
+
/* Not mode-dependent (a site's chosen font doesn't change between
|
|
556
|
+
light/dark) - set once here rather than duplicated in the
|
|
557
|
+
:root[data-theme='dark'] block below. */
|
|
558
|
+
--wd-font-family: var(--wdFontFamily);
|
|
559
|
+
--wd-font-family-heading: var(--wdFontFamilyHeading);
|
|
560
|
+
--wd-font-family-body: var(--wdFontFamilyBody);
|
|
561
|
+
}
|
|
562
|
+
/* Higher specificity than the plain :root rule above (an attribute
|
|
563
|
+
selector on :root beats an unqualified one), so this wins
|
|
564
|
+
regardless of source order once data-theme is set to "dark" by
|
|
565
|
+
the inline script above or the toggle button's click handler
|
|
566
|
+
(src/scripts/theme-toggle.ts). */
|
|
567
|
+
:root[data-theme='dark'] {
|
|
568
|
+
--wd-primary: var(--wdPrimaryDark);
|
|
569
|
+
--wd-background: var(--wdBackgroundDark);
|
|
570
|
+
--wd-text: var(--wdTextDark);
|
|
571
|
+
--wd-navbar-background: var(--wdNavbarDark);
|
|
572
|
+
--wd-navbar-foreground: var(--wdNavbarFgDark);
|
|
573
|
+
--wd-navbar-foreground-muted: var(--wdNavbarFgMutedDark);
|
|
574
|
+
--wd-navbar-accent: var(--wdNavbarAccentDark);
|
|
575
|
+
--wd-navbar-accent-text: var(--wdNavbarAccentTextDark);
|
|
576
|
+
--wd-navbar-border: var(--wdNavbarBorderDark);
|
|
577
|
+
--wd-page-bg-color: var(--wdPageBgColorDark);
|
|
578
|
+
--wd-page-bg-image: var(--wdPageBgImageDark);
|
|
579
|
+
--wd-text-muted: #94a3b8;
|
|
580
|
+
--wd-border: #1e293b;
|
|
581
|
+
--wd-surface: #131a2b;
|
|
582
|
+
}
|
|
583
|
+
</style>
|
|
584
|
+
{/* styles.fonts: local/externally-hosted font(s) (fontFaceRule() in
|
|
585
|
+
lib/config.ts - a local `source` path resolves the same way
|
|
586
|
+
styles.favicon/styles.logo/styles.background.images already do, see
|
|
587
|
+
collectConfiguredAssetPaths() + styles-asset-integration.js, no
|
|
588
|
+
extra wiring needed here beyond listing it there), plus a
|
|
589
|
+
font-weight override for h1-h6/body whenever `weight` was actually
|
|
590
|
+
configured somewhere (fontWeightCss above) - both concatenated
|
|
591
|
+
into one block since both need the same raw-CSS-text treatment.
|
|
592
|
+
set:html (not a plain <style>{fontExtraCss}</style>) because this
|
|
593
|
+
needs to render already-built CSS text as-is, not have Astro's own
|
|
594
|
+
CSS parser/scoper attempt to process an `@font-face` block or a
|
|
595
|
+
bare `h1, h2, ...` selector list assembled as a runtime string -
|
|
596
|
+
empty string renders nothing when neither piece has anything to
|
|
597
|
+
contribute (every configured font is a Google Font, and no
|
|
598
|
+
`weight` was set anywhere in `styles.fonts`). */}
|
|
599
|
+
{fontExtraCss && <style set:html={fontExtraCss} />}
|
|
600
|
+
{/* writedocs.json's `scripts.head` - see scriptEntrySchema in lib/config.ts.
|
|
601
|
+
Each entry is exactly one of an external/local `src` or inline
|
|
602
|
+
`content` (enforced by the schema's own .refine()), rendered right
|
|
603
|
+
before </head> so it runs before body content paints - the usual
|
|
604
|
+
spot a third-party script's own install instructions ask for. */}
|
|
605
|
+
{config.scripts.head.map((s) =>
|
|
606
|
+
s.src ? <script is:inline src={s.src} /> : <script is:inline set:html={s.content} />
|
|
607
|
+
)}
|
|
608
|
+
{/* Zero-config custom CSS - see findRootAssets() in lib/config.ts and
|
|
609
|
+
rootCssContents above. Rendered last in <head>, so a project-owned
|
|
610
|
+
file - anywhere in the project, not just the root - wins over
|
|
611
|
+
everything else writedocs generates itself. Inlined as raw <style>
|
|
612
|
+
content rather than a <link> - these files aren't necessarily under
|
|
613
|
+
`public/`, so there's no guaranteed build-time copy step making them
|
|
614
|
+
independently fetchable as a URL; inlining sidesteps needing one.
|
|
615
|
+
One <style> tag per file (not concatenated) so each is independently
|
|
616
|
+
visible/attributable
|
|
617
|
+
in devtools. */}
|
|
618
|
+
{rootCssContents.map((content) => (
|
|
619
|
+
<style is:inline set:html={content} />
|
|
620
|
+
))}
|
|
621
|
+
{/* Zero-config custom CSS living in public/ - see findRootAssets()'s
|
|
622
|
+
own comment in lib/config.ts for why these render as <link> tags
|
|
623
|
+
pointing at their already-public URL instead of joining
|
|
624
|
+
rootCssContents above as inlined content. */}
|
|
625
|
+
{publicCssHrefs.map((href) => (
|
|
626
|
+
<link rel="stylesheet" href={href} />
|
|
627
|
+
))}
|
|
628
|
+
</head>
|
|
629
|
+
<body>
|
|
630
|
+
{mode === 'blank' ? (
|
|
631
|
+
// No site chrome at all - no topbar, no sidebar/toc columns, not
|
|
632
|
+
// even the .wd-shell wrapper's own max-width - the page's own
|
|
633
|
+
// MDX/components control 100% of the layout, including whether
|
|
634
|
+
// there's any nav/branding at all. SearchModal.astro's own overlay
|
|
635
|
+
// markup and its ⌘K listener still work even with no visible
|
|
636
|
+
// trigger button to click (it's rendered unconditionally below,
|
|
637
|
+
// outside this mode === 'blank' branch), so that's left in rather
|
|
638
|
+
// than also gated on mode.
|
|
639
|
+
<slot />
|
|
640
|
+
) : (
|
|
641
|
+
<Fragment>
|
|
642
|
+
{/* Everything above the footer, in one wrapper - see .wd-page/body in
|
|
643
|
+
base.css: body is a flex column, this is its flex: 1 0 auto item,
|
|
644
|
+
and the footer (a plain sibling below, not inside this wrapper) is
|
|
645
|
+
what that pushes down to the viewport's bottom edge on a page
|
|
646
|
+
short enough not to need scrolling - the "sticky footer" pattern,
|
|
647
|
+
not position: sticky/fixed (either of which would cover page
|
|
648
|
+
content instead of just trailing it once actually reached). */}
|
|
649
|
+
<div class="wd-page">
|
|
650
|
+
{config.banner && (
|
|
651
|
+
<div class={`wd-banner wd-banner-${config.banner.type}`} id="wd-banner">
|
|
652
|
+
<div class="wd-banner-inner">
|
|
653
|
+
<span class="wd-banner-content">{config.banner.content}</span>
|
|
654
|
+
{config.banner.dismissible && (
|
|
655
|
+
<button type="button" class="wd-banner-dismiss" aria-label="Dismiss banner" data-banner-dismiss>
|
|
656
|
+
×
|
|
657
|
+
</button>
|
|
658
|
+
)}
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
)}
|
|
662
|
+
<TopBar
|
|
663
|
+
config={config}
|
|
664
|
+
selectors={selectors}
|
|
665
|
+
globalDropdowns={globalDropdowns}
|
|
666
|
+
showSidebarCol={showSidebarCol}
|
|
667
|
+
logoLight={logoLight}
|
|
668
|
+
logoDark={logoDark}
|
|
669
|
+
brandLabel={brandLabel}
|
|
670
|
+
/>
|
|
671
|
+
<MobileMenu
|
|
672
|
+
config={config}
|
|
673
|
+
selectors={selectors}
|
|
674
|
+
globalDropdowns={globalDropdowns}
|
|
675
|
+
showSidebarCol={showSidebarCol}
|
|
676
|
+
logoLight={logoLight}
|
|
677
|
+
logoDark={logoDark}
|
|
678
|
+
brandLabel={brandLabel}
|
|
679
|
+
>
|
|
680
|
+
<slot name="sidebar" slot="sidebar" />
|
|
681
|
+
</MobileMenu>
|
|
682
|
+
<div class="wd-shell">
|
|
683
|
+
{showSidebarCol && <div class="wd-sidebar-col"><slot name="sidebar" /></div>}
|
|
684
|
+
<div class="wd-main-col"><slot /></div>
|
|
685
|
+
{showTocCol && <div class="wd-toc-col"><slot name="toc" /></div>}
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
<SiteFooter config={config} logoLight={footerLogoLight} logoDark={footerLogoDark} />
|
|
689
|
+
</Fragment>
|
|
690
|
+
)}
|
|
691
|
+
|
|
692
|
+
<SearchModal />
|
|
693
|
+
<ImageZoom />
|
|
694
|
+
|
|
695
|
+
<script>
|
|
696
|
+
import { initBanner } from '../scripts/banner';
|
|
697
|
+
import { initTopbarOffset } from '../scripts/topbar-offset';
|
|
698
|
+
|
|
699
|
+
initBanner(document);
|
|
700
|
+
document.addEventListener('astro:page-load', () => initBanner(document));
|
|
701
|
+
|
|
702
|
+
// No ordering dependency on initBanner above - see topbar-offset.ts
|
|
703
|
+
// for why it measures only the topbar itself (not the banner) and
|
|
704
|
+
// why that's live-measured rather than a fixed CSS value.
|
|
705
|
+
initTopbarOffset();
|
|
706
|
+
document.addEventListener('astro:page-load', () => initTopbarOffset());
|
|
707
|
+
</script>
|
|
708
|
+
{/* writedocs.json's `scripts.body` - see scriptEntrySchema in lib/config.ts.
|
|
709
|
+
Rendered after everything above (including every component's own
|
|
710
|
+
<script> tags, which Astro/Vite already bundles to run in document
|
|
711
|
+
order) so it runs after every other script on the page has already
|
|
712
|
+
loaded/hydrated - matching where a third-party script's own install
|
|
713
|
+
instructions usually ask for a "body end" placement. */}
|
|
714
|
+
{config.scripts.body.map((s) =>
|
|
715
|
+
s.src ? <script is:inline src={s.src} /> : <script is:inline set:html={s.content} />
|
|
716
|
+
)}
|
|
717
|
+
{/* Zero-config custom JS - see findRootAssets() in lib/config.ts and
|
|
718
|
+
rootJsContents above. Rendered last, after writedocs.json's own
|
|
719
|
+
`scripts.body`, same "most locally-owned override runs last"
|
|
720
|
+
reasoning as the root CSS above. `is:inline` here isn't optional -
|
|
721
|
+
without it, Astro tries to process/bundle the tag as a module and
|
|
722
|
+
the build fails outright ("Cannot find the built path for ...
|
|
723
|
+
type=script&index=N") the same way a dynamic-`src` script did
|
|
724
|
+
while this file's own writedocs.json `scripts` support was first being
|
|
725
|
+
built - see that feature's own commit for the full story. */}
|
|
726
|
+
{rootJsContents.map((content) => (
|
|
727
|
+
<script is:inline set:html={content} />
|
|
728
|
+
))}
|
|
729
|
+
{/* Zero-config custom JS living in public/ - see this file's own CSS
|
|
730
|
+
equivalent above and findRootAssets()'s comment in lib/config.ts.
|
|
731
|
+
`<script src>`, not `is:inline set:html` - the file's already a
|
|
732
|
+
fetchable URL, no reason to inline its content. Filtered against
|
|
733
|
+
config.scripts.head's own `src` entries (publicJsHrefs) for the
|
|
734
|
+
same duplicate-tag reason as the CSS <link>s above - note this
|
|
735
|
+
only dedupes against `scripts.head`, not `scripts.body` (a public/
|
|
736
|
+
file explicitly listed there instead would still also render here
|
|
737
|
+
as a second tag; `scripts.head` vs `scripts.body` placement is a
|
|
738
|
+
deliberate choice this can't second-guess, but `head` is by far
|
|
739
|
+
the more common spot for a plain local script and this hasn't come
|
|
740
|
+
up in practice). */}
|
|
741
|
+
{publicJsHrefs.map((href) => (
|
|
742
|
+
<script is:inline src={href} />
|
|
743
|
+
))}
|
|
744
|
+
{/* writedocs.json's `integrations.askAi` - see AskAiWidget.astro. Rendered
|
|
745
|
+
last of all - it's a floating chat widget, not something anything
|
|
746
|
+
else on the page depends on running before, so there's no reason
|
|
747
|
+
to load it any earlier than absolutely last. */}
|
|
748
|
+
<AskAiWidget config={config} />
|
|
749
|
+
</body>
|
|
750
|
+
</html>
|