Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions docs/specs/website-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ Source of truth: `buildDocument` in `website/scripts/generate-docs.js`; `Markdow

**Every in-site link spells that served path** — `sitePath` in components, the generator's rewrites in reference prose — so no reader lands on a redirect. Exempt: `/` with its anchors, and the `/docs` entrypoint, which names no page. `checkInSiteHrefsAreServed` pins it.

## Website fonts

**Must serve website fonts from dormouse.sh.** **Must defer initial page content until its fonts finish loading, with a bounded fallback for unavailable fonts and readable pages without JavaScript.** This applies across the website, including prerendered routes and the SPA fallback.

Source of truth: `Layout` in `website/src/root.tsx`.

## Reference page chrome

**Each page's `linkedFrom` names every document owing it a link** — the two READMEs and the homepage — so the obligation is registry-driven, never inferred from the path. `checkRoutesToReferences` reads it.
Expand Down
51 changes: 16 additions & 35 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/spec-word-budgets.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@
"docs/specs/tutorial.md": 2000,
"docs/specs/vscode.md": 4450,
"docs/specs/webgl-text.md": 800,
"docs/specs/website-docs.md": 2850
"docs/specs/website-docs.md": 2900
}
2 changes: 2 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"test": "pnpm typecheck && vitest run"
},
"dependencies": {
"@fontsource/ubuntu-mono": "^5.3.0",
"@fontsource/ubuntu-sans-mono": "^5.3.0",
"@phosphor-icons/react": "^2.1.10",
"ascii-splash": "0.6.1",
"dormouse-lib": "workspace:*",
Expand Down
5 changes: 5 additions & 0 deletions website/src/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
@import "tailwindcss";
/* Served from dormouse.sh, not Google Fonts, so a visit makes no third-party
request. Regular weight only, as the Google Fonts request was; each file
carries unicode-range subsets, so a page fetches only the scripts it uses. */
@import "@fontsource/ubuntu-mono/400.css";
@import "@fontsource/ubuntu-sans-mono/400.css";

/* Library components compile from source here (the `dormouse-lib` alias in
vite.config.ts), so Tailwind must scan them or their utilities are never
Expand Down
79 changes: 79 additions & 0 deletions website/src/lib/font-ready.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { runInNewContext } from "node:vm";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FONT_READY_SCRIPT } from "./font-ready";

function boot(fontsAvailable = true) {
vi.useFakeTimers();
const attributes = new Map<string, string>();
let resolve!: () => void;
let reject!: (error: Error) => void;
const promise = new Promise<void>((done, fail) => { resolve = done; reject = fail; });
const ready = { promise, resolve, reject };
const onReady = vi.fn();
const layout = vi.fn();
const document = {
documentElement: {
setAttribute: (key: string, value: string) => attributes.set(key, value),
removeAttribute: (key: string) => attributes.delete(key),
},
body: { get offsetHeight() { layout(); return 100; } },
fonts: fontsAvailable ? {
get ready() {
onReady();
expect(layout).toHaveBeenCalledOnce();
return ready.promise;
},
} : undefined,
addEventListener: vi.fn(),
};
runInNewContext(FONT_READY_SCRIPT, { document, setTimeout, clearTimeout });
return {
hidden: () => attributes.has("data-fonts-pending"),
parsed: () => document.addEventListener.mock.calls[0][1](),
ready,
onReady,
};
}

afterEach(() => vi.useRealTimers());

describe("initial font paint", () => {
it("hides before parsing and reveals only after the page's font layout is ready", async () => {
const page = boot();
expect(page.hidden()).toBe(true);
expect(page.onReady).not.toHaveBeenCalled();
page.parsed();
expect(page.hidden()).toBe(true);
page.ready.resolve();
await page.ready.promise;
expect(page.hidden()).toBe(false);
expect(vi.getTimerCount()).toBe(0);
});

it("reveals when fonts fail", async () => {
const page = boot();
page.parsed();
page.ready.reject(new Error("font unavailable"));
await Promise.resolve();
expect(page.hidden()).toBe(false);
expect(vi.getTimerCount()).toBe(0);
});

it.each([false, true])("bounds the wait even if parsing completed: %s", async (parsed) => {
const page = boot();
if (parsed) page.parsed();
vi.advanceTimersByTime(3000);
expect(page.hidden()).toBe(false);
// Late completion must not hide the page again.
if (!parsed) page.parsed();
page.ready.resolve();
await page.ready.promise;
expect(page.hidden()).toBe(false);
});

it("leaves content visible without the Font Loading API", () => {
const page = boot(false);
expect(page.hidden()).toBe(false);
expect(vi.getTimerCount()).toBe(0);
});
});
24 changes: 24 additions & 0 deletions website/src/lib/font-ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Runs inline in the shared head, before prerendered content can paint; waiting
* in entry.client would let fallback text appear while the JS bundle downloads.
* Only JS-enabled visits get the attribute, so static/no-JS pages stay readable.
* Keep this independent of the application bundle and hydration.
*/
export const FONT_READY_STYLE = 'html[data-fonts-pending] body { visibility: hidden; }';
export const FONT_READY_SCRIPT = `(() => {
if (!document.fonts) return;
const root = document.documentElement;
const reveal = () => {
root.removeAttribute('data-fonts-pending');
clearTimeout(timeout);
};
// A failed or stalled font request must never leave the site blank.
const timeout = setTimeout(reveal, 3000);
root.setAttribute('data-fonts-pending', '');
document.addEventListener('DOMContentLoaded', () => {
// Discover the fonts used by the parsed page before reading ready. Hidden
// content still participates in layout and requests its font subsets.
void document.body.offsetHeight;
document.fonts.ready.then(reveal, reveal);
}, { once: true });
})();`;
2 changes: 1 addition & 1 deletion website/src/pages/Privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const PRIVACY_SECTIONS: PolicySection[] = [
<p>DiffPlug LLC operates Dormouse Hosted and is the controller of the personal information described here: the account service at hosted.dormouse.sh, the managed Relay and Pocket at relay.dormouse.sh, and managed voices at voice.dormouse.sh. This policy covers those services, the paid subscription that unlocks them, and related support. Contact us at <a className={LINK_CLASS} href="mailto:support@dormouse.sh">support@dormouse.sh</a> about privacy or your account.</p>
<p>Our business address is DiffPlug LLC, 447 Sutter St Ste 405, San Francisco, CA 94108, United States.</p>
<p>Dormouse itself runs on your computer and sends no usage telemetry. An update check fetches a version file from dormouse.sh without sending terminal contents or a Hosted account identifier. Automatic update checks run only if enabled. "Check now" makes a request when you click it. Creating an account does not upload your terminal contents, commands, files, or audio.</p>
<p>This website, dormouse.sh, is served by Cloudflare, which processes visitors’ IP addresses and request information to deliver it. The site currently loads fonts from Google Fonts, so Google receives your IP address and browser information when you visit.</p>
<p>This website, dormouse.sh, is served by Cloudflare, which processes visitors’ IP addresses and request information to deliver it.</p>
<p>This policy does not describe a Relay you operate yourself or third-party services you choose to open. Those services have their own operators and policies.</p>
<p>Hosted is not directed to children under 13, and we do not knowingly collect their personal information. If you believe a child under 13 has given us personal information, contact us and we will delete it.</p>
</> },
Expand Down
13 changes: 9 additions & 4 deletions website/src/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
type MetaArgs,
} from "react-router";
import { siteMeta } from "./lib/site-meta";
import { FONT_READY_SCRIPT, FONT_READY_STYLE } from "./lib/font-ready";
import bodyFont from "@fontsource/ubuntu-mono/files/ubuntu-mono-latin-400-normal.woff2?url";
import displayFont from "@fontsource/ubuntu-sans-mono/files/ubuntu-sans-mono-latin-400-normal.woff2?url";

/**
* Every page's title, description, canonical, and social cards.
Expand All @@ -22,8 +25,9 @@ export function meta({ location }: MetaArgs) {
}

export function Layout({ children }: { children: ReactNode }) {
// The inline font gate can still own an html attribute when hydration starts.
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand All @@ -32,11 +36,12 @@ export function Layout({ children }: { children: ReactNode }) {
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Ubuntu+Sans+Mono&display=swap" rel="stylesheet" />
<link rel="preload" href={bodyFont} as="font" type="font/woff2" crossOrigin="anonymous" />
<link rel="preload" href={displayFont} as="font" type="font/woff2" crossOrigin="anonymous" />
<Meta />
<Links />
<style>{FONT_READY_STYLE}</style>
<script dangerouslySetInnerHTML={{ __html: FONT_READY_SCRIPT }} />
</head>
<body>
{children}
Expand Down
Loading