Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions observablehq.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ export default {
{name: "Contributing", path: "/contributing", pager: false}
],
base: "/framework",
head: `<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Spline+Sans+Mono:ital,wght@0,300..700;1,300..700&display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Spline+Sans+Mono:ital,wght@0,300..700;1,300..700&display=swap" crossorigin>
<link rel="apple-touch-icon" href="/observable.png">
globalStylesheets: [
"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Spline+Sans+Mono:ital,wght@0,300..700;1,300..700&display=swap"
],
head: `<link rel="apple-touch-icon" href="/observable.png">
<link rel="icon" type="image/png" href="/observable.png" sizes="32x32">${
process.env.CI
? `
Expand Down
4 changes: 2 additions & 2 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function build(
{config}: BuildOptions,
effects: BuildEffects = new FileBuildEffects(config.output, join(config.root, ".observablehq", "cache"))
): Promise<void> {
const {root, loaders, normalizePath} = config;
const {root, loaders} = config;
Telemetry.record({event: "build", step: "start"});

// Make sure all files are readable before starting to write output files.
Expand Down Expand Up @@ -79,7 +79,7 @@ export async function build(
effects.logger.log(faint("(skipped)"));
continue;
}
const resolvers = await getResolvers(page, {root, path: sourceFile, normalizePath, loaders});
const resolvers = await getResolvers(page, {path: sourceFile, ...config});
const elapsed = Math.floor(performance.now() - start);
for (const f of resolvers.assets) files.add(resolvePath(sourceFile, f));
for (const f of resolvers.files) files.add(resolvePath(sourceFile, f));
Expand Down
13 changes: 13 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interface Config {
footer: PageFragmentFunction | string | null; // defaults to “Built with Observable on [date].”
toc: TableOfContents;
style: null | Style; // defaults to {theme: ["light", "dark"]}
globalStylesheets: string[]; // defaults to Source Serif from Google Fonts
search: SearchConfig | null; // default to null
md: MarkdownIt;
normalizePath: (path: string) => string;
Expand All @@ -99,6 +100,7 @@ export interface ConfigSpec {
base?: unknown;
sidebar?: unknown;
style?: unknown;
globalStylesheets?: unknown;
theme?: unknown;
search?: unknown;
scripts?: unknown;
Expand Down Expand Up @@ -224,6 +226,10 @@ export function normalizeConfig(spec: ConfigSpec = {}, defaultRoot?: string, wat
: spec.style !== undefined
? {path: String(spec.style)}
: {theme: normalizeTheme(spec.theme === undefined ? "default" : spec.theme)};
const globalStylesheets =
spec.globalStylesheets === undefined
? defaultGlobalStylesheets()
: Array.from(spec.globalStylesheets as any, String);
const md = createMarkdownIt({
linkify: spec.linkify === undefined ? undefined : Boolean(spec.linkify),
typographer: spec.typographer === undefined ? undefined : Boolean(spec.typographer),
Expand Down Expand Up @@ -255,6 +261,7 @@ export function normalizeConfig(spec: ConfigSpec = {}, defaultRoot?: string, wat
footer,
toc,
style,
globalStylesheets,
search,
md,
normalizePath: getPathNormalizer(spec.cleanUrls),
Expand Down Expand Up @@ -282,6 +289,12 @@ function pageFragment(spec: unknown): PageFragmentFunction | string | null {
return typeof spec === "function" ? (spec as PageFragmentFunction) : stringOrNull(spec);
}

function defaultGlobalStylesheets(): string[] {
return [
"https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap"
];
}

function defaultFooter(): string {
const date = currentDate ?? new Date();
return `Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="${formatIsoDate(
Expand Down
4 changes: 2 additions & 2 deletions src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, configPromise: Pro

async function watcher(event: WatchEventType, force = false) {
if (!path || !config) throw new Error("not initialized");
const {root, loaders, normalizePath} = config;
const {root, loaders} = config;
switch (event) {
case "rename": {
markdownWatcher?.close();
Expand Down Expand Up @@ -336,7 +336,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, configPromise: Pro
clearTimeout(emptyTimeout);
emptyTimeout = null;
}
const resolvers = await getResolvers(page, {root, path, loaders, normalizePath});
const resolvers = await getResolvers(page, {path, ...config});
if (hash === resolvers.hash) break;
const previousHash = hash!;
const previousHtml = html!;
Expand Down
9 changes: 8 additions & 1 deletion src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ function renderListItem(page: Page, path: string, resolveLink: (href: string) =>

function renderHead(head: MarkdownPage["head"], resolvers: Resolvers): Html {
const {stylesheets, staticImports, resolveImport, resolveStylesheet} = resolvers;
return html`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>${
return html`${
hasGoogleFonts(stylesheets) ? html`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>` : null
}${
Array.from(new Set(Array.from(stylesheets, resolveStylesheet)), renderStylesheetPreload) // <link rel=preload as=style>
}${
Array.from(new Set(Array.from(stylesheets, resolveStylesheet)), renderStylesheet) // <link rel=stylesheet>
Expand Down Expand Up @@ -266,3 +268,8 @@ function renderPager({prev, next}: PageLink, resolveLink: (href: string) => stri
function renderRel(page: Page, rel: "prev" | "next", resolveLink: (href: string) => string): Html {
return html`<a rel="${rel}" href="${encodeURI(resolveLink(page.path))}"><span>${page.name}</span></a>`;
}

function hasGoogleFonts(stylesheets: Set<string>): boolean {
for (const s of stylesheets) if (s.startsWith("https://fonts.googleapis.com/")) return true;
return false;
}
9 changes: 4 additions & 5 deletions src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ResolversConfig {
root: string;
path: string;
normalizePath: (path: string) => string;
globalStylesheets?: string[];
loaders: LoaderResolver;
}

Expand Down Expand Up @@ -83,7 +84,7 @@ export const builtins = new Map<string, string>([
*/
export async function getResolvers(
page: MarkdownPage,
{root, path, normalizePath, loaders}: ResolversConfig
{root, path, normalizePath, globalStylesheets: defaultStylesheets, loaders}: ResolversConfig
): Promise<Resolvers> {
const hash = createHash("sha256").update(page.body).update(JSON.stringify(page.data));
const assets = new Set<string>();
Expand All @@ -92,7 +93,7 @@ export async function getResolvers(
const localImports = new Set<string>();
const globalImports = new Set<string>(defaultImports);
const staticImports = new Set<string>(defaultImports);
const stylesheets = new Set<string>();
const stylesheets = new Set<string>(defaultStylesheets);
const resolutions = new Map<string, string>();

// Add assets.
Expand All @@ -105,9 +106,7 @@ export async function getResolvers(
for (const i of info.staticImports) staticImports.add(i);
}

// Add stylesheets. TODO Instead of hard-coding Source Serif Pro, parse the
// page’s stylesheet to look for external imports.
stylesheets.add("https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap"); // prettier-ignore
// Add stylesheets.
if (page.style) stylesheets.add(page.style);

// Collect directly-attached files, local imports, and static imports.
Expand Down
3 changes: 1 addition & 2 deletions src/style/global.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
:root {
--monospace: Menlo, Consolas, monospace;
--monospace-font: 14px/1.5 var(--monospace);
--serif: "Source Serif Pro", "Iowan Old Style", "Apple Garamond", "Palatino Linotype", "Times New Roman",
"Droid Serif", Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--serif: "Source Serif 4", serif;
--sans-serif: -apple-system, BlinkMacSystemFont, "avenir next", avenir, helvetica, "helvetica neue", ubuntu, roboto,
noto, "segoe ui", arial, sans-serif;
--theme-blue: #4269d0;
Expand Down
6 changes: 6 additions & 0 deletions test/config-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ describe("readConfig(undefined, root)", () => {
output: "dist",
base: "/",
style: {theme: ["air", "near-midnight"]},
globalStylesheets: [
"https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap"
],
sidebar: true,
pages: [
{path: "/index", name: "Index", pager: "main"},
Expand Down Expand Up @@ -52,6 +55,9 @@ describe("readConfig(undefined, root)", () => {
output: "dist",
base: "/",
style: {theme: ["air", "near-midnight"]},
globalStylesheets: [
"https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap"
],
sidebar: true,
pages: [{name: "Build test case", path: "/simple", pager: "main"}],
title: undefined,
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/404/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Page not found</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/archives.posix/tar.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Tar</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/archives.posix/zip.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Zip</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/archives.win32/tar.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Tar</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/archives.win32/zip.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Zip</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/config/closed/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>A page…</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="../_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="../_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="../_observablehq/client.00000001.js">
<link rel="modulepreload" href="../_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/config/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Index</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/config/one.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>One</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/config/sub/two.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Two</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="../_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="../_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="../_observablehq/client.00000001.js">
<link rel="modulepreload" href="../_observablehq/runtime.00000002.js">
Expand Down
Loading