| 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 | */ |
| 14 | import { 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. |
| 17 | import { fileURLToPath } from 'node:url'; |
| 18 | |
| 19 | /** @param {Record<string, string>} moved */ |
| 20 | export 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} */ |
| 25 | export 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 | } |