From 99089a840437b4793be15c8ea2d9ee348794235e Mon Sep 17 00:00:00 2001 From: Iain Date: Fri, 28 Aug 2026 17:40:05 +1000 Subject: [PATCH] #8: Prepare link42.app for GitHub Pages --- .github/workflows/pages.yml | 71 +++++ PLAN.md | 14 +- README.md | 16 +- package.json | 2 +- playwright.config.ts | 6 +- pnpm-lock.yaml | 430 +---------------------------- provenance/import-receipts.json | 4 +- provenance/public-tree-policy.json | 7 +- scripts/check-routes.mjs | 201 ++++++++------ src/routes/+layout.ts | 2 + src/routes/blog/rss.xml/+server.ts | 3 + src/routes/learn/+layout.svelte | 12 +- static/.nojekyll | 0 svelte.config.js | 38 ++- tests/e2e/blog.spec.ts | 2 +- tests/e2e/shell.spec.ts | 2 +- tests/pages-workflow.test.mjs | 78 ++++++ tests/static-pages.test.mjs | 57 ++++ 18 files changed, 411 insertions(+), 534 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 src/routes/+layout.ts create mode 100644 static/.nojekyll create mode 100644 tests/pages-workflow.test.mjs create mode 100644 tests/static-pages.test.mjs diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..ed22179 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,71 @@ +name: GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Pages / Build + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Check out the independent repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + with: + persist-credentials: false + + - name: Set up pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + run_install: false + + - name: Set up Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 + with: + node-version-file: .node-version + cache: pnpm + + - name: Configure GitHub Pages + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b + + - name: Install dependencies from the lockfile + run: pnpm install --frozen-lockfile + + - name: Build the static site + run: pnpm build + + - name: Verify the static artifact + run: pnpm check:routes + + - name: Upload the static site artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 + with: + path: build + + deploy: + name: Pages / Deploy + needs: build + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy GitHub Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 diff --git a/PLAN.md b/PLAN.md index 9921371..081242a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -10,7 +10,7 @@ Repository visibility: Public - **Purpose:** Publish and maintain the public Link42 company website independently of the private platform monorepo. - **Audience:** Security practitioners, learners, customers, contributors, and the public. - **Stack:** TypeScript, Svelte 5, SvelteKit, pnpm, Node 22, Vite, Vitest, and Biome. -- **Runtime target:** SvelteKit `adapter-node`; hosting and production cutover are outside this plan. +- **Runtime target:** SvelteKit `adapter-static` output for GitHub Pages at the `link42.app` domain root; repository Pages settings, custom-domain APIs, DNS, and production cutover remain separate. - **Content:** Git-backed Markdown for Learn and Blog. - **Software licence:** AGPL-3.0-only. - **Original editorial content licence:** CC BY-NC 4.0. @@ -102,7 +102,7 @@ All package manifests, lockfiles, configuration, documentation, licences, CI, an - Move the required footer, theme, and public navigation into website-owned source; remove `@link42/ui`. - Remove `@link42/auth-client`, `src/lib/api.ts`, auth locals, session resolution, sign-out logic, and auth environment variables. - Remove Cytoscape and every dependency used only by investigations. -- Retain only Svelte, SvelteKit, `adapter-node`, and Marked as runtime dependencies. +- Retain only Svelte, SvelteKit, `adapter-static`, and Marked as runtime dependencies. - Retain Vite, TypeScript, Vitest, Biome, and the Svelte Vite plugin for development. - Add Playwright and Axe coverage only after plan and dependency approval. - Replace the Google Fonts runtime import with either locally vendored, provenance-recorded Geist assets under their original licence or an approved system-font fallback. @@ -119,6 +119,7 @@ All package manifests, lockfiles, configuration, documentation, licences, CI, an | 5 | Public company pages | Port home, about, changelog, and licence after editorial and public-safety review; remove or neutralise links to excluded API, reports, and investigations routes and publish the selected licensing policy accurately. | 3, 4 | done | | 6 | Verification and private CI | Add one-command verification, route and link tests, Playwright and Axe checks, secret and privacy scans, dependency audit, deterministic generation checks, and hosted CI in the private repository. | 3, 4, 5 | done | | 7 | Public release | Publish the independent repository only after private hosted CI succeeds; configure public default-branch protections and security features; verify a credential-free fresh clone, full history, tree, receipts, routes, browsers, accessibility, and final public hosted CI. Hosting and production remain excluded. | 6 | done | +| 8 | GitHub Pages and domain preparation | Produce strict static output for every included route and asset, add a pinned least-privilege Pages workflow for protected `main`, and prepare domain-root metadata for `link42.app`. Repository Pages settings, custom-domain APIs, DNS, DigitalOcean, and production verification remain excluded. | 7 | done | One completed feature equals one tested commit and push. Do not start the next feature with uncommitted work. @@ -136,6 +137,8 @@ One completed feature equals one tested commit and push. Do not start the next f - `pnpm audit --prod --audit-level=high` has no high or critical finding. - Secret scans pass on the repository tree and complete independent history. - Hosted CI must complete successfully; local results cannot substitute for an unavailable hosted run. +- The static artifact contains Home, About, Changelog, Licence, all 15 Learn endpoints, Blog index and article, RSS, 404 handling, local fonts, and all referenced assets at domain-root paths. +- The Pages workflow builds on protected `main` or manual dispatch only, deploys no pull request, uses pinned actions, and separates read-only build permission from the Pages and OIDC deploy permissions. ## Commands @@ -155,8 +158,8 @@ pnpm verify - The private monorepo remains in place and private. - The website repository is public after completing Feature 7's explicit publication gates. -- No public deployment specification, credential, private app identifier, or DNS mutation script is copied. -- Hosting, DNS, staging, and production cutover are explicitly excluded from this plan. +- No credential, private app identifier, or DNS mutation script is copied. +- GitHub Pages source preparation is included in Feature 8; repository settings, custom-domain APIs, DNS, DigitalOcean, staging, and production cutover are explicitly excluded. - Keep website links pointed at the durable `https://rule1.link42.app` product URL, not either GitHub Pages owner URL. ## Known Risks @@ -166,7 +169,7 @@ pnpm verify - The current changelog links to `/reports/**` and `/api`; those links would be broken or imply deferred functionality is present. - The current About page publishes infrastructure, costs, private-repository references, and broad authorship claims. It requires an editorial accuracy review. - Code, editorial text, brand assets, and third-party content require explicit file-level licensing boundaries; a root AGPL licence alone must not imply a trademark grant for logos. -- A future deployment must be separately planned and must not expose private platform components or configuration. +- Enabling Pages, attaching the custom domain, changing DNS, and verifying production require a separate controlled step after Feature 8 succeeds. ## Discovered Dependencies @@ -176,3 +179,4 @@ pnpm verify | D2 | Approve Playwright and Axe development dependencies. **Decision:** approved for the verification feature. | 6 | resolved | | D3 | Choose locally vendored Geist fonts or the system-font fallback. **Decision:** locally vendored Geist assets with provenance, checksums, and their original licence. | 2 | resolved | | D4 | Permit literal SvelteKit bracketed route directories in concrete provenance paths without permitting `*` or `?` wildcards. | 4 | resolved | +| D5 | Replace `adapter-node` with `adapter-static` and add immutable official GitHub Pages actions. **Decision:** approved for Feature 8 with exact action SHAs and least-privilege jobs. | 8 | resolved | diff --git a/README.md b/README.md index 8af4862..9ed252e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository is the independent, public source for the Link42 company website at [link42.app](https://link42.app). The website is being separated from Link42's private platform monorepo so that its public source, content provenance, licensing boundaries, and release history can be reviewed without exposing private services or operational configuration. -The repository contains the public-only website shell and the publication safeguards used to keep it independent of the private platform. Content is being added in small, tested features recorded in [PLAN.md](PLAN.md). Until the public release and production cutover gates are complete, this repository is source-in-progress rather than the canonical production deployment. +The repository contains the public-only website shell and the publication safeguards used to keep it independent of the private platform. Work is recorded in small, tested features in [PLAN.md](PLAN.md). The repository is public and produces a GitHub Pages-compatible artifact, but it is not the canonical production deployment until the separately controlled Pages, custom-domain, DNS, and live-verification gates are complete. ## Development @@ -28,10 +28,20 @@ history for secrets, formatting, types, unit tests, the production build, every included and excluded route, internal links, desktop/mobile accessibility in light and dark themes, and high-severity production dependency advisories. -Route and internal-link verification starts the built website only on loopback. -It validates external HTTPS link syntax but deliberately does not fetch external +Route and internal-link verification inspects the static `build/` artifact +directly. Browser and accessibility checks serve that artifact only on loopback. +They validate external HTTPS link syntax but deliberately do not fetch external URLs. CI uses the same command with read-only repository access and no secrets. +## GitHub Pages artifact + +`pnpm build` writes the complete domain-root site to `build/`. The pinned Pages +workflow builds and verifies that directory on protected `main` pushes or an +explicit manual run, then grants only its deploy job Pages and OIDC write access. +The source intentionally contains no `CNAME`; Pages enablement, the `link42.app` +custom-domain setting, `www.link42.app` redirect DNS, and production verification +are separate operational steps. + The source manifest is pinned to a single private-source commit. A path not explicitly listed in that manifest is rejected by default. See [provenance/SOURCE_PROVENANCE.md](provenance/SOURCE_PROVENANCE.md) before importing any material. ## Publication boundary diff --git a/package.json b/package.json index 5b7c04d..a71093f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "verify": "pnpm check && pnpm check:gitleaks && pnpm test && pnpm build && pnpm check:routes && pnpm test:e2e && pnpm audit --prod --audit-level=high" }, "dependencies": { - "@sveltejs/adapter-node": "5.5.7", + "@sveltejs/adapter-static": "3.0.10", "@sveltejs/kit": "2.70.3", "marked": "18.0.11", "svelte": "5.56.10" diff --git a/playwright.config.ts b/playwright.config.ts index b2463f0..a5cdd35 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,12 +6,12 @@ export default defineConfig({ retries: 0, reporter: "line", use: { - baseURL: "http://127.0.0.1:4173", + baseURL: "http://127.0.0.1:4174", trace: "retain-on-failure", }, webServer: { - command: "pnpm build && pnpm preview --host 127.0.0.1 --port 4173", - url: "http://127.0.0.1:4173", + command: "pnpm build && pnpm preview --host 127.0.0.1 --port 4174", + url: "http://127.0.0.1:4174", reuseExistingServer: false, timeout: 120_000, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce184a0..86aae4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@sveltejs/adapter-node': - specifier: 5.5.7 - version: 5.5.7(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@22.20.1)))(svelte@5.56.10)(typescript@6.0.3)(vite@8.2.2(@types/node@22.20.1))) + '@sveltejs/adapter-static': + specifier: 3.0.10 + version: 3.0.10(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@22.20.1)))(svelte@5.56.10)(typescript@6.0.3)(vite@8.2.2(@types/node@22.20.1))) '@sveltejs/kit': specifier: 2.70.3 version: 2.70.3(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@22.20.1)))(svelte@5.56.10)(typescript@6.0.3)(vite@8.2.2(@types/node@22.20.1)) @@ -125,12 +125,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@napi-rs/lzma-linux-x64-gnu@1.5.1': - resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} - engines: {node: ^22.20 || ^24.12 || >=25} - cpu: [x64] - os: [linux] - '@oxc-project/types@0.147.0': resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} @@ -235,176 +229,6 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@rollup/plugin-commonjs@29.0.3': - resolution: {integrity: sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-json@6.1.0': - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-node-resolve@16.0.3': - resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-replace@6.0.3': - resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/pluginutils@5.4.0': - resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.63.0': - resolution: {integrity: sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.63.0': - resolution: {integrity: sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.63.0': - resolution: {integrity: sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.63.0': - resolution: {integrity: sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.63.0': - resolution: {integrity: sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.63.0': - resolution: {integrity: sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.63.0': - resolution: {integrity: sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.63.0': - resolution: {integrity: sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.63.0': - resolution: {integrity: sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.63.0': - resolution: {integrity: sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.63.0': - resolution: {integrity: sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.63.0': - resolution: {integrity: sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.63.0': - resolution: {integrity: sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.63.0': - resolution: {integrity: sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.63.0': - resolution: {integrity: sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.63.0': - resolution: {integrity: sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.63.0': - resolution: {integrity: sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.63.0': - resolution: {integrity: sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.63.0': - resolution: {integrity: sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openbsd-x64@4.63.0': - resolution: {integrity: sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.63.0': - resolution: {integrity: sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.63.0': - resolution: {integrity: sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.63.0': - resolution: {integrity: sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.63.0': - resolution: {integrity: sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.63.0': - resolution: {integrity: sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==} - cpu: [x64] - os: [win32] - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -413,10 +237,10 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/adapter-node@5.5.7': - resolution: {integrity: sha512-uOfc9eVlI3A37RRSaKcgrheBYPrfJwC9VMqDp8x/O6tlKdcLLvHThSWD0KNIbjQ/d+7bwLGx3vx6aowAcRfd2g==} + '@sveltejs/adapter-static@3.0.10': + resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==} peerDependencies: - '@sveltejs/kit': ^2.4.0 + '@sveltejs/kit': ^2.0.0 '@sveltejs/kit@2.70.3': resolution: {integrity: sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==} @@ -456,9 +280,6 @@ packages: '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -524,9 +345,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -545,10 +363,6 @@ packages: devalue@5.9.2: resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==} - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -563,9 +377,6 @@ packages: '@typescript-eslint/types': optional: true - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -592,23 +403,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} - engines: {node: '>= 0.4'} - - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -717,9 +511,6 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -748,21 +539,11 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} - hasBin: true - rolldown@1.2.6: resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.63.0: - resolution: {integrity: sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -787,10 +568,6 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - svelte-check@4.5.0: resolution: {integrity: sha512-9lNwPxCLWniFvQIcEv1LFqjIxcFtO3smb5+5BKbRJ3ttL4o2lXCej5rLF4DAnfLPI66oaA81vAxw6ILdIWI7kA==} engines: {node: '>= 18.0.0'} @@ -991,9 +768,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 - '@napi-rs/lzma-linux-x64-gnu@1.5.1': - optional: true - '@oxc-project/types@0.147.0': {} '@playwright/test@1.62.1': @@ -1049,138 +823,15 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-commonjs@29.0.3(rollup@4.63.0)': - dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.63.0) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.7) - is-reference: 1.2.1 - magic-string: 0.30.21 - picomatch: 4.0.7 - optionalDependencies: - rollup: 4.63.0 - - '@rollup/plugin-json@6.1.0(rollup@4.63.0)': - dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.63.0) - optionalDependencies: - rollup: 4.63.0 - - '@rollup/plugin-node-resolve@16.0.3(rollup@4.63.0)': - dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.63.0) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.12 - optionalDependencies: - rollup: 4.63.0 - - '@rollup/plugin-replace@6.0.3(rollup@4.63.0)': - dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.63.0) - magic-string: 0.30.21 - optionalDependencies: - rollup: 4.63.0 - - '@rollup/pluginutils@5.4.0(rollup@4.63.0)': - dependencies: - '@types/estree': 1.0.9 - estree-walker: 2.0.2 - picomatch: 4.0.7 - optionalDependencies: - rollup: 4.63.0 - - '@rollup/rollup-android-arm-eabi@4.63.0': - optional: true - - '@rollup/rollup-android-arm64@4.63.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.63.0': - optional: true - - '@rollup/rollup-darwin-x64@4.63.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.63.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.63.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.63.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.63.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.63.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.63.0': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.63.0': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.63.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.63.0': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.63.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.63.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.63.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.63.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.63.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.63.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.63.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.63.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.63.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.63.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.63.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.63.0': - optional: true - '@standard-schema/spec@1.1.0': {} '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': dependencies: acorn: 8.18.0 - '@sveltejs/adapter-node@5.5.7(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@22.20.1)))(svelte@5.56.10)(typescript@6.0.3)(vite@8.2.2(@types/node@22.20.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@22.20.1)))(svelte@5.56.10)(typescript@6.0.3)(vite@8.2.2(@types/node@22.20.1)))': dependencies: - '@rollup/plugin-commonjs': 29.0.3(rollup@4.63.0) - '@rollup/plugin-json': 6.1.0(rollup@4.63.0) - '@rollup/plugin-node-resolve': 16.0.3(rollup@4.63.0) - '@rollup/plugin-replace': 6.0.3(rollup@4.63.0) '@sveltejs/kit': 2.70.3(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@22.20.1)))(svelte@5.56.10)(typescript@6.0.3)(vite@8.2.2(@types/node@22.20.1)) - rollup: 4.63.0 '@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@22.20.1)))(svelte@5.56.10)(typescript@6.0.3)(vite@8.2.2(@types/node@22.20.1))': dependencies: @@ -1226,8 +877,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/resolve@1.20.2': {} - '@types/trusted-types@2.0.7': {} '@vitest/expect@4.1.11': @@ -1289,8 +938,6 @@ snapshots: clsx@2.1.1: {} - commondir@1.0.1: {} - convert-source-map@2.0.0: {} cookie@0.6.0: {} @@ -1301,8 +948,6 @@ snapshots: devalue@5.9.2: {} - es-errors@1.3.0: {} - es-module-lexer@2.3.2: {} esm-env@1.2.2: {} @@ -1311,8 +956,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 - estree-walker@2.0.2: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -1329,22 +972,6 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - - is-core-module@2.16.2: - dependencies: - hasown: 2.0.4 - - is-module@1.0.0: {} - - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.9 - is-reference@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -1420,8 +1047,6 @@ snapshots: obug@2.1.4: {} - path-parse@1.0.7: {} - pathe@2.0.3: {} picocolors@1.1.1: {} @@ -1444,13 +1069,6 @@ snapshots: readdirp@4.1.2: {} - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - rolldown@1.2.6: dependencies: '@oxc-project/types': 0.147.0 @@ -1472,38 +1090,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.6 '@rolldown/binding-win32-x64-msvc': 1.2.6 - rollup@4.63.0: - dependencies: - '@types/estree': 1.0.9 - optionalDependencies: - '@napi-rs/lzma-linux-x64-gnu': 1.5.1 - '@rollup/rollup-android-arm-eabi': 4.63.0 - '@rollup/rollup-android-arm64': 4.63.0 - '@rollup/rollup-darwin-arm64': 4.63.0 - '@rollup/rollup-darwin-x64': 4.63.0 - '@rollup/rollup-freebsd-arm64': 4.63.0 - '@rollup/rollup-freebsd-x64': 4.63.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.63.0 - '@rollup/rollup-linux-arm-musleabihf': 4.63.0 - '@rollup/rollup-linux-arm64-gnu': 4.63.0 - '@rollup/rollup-linux-arm64-musl': 4.63.0 - '@rollup/rollup-linux-loong64-gnu': 4.63.0 - '@rollup/rollup-linux-loong64-musl': 4.63.0 - '@rollup/rollup-linux-ppc64-gnu': 4.63.0 - '@rollup/rollup-linux-ppc64-musl': 4.63.0 - '@rollup/rollup-linux-riscv64-gnu': 4.63.0 - '@rollup/rollup-linux-riscv64-musl': 4.63.0 - '@rollup/rollup-linux-s390x-gnu': 4.63.0 - '@rollup/rollup-linux-x64-gnu': 4.63.0 - '@rollup/rollup-linux-x64-musl': 4.63.0 - '@rollup/rollup-openbsd-x64': 4.63.0 - '@rollup/rollup-openharmony-arm64': 4.63.0 - '@rollup/rollup-win32-arm64-msvc': 4.63.0 - '@rollup/rollup-win32-ia32-msvc': 4.63.0 - '@rollup/rollup-win32-x64-gnu': 4.63.0 - '@rollup/rollup-win32-x64-msvc': 4.63.0 - fsevents: 2.3.3 - sade@1.8.1: dependencies: mri: 1.2.0 @@ -1524,8 +1110,6 @@ snapshots: std-env@4.2.0: {} - supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.5.0(picomatch@4.0.7)(svelte@5.56.10)(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 diff --git a/provenance/import-receipts.json b/provenance/import-receipts.json index fffd318..13fcc1c 100644 --- a/provenance/import-receipts.json +++ b/provenance/import-receipts.json @@ -252,7 +252,7 @@ "sourceBlobSha1": "2dacd931c9cb4e22d4dcd69c86536259412e5cb7", "sourceSha256": "28340959f557fbcf2f983a94296781df8644754617cff037cc81d97af7a70c1b", "destinationPath": "src/routes/blog/rss.xml/+server.ts", - "destinationSha256": "2446626d4a1fdff22dca6a72dc17e8bb0851a0acc3a674ccdf4f4d125adf1b06", + "destinationSha256": "c8436cb6b9f91eeff325f35267979ed197f8983b4935395b8b332ab2ac1f9f12", "mode": "adapted" }, { @@ -261,7 +261,7 @@ "sourceBlobSha1": "cf1615ac5f9d1a0cc165b19d20da6c767ed51258", "sourceSha256": "ba4b9b89a29ad60fd07666a411807dc5e5fee8fdd419009a6a18dd7ac11a6011", "destinationPath": "src/routes/learn/+layout.svelte", - "destinationSha256": "49af33fc2ca4b4dc2b89fbc45c4a6840b41418728c953e680be4aae80bd334a6", + "destinationSha256": "0f28373223cdbdf7d6e43508c359d7882f84d4afe64ea617b63bf245394d3e2c", "mode": "adapted" }, { diff --git a/provenance/public-tree-policy.json b/provenance/public-tree-policy.json index 39f725d..84258bd 100644 --- a/provenance/public-tree-policy.json +++ b/provenance/public-tree-policy.json @@ -2,6 +2,7 @@ "schemaVersion": 2, "allowedPaths": [ ".github/workflows/ci.yml", + ".github/workflows/pages.yml", ".gitignore", ".node-version", ".npmrc", @@ -57,6 +58,7 @@ "src/lib/theme.ts", "src/routes/+error.svelte", "src/routes/+layout.svelte", + "src/routes/+layout.ts", "src/routes/+page.server.ts", "src/routes/+page.svelte", "src/routes/about/+page.svelte", @@ -83,6 +85,7 @@ "src/routes/learn/vulnerability/cvss/+page.svelte", "src/routes/learn/vulnerability/epss/+page.svelte", "src/routes/licence/+page.svelte", + "static/.nojekyll", "static/favicon.svg", "static/fonts/Geist-Variable.woff2", "static/fonts/GeistMono-Variable.woff2", @@ -100,10 +103,12 @@ "tests/e2e/support.ts", "tests/gitleaks.test.mjs", "tests/import-receipts.test.mjs", + "tests/pages-workflow.test.mjs", "tests/public-safety.test.mjs", "tests/route-links.test.mjs", "tests/shell-boundary.test.mjs", "tests/source-manifest.test.mjs", + "tests/static-pages.test.mjs", "tests/theme.test.ts", "tsconfig.json", "vite.config.ts" @@ -136,7 +141,7 @@ ".yaml", ".yml" ], - "textPaths": [".gitignore", ".node-version", ".npmrc", "LICENSE"], + "textPaths": [".gitignore", ".node-version", ".npmrc", "LICENSE", "static/.nojekyll"], "maxTextBytes": 1000000, "binaryFiles": { "static/fonts/Geist-Variable.woff2": "2ffebe993e969069a9789d15164b7715d42491b5835516c5e3b935d5f81b05f1", diff --git a/scripts/check-routes.mjs b/scripts/check-routes.mjs index c0e8645..59d1999 100644 --- a/scripts/check-routes.mjs +++ b/scripts/check-routes.mjs @@ -1,12 +1,10 @@ -import { spawn } from "node:child_process"; -import { access } from "node:fs/promises"; -import { createServer } from "node:net"; -import { dirname, resolve } from "node:path"; +import { access, lstat, readdir, readFile } from "node:fs/promises"; +import { dirname, extname, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { assertValidDocumentGraph } from "./check-links.mjs"; const REPOSITORY_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const HOST = "127.0.0.1"; +const BUILD_ROOT = resolve(REPOSITORY_ROOT, "build"); const ARTICLE_SLUG = "irap-assessed-not-certified-or-accredited"; export const HTML_ROUTES = [ @@ -43,104 +41,129 @@ export const ABSENT_ROUTES = [ "/blog/does-not-exist", ]; -const reservePort = async () => { - const server = createServer(); - await new Promise((resolveListen, reject) => { - server.once("error", reject); - server.listen(0, HOST, resolveListen); - }); - const address = server.address(); - if (!address || typeof address === "string") throw new Error("could not reserve a local test port"); - await new Promise((resolveClose, reject) => - server.close((error) => (error ? reject(error) : resolveClose())), - ); - return address.port; +const REQUIRED_STATIC_ASSETS = [ + "/.nojekyll", + "/404.html", + "/favicon.svg", + "/logo-dark.svg", + "/logo-light.svg", + "/fonts/Geist-Variable.woff2", + "/fonts/GeistMono-Variable.woff2", + "/fonts/OFL.txt", +]; + +const toBuildPath = (pathname) => { + if (pathname === "/") return resolve(BUILD_ROOT, "index.html"); + const relativePath = pathname.replace(/^\/+|\/+$/gu, ""); + const basename = relativePath.split("/").at(-1) ?? ""; + return extname(relativePath) || basename.startsWith(".") + ? resolve(BUILD_ROOT, relativePath) + : resolve(BUILD_ROOT, relativePath, "index.html"); }; -const waitForServer = async (origin, child, output) => { - const deadline = Date.now() + 20_000; - while (Date.now() < deadline) { - if (child.exitCode !== null) { - throw new Error(`built website server exited with ${child.exitCode}\n${output.join("")}`); - } +const assertInsideBuild = (path) => { + const relativePath = relative(BUILD_ROOT, path); + if (relativePath === "" || relativePath.startsWith("..") || relativePath.startsWith("/")) { + throw new Error(`${path}: artifact path escapes build root`); + } +}; + +const readArtifact = async (pathname, label = pathname) => { + const path = toBuildPath(pathname); + assertInsideBuild(path); + const stats = await lstat(path).catch(() => null); + if (!stats?.isFile() || stats.isSymbolicLink()) { + throw new Error(`${label}: missing regular static artifact ${relative(BUILD_ROOT, path)}`); + } + return readFile(path); +}; + +const assertRouteAbsent = async (route) => { + const relativePath = route.replace(/^\/+|\/+$/gu, ""); + const candidates = [ + resolve(BUILD_ROOT, relativePath), + resolve(BUILD_ROOT, `${relativePath}.html`), + resolve(BUILD_ROOT, relativePath, "index.html"), + ]; + for (const candidate of candidates) { + assertInsideBuild(candidate); try { - const response = await fetch(origin, { redirect: "manual" }); - if (response.status === 200) return; - } catch { - // The server has not bound its local port yet. + await access(candidate); + throw new Error(`${route}: excluded route produced ${relative(BUILD_ROOT, candidate)}`); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") continue; + throw error; } - await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); } - throw new Error(`timed out waiting for built website server\n${output.join("")}`); }; -const stopServer = async (child) => { - if (child.exitCode !== null) return; - child.kill("SIGTERM"); - await Promise.race([ - new Promise((resolveExit) => child.once("exit", resolveExit)), - new Promise((resolveTimeout) => setTimeout(resolveTimeout, 5_000)), - ]); - if (child.exitCode === null) child.kill("SIGKILL"); +const listArtifactFiles = async (directory = BUILD_ROOT) => { + const files = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = resolve(directory, entry.name); + if (entry.isSymbolicLink()) { + throw new Error(`${relative(BUILD_ROOT, path)}: symbolic links are not allowed in the artifact`); + } + if (entry.isDirectory()) files.push(...(await listArtifactFiles(path))); + else if (entry.isFile()) files.push(path); + } + return files; }; -const fetchLocal = async (origin, target) => fetch(`${origin}${target}`, { redirect: "manual" }); - export const checkBuiltRoutes = async () => { - await access(resolve(REPOSITORY_ROOT, "build/index.js")); - const port = await reservePort(); - const origin = `http://${HOST}:${port}`; - const output = []; - const child = spawn(process.execPath, ["build/index.js"], { - cwd: REPOSITORY_ROOT, - env: { ...process.env, HOST, ORIGIN: origin, PORT: String(port) }, - stdio: ["ignore", "pipe", "pipe"], - }); - child.stdout.on("data", (chunk) => output.push(chunk.toString())); - child.stderr.on("data", (chunk) => output.push(chunk.toString())); - - try { - await waitForServer(origin, child, output); - const documents = new Map(); - for (const route of HTML_ROUTES) { - const response = await fetchLocal(origin, route); - if (response.status !== 200) throw new Error(`${route}: expected 200, received ${response.status}`); - if (!response.headers.get("content-type")?.startsWith("text/html")) { - throw new Error(`${route}: expected an HTML response`); - } - documents.set(route, await response.text()); + const documents = new Map(); + for (const route of HTML_ROUTES) { + const html = (await readArtifact(route)).toString("utf8"); + if (!html.startsWith("")) { + throw new Error(`${route}: expected a prerendered HTML document`); } + documents.set(route, html); + } - const rss = await fetchLocal(origin, "/blog/rss.xml"); - if (rss.status !== 200) throw new Error(`/blog/rss.xml: expected 200, received ${rss.status}`); - if (rss.headers.get("content-type") !== "application/rss+xml; charset=utf-8") { - throw new Error("/blog/rss.xml: unexpected content type"); - } - const rssBody = await rss.text(); - if (!rssBody.startsWith('') || !rssBody.includes(ARTICLE_SLUG)) { - throw new Error("/blog/rss.xml: missing expected XML declaration or current article"); - } + const rssBody = (await readArtifact("/blog/rss.xml")).toString("utf8"); + if (!rssBody.startsWith('') || !rssBody.includes(ARTICLE_SLUG)) { + throw new Error("/blog/rss.xml: missing expected XML declaration or current article"); + } - const internalTargets = assertValidDocumentGraph(documents); - for (const target of internalTargets) { - const response = await fetchLocal(origin, target); - if (response.status !== 200) { - throw new Error(`${target}: linked internal target returned ${response.status}`); - } - } - for (const route of ABSENT_ROUTES) { - const response = await fetchLocal(origin, route); - if (response.status !== 404) throw new Error(`${route}: expected 404, received ${response.status}`); - } + for (const asset of REQUIRED_STATIC_ASSETS) await readArtifact(asset, asset); + await access(resolve(BUILD_ROOT, "CNAME")).then( + () => { + throw new Error("CNAME: must not be generated before custom-domain configuration is approved"); + }, + (error) => { + if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error; + }, + ); + await access(resolve(BUILD_ROOT, "index.js")).then( + () => { + throw new Error("index.js: adapter-node server output must not be present"); + }, + (error) => { + if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error; + }, + ); + + const internalTargets = assertValidDocumentGraph(documents); + for (const target of internalTargets) { + const pathname = new URL(target, "https://link42.app").pathname; + await readArtifact(pathname, target); + } + for (const route of ABSENT_ROUTES) await assertRouteAbsent(route); - return { - checkedHtmlRoutes: documents.size, - checkedInternalTargets: internalTargets.size, - checkedMissingRoutes: ABSENT_ROUTES.length, - }; - } finally { - await stopServer(child); + const artifactFiles = await listArtifactFiles(); + if (!artifactFiles.some((path) => path.endsWith(".js"))) { + throw new Error("static artifact has no generated JavaScript assets"); + } + if (!artifactFiles.some((path) => path.endsWith(".css"))) { + throw new Error("static artifact has no generated CSS assets"); } + + return { + checkedArtifactFiles: artifactFiles.length, + checkedHtmlRoutes: documents.size, + checkedInternalTargets: internalTargets.size, + checkedMissingRoutes: ABSENT_ROUTES.length, + }; }; const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url); @@ -148,7 +171,7 @@ if (isMain) { try { const result = await checkBuiltRoutes(); console.log( - `Route verification passed: ${result.checkedHtmlRoutes} HTML routes, RSS, ${result.checkedInternalTargets} internal targets, and ${result.checkedMissingRoutes} expected 404 routes.`, + `Static artifact verification passed: ${result.checkedHtmlRoutes} HTML routes, RSS, ${result.checkedInternalTargets} internal targets, ${result.checkedMissingRoutes} absent routes, and ${result.checkedArtifactFiles} files.`, ); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..e325c17 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const prerender = true; +export const trailingSlash = "always"; diff --git a/src/routes/blog/rss.xml/+server.ts b/src/routes/blog/rss.xml/+server.ts index 7a43186..5524995 100644 --- a/src/routes/blog/rss.xml/+server.ts +++ b/src/routes/blog/rss.xml/+server.ts @@ -1,6 +1,9 @@ import { loadBlogArticles, renderBlogRss } from "../../../lib/server/blog"; import type { RequestHandler } from "./$types"; +export const prerender = true; +export const trailingSlash = "never"; + export const GET: RequestHandler = async () => new Response(renderBlogRss(await loadBlogArticles()), { headers: { diff --git a/src/routes/learn/+layout.svelte b/src/routes/learn/+layout.svelte index 1d4d540..b7531b9 100644 --- a/src/routes/learn/+layout.svelte +++ b/src/routes/learn/+layout.svelte @@ -68,14 +68,20 @@ const nav = [ }, ]; +// biome-ignore lint/correctness/noUnusedVariables: used in Svelte template +function normalisePath(path: string) { + return path === "/" ? path : path.replace(/\/$/u, ""); +} + // biome-ignore lint/correctness/noUnusedVariables: used in Svelte template function isActive(href: string) { - return page.url.pathname === href || page.url.pathname.startsWith(`${href}/`); + const path = normalisePath(page.url.pathname); + return path === href || path.startsWith(`${href}/`); } // biome-ignore lint/correctness/noUnusedVariables: used in Svelte template let breadcrumbs = $derived.by(() => { - const path = page.url.pathname; + const path = normalisePath(page.url.pathname); if (path === "/learn") return [{ title: "Learn", href: "/learn" }]; const segments = path.split("/").filter(Boolean); @@ -125,7 +131,7 @@ let breadcrumbs = $derived.by(() => {