Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

54 linesCodeBlameRaw
1// @ts-check
2/**
3 * Pages that moved, and parts of pages that moved on their own.
4 *
5 * astro.config.mjs lists them as `'/old/': '/new/'`, and a part of a page
6 * as `'/old/#heading': '/new/#heading'`. Astro's redirects take the pages.
7 * A redirect never sees a link's #fragment, so the parts are this
8 * integration's: once the site is built, it puts a short script at the top
9 * of each old address's page (the redirect, or the page itself when it is
10 * still there) that sends a link to a moved part on to where it went. On a
11 * redirect, a link to any other part keeps its #fragment, since the parts
12 * of a moved page keep their names.
13 */
14import { readFile, writeFile } from 'node:fs/promises';
15// readFile throws for an old address with no page, so a moved part never
16// names a page the build does not make.
17import { fileURLToPath } from 'node:url';
18
19/** @param {Record<string, string>} moved */
20export function pages(moved) {
21 return Object.fromEntries(Object.entries(moved).filter(([from]) => !from.includes('#')));
22}
23
24/** @param {Record<string, string>} moved @returns {import('astro').AstroIntegration} */
25export function movedParts(moved) {
26 return {
27 name: 'g1t-moved-parts',
28 hooks: {
29 'astro:build:done': async ({ dir, logger }) => {
30 /** @type {Map<string, Record<string, string>>} */
31 const parts = new Map();
32 for (const [from, to] of Object.entries(moved)) {
33 const [page, part] = from.split('#');
34 if (!part) continue;
35 parts.set(page, { ...parts.get(page), [part]: to });
36 }
37 for (const [page, map] of parts) {
38 const file = fileURLToPath(new URL(`.${page}index.html`, dir));
39 const html = await readFile(file, 'utf8');
40 const whole = moved[page] ?? '';
41 const script =
42 `<script>(function(){var p=${JSON.stringify(map)},w=${JSON.stringify(whole)},h=location.hash,` +
43 `t=p[decodeURIComponent(h.slice(1))]||(w&&w+h);if(t)location.replace(t)})()</script>`;
44 // First in the page, before a redirect's own refresh. Astro's
45 // redirect pages have no <head> tag, Starlight's pages do.
46 const at = /<head[^>]*>/i.exec(html) ?? /<!doctype html>/i.exec(html);
47 const end = at ? at.index + at[0].length : 0;
48 await writeFile(file, html.slice(0, end) + script + html.slice(end));
49 }
50 logger.info(`${parts.size} pages send links to their moved parts on`);
51 },
52 },
53 };
54}