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
226 changes: 226 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion website/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ export default defineConfig({
build: { format: 'file' },
integrations: [react()],
markdown: {
// The same themes as lib/highlight.ts.
shikiConfig: {
themes: { light: 'github-light', dark: 'github-dark-dimmed' },
themes: { light: 'github-light-high-contrast', dark: 'github-dark-dimmed' },
defaultColor: false,
},
},
Expand Down
38 changes: 38 additions & 0 deletions website/netlify/edge-functions/canonical-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// One 301 to the canonical URL for the variants static hosting would otherwise serve as copies:
// a trailing slash (/zh/, /icons/menu/), the .html file (/icons/menu.html) and old or misspelled
// paths (/docs, /licensing). A rule in _redirects can't do this: Netlify matches its rules with and
// without the trailing slash alike, so /documentation/ -> /documentation would loop.
type Context = { next: () => Promise<Response> };

const ALIASES: Record<string, string> = {
'/docs': '/documentation',
'/license': '/licencing-and-terms',
'/licence': '/licencing-and-terms',
'/licensing': '/licencing-and-terms',
'/licensing-and-terms': '/licencing-and-terms',
};

/** The canonical path of a pathname, e.g. /icons/menu.html/ -> /icons/menu. */
const canonicalPath = (pathname: string) => {
let path = pathname.replace(/\/+$/, '') || '/';
if (path.endsWith('.html') && path !== '/404.html') path = path.slice(0, -'.html'.length);
path = path.replace(/\/index$/, '') || '/';
return ALIASES[path] ?? path;
};

export default async (request: Request, context: Context) => {
const url = new URL(request.url);
const path = canonicalPath(url.pathname);
if (path === url.pathname) return context.next();
return new Response(null, {
status: 301,
headers: { Location: `${path}${url.search}`, 'Cache-Control': 'public, max-age=3600' },
});
};

// Paths with a trailing slash, .html pages and the aliases; files (.json, .zip, .svg, .md, .png,
// .txt, .xml) never match, and /404.html is served as it is.
export const config = {
pattern: ['^/.+/$', '^/.+\\.html$', '^/(docs|license|licence|licensing|licensing-and-terms)$'],
excludedPattern: ['^/404\\.html$', '^/\\.netlify/'],
};
51 changes: 33 additions & 18 deletions website/netlify/edge-functions/markdown-negotiation.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
// Serves the Markdown version of a page to clients that ask for it (Accept: text/markdown),
// such as AI agents. Everyone else gets the HTML page.
// such as AI agents. Everyone else gets the HTML page, which announces the Markdown version in a
// Link header. Both vary by Accept, so caches keep them apart.
type Context = { next: () => Promise<Response> };

const MARKDOWN: Record<string, string> = {
'/': '/index.md',
'/documentation': '/documentation.md',
'/documentation/': '/documentation.md',
};
/** The Markdown file of a page: / -> /index.md, /icons/menu -> /icons/menu.md. */
const markdownPath = (pathname: string) => (pathname === '/' ? '/index.md' : `${pathname}.md`);

export default async (request: Request, context: Context) => {
const markdown = MARKDOWN[new URL(request.url).pathname];
if (!markdown || !request.headers.get('accept')?.includes('text/markdown')) return context.next();
const response = await fetch(new URL(markdown, request.url));
if (!response.ok) return context.next();
return new Response(await response.text(), {
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
Vary: 'Accept',
},
});
const markdown = markdownPath(new URL(request.url).pathname);
if (request.headers.get('accept')?.includes('text/markdown')) {
const response = await fetch(new URL(markdown, request.url));
if (response.ok) {
return new Response(await response.text(), {
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
Vary: 'Accept',
},
});
}
}
const response = await context.next();
// Only pages that exist have a Markdown version; a 404 stays as it is.
if (!response.ok) return response;
const headers = new Headers(response.headers);
headers.append('Link', `<${markdown}>; rel="alternate"; type="text/markdown"`);
headers.append('Vary', 'Accept');
return new Response(response.body, { status: response.status, headers });
};

export const config = { path: ['/', '/documentation', '/documentation/'] };
// Exact pages only, so files such as /icons/menu.json, .zip or .svg never run this function.
export const config = {
pattern: [
'^/$',
'^/(documentation|licencing-and-terms)$',
'^/icons/[a-z0-9-]+$',
'^/categories/[a-z0-9-]+$',
],
};
2 changes: 2 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"@astrojs/react": "^7.0.1",
"@fontsource-variable/geist": "^5.3.0",
"@fontsource-variable/geist-mono": "^5.3.0",
"@fontsource/geist": "^5.3.0",
"@resvg/resvg-js": "^2.6.2",
"@tailwindcss/vite": "^4.3.3",
"astro": "^7.3.8",
"fflate": "^0.8.3",
Expand Down
Loading
Loading