diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d3f94de --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 + +updates: + - package-ecosystem: npm + directory: /website + schedule: + interval: weekly + target-branch: main + open-pull-requests-limit: 3 + cooldown: + default-days: 7 + semver-major-days: 30 + semver-minor-days: 7 + semver-patch-days: 3 + groups: + development-dependencies: + dependency-type: development + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + target-branch: main + open-pull-requests-limit: 1 + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/pages-deploy.yml b/.github/workflows/pages-deploy.yml new file mode 100644 index 0000000..27fcca9 --- /dev/null +++ b/.github/workflows/pages-deploy.yml @@ -0,0 +1,151 @@ +name: Pages deployment + +on: + workflow_run: + workflows: + - Website checks + types: + - completed + workflow_dispatch: + inputs: + run_id: + description: Successful Website checks run containing inkcre-website-dist + required: true + type: number + +permissions: + actions: read + contents: read + +concurrency: + group: pages-main + cancel-in-progress: true + +jobs: + identity: + name: Resolve trusted production identity + if: >- + github.event_name == 'workflow_dispatch' || ( + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main' + ) + runs-on: ubuntu-latest + outputs: + head_sha: ${{ steps.identity.outputs.head_sha }} + run_id: ${{ steps.identity.outputs.run_id }} + steps: + - name: Resolve the exact current main artifact + id: identity + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const runId = context.eventName === 'workflow_dispatch' + ? Number(context.payload.inputs.run_id) + : context.payload.workflow_run.id + const { data: run } = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }) + if (run.name !== 'Website checks' || run.path !== '.github/workflows/website-check.yml') { + core.setFailed('Artifact must come from the trusted Website checks workflow.') + return + } + if (run.conclusion !== 'success' || run.head_branch !== 'main') { + core.setFailed('Production delivery requires a successful main run.') + return + } + const currentMain = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'heads/main', + }) + if (currentMain.data.object.sha !== run.head_sha) { + core.setFailed('Production delivery requires the current exact main head.') + return + } + core.setOutput('head_sha', run.head_sha) + core.setOutput('run_id', String(runId)) + + deploy: + name: Deploy checked website artifact + needs: identity + permissions: + actions: read + contents: read + deployments: write + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: + name: production + url: https://inkcre.dev + steps: + - name: Checkout the trusted delivery controller + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + path: controller + persist-credentials: false + + - name: Set up Node + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version-file: controller/website/package.json + + - name: Download the exact checked artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: inkcre-website-dist + path: .pages-delivery/dist + run-id: ${{ needs.identity.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Reverify main before delivery + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RELEASE_HEAD_SHA: ${{ needs.identity.outputs.head_sha }} + with: + script: | + const currentMain = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'heads/main', + }) + if (currentMain.data.object.sha !== process.env.RELEASE_HEAD_SHA) { + core.setFailed('Main changed after artifact selection; refusing delivery.') + } + + - name: Ensure the Pages project + run: node controller/website/scripts/configure-pages.mjs project + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_PAGES_DOMAIN: inkcre.dev + CLOUDFLARE_PAGES_PROJECT: ${{ vars.CLOUDFLARE_PAGES_PROJECT }} + + - name: Deploy to Cloudflare Pages + id: pages + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + packageManager: npm + wranglerVersion: 4.114.0 + workingDirectory: .pages-delivery + command: >- + pages deploy dist --project-name=${{ vars.CLOUDFLARE_PAGES_PROJECT }} --branch=main + + - name: Ensure the custom domain + run: node controller/website/scripts/configure-pages.mjs domain + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_PAGES_DOMAIN: inkcre.dev + CLOUDFLARE_PAGES_PROJECT: ${{ vars.CLOUDFLARE_PAGES_PROJECT }} + + - name: Smoke-test Pages and inkcre.dev + run: node controller/website/scripts/verify-pages-deployment.mjs + env: + CLOUDFLARE_PAGES_DEPLOYMENT_ID: ${{ steps.pages.outputs.pages-deployment-id }} + CLOUDFLARE_PAGES_DEPLOYMENT_URL: ${{ steps.pages.outputs.deployment-url }} + INKCRE_WEBSITE_ORIGIN: https://inkcre.dev diff --git a/.github/workflows/website-check.yml b/.github/workflows/website-check.yml new file mode 100644 index 0000000..8dce620 --- /dev/null +++ b/.github/workflows/website-check.yml @@ -0,0 +1,53 @@ +name: Website checks + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: website-checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + website: + name: Website contract + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install the pinned pnpm release + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + package_json_file: website/package.json + + - name: Set up Node + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version-file: website/package.json + cache: pnpm + cache-dependency-path: website/pnpm-lock.yaml + + - name: Install the frozen website package + run: pnpm --dir website install --frozen-lockfile + + - name: Verify the website contract + run: pnpm --dir website check + + - name: Upload the exact checked website artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: inkcre-website-dist + path: website/.vitepress/dist + if-no-files-found: error + include-hidden-files: true + retention-days: 7 diff --git a/tasks/vitepress-documentation-site/foundation-audit.md b/tasks/vitepress-documentation-site/foundation-audit.md new file mode 100644 index 0000000..d45f763 --- /dev/null +++ b/tasks/vitepress-documentation-site/foundation-audit.md @@ -0,0 +1,195 @@ +# VitePress Website Evolution Foundation Audit + +Date: 2026-07-28 + +## Purpose + +This audit asks a stricter question than whether the current proposal can build: which early +decisions become expensive after URLs are published, content grows, or CI becomes a merge +contract? + +The answer is not to pre-build the future site. It is to establish a few stable seams now and +leave reversible features unimplemented. + +## Decision Matrix + +| Concern | Correct early decision | Activation point | +|---|---|---| +| Site boundary | Keep one independent `website/` package and expose only its content root. | Initial scaffold | +| Locale-ready source | Store the default-language source in `website/content/en/**`; rewrite it to public root routes. | Initial scaffold | +| Public URL shape | Use `base: '/'`, `cleanUrls: true`, extensionless links, and stable section-index routes. | Initial scaffold | +| Active locales | Configure only the default root locale; do not advertise an incomplete second locale. | Initial scaffold | +| Content conventions | Use one H1, useful page descriptions, public-route links, and explicit anchors for durable deep links. | First page | +| Formatting | Add package-local EditorConfig and pinned formatter checks before content accumulates. | Initial scaffold | +| CI contract | Run one always-reporting required check with frozen install, least privilege, concurrency, timeout, and pinned actions. | First CI slice | +| Dependency upkeep | Add a scoped weekly updater for `/website`, without auto-merge. | First CI slice | +| Canonical origin | Confirm hostname, canonical URLs, sitemap hostname, robots behavior, and social URLs. | Before first public deployment | +| Redirect ownership | Keep direct redirects in version control under the site public directory. | Before the first published route moves | +| Section namespaces | Choose a durable route before publishing the first page in that section; do not create placeholders now. | First section page | +| Search and custom UI | Add only after corpus size or a real reader need justifies them. | Later pressure | + +## Recommended Initial Route Contract + +The initial configuration should encode: + +```text +source root: website/content +default source: website/content/en/** +default public root: /** +future Chinese root: /zh/** +deployment base: / +clean URLs: enabled +active locales: English root only +``` + +The relevant VitePress model is: + +```text +srcDir: 'content' +base: '/' +cleanUrls: true +rewrites: { 'en/:rest*': ':rest*' } +locales.root: English +``` + +English at the public root is a recommendation, not yet a confirmed product decision. It +matches the language of the current durable Hub documents and avoids an `/en/` migration if +Chinese is added later. Sir should confirm it before implementation. + +Public route rules: + +- `/` is the simple site index. +- Section indexes use a trailing slash, for example `/manual/`. +- Leaf pages are extensionless, for example `/manual/getting-started`. +- Route segments are lowercase ASCII kebab-case and do not contain numeric ordering prefixes. +- Internal Markdown links target public routes and omit `.md` and `.html`. +- A heading used as a durable cross-page target receives an explicit custom anchor. +- Once published, a route is an external contract; a move requires a direct permanent redirect. + +Candidate future namespaces are `/manual/`, `/developer/`, and `/about/`. They reserve no content +and should not appear in navigation until real pages exist. Their names must be confirmed before +the first page in each section is published, not necessarily for the one-page scaffold. + +## Why This Must Be Done Early + +### Locale-ready source without locale-prefixed default URLs + +VitePress itself stores English under `docs/en/**` and rewrites it to root routes. This separates +source ownership from public addresses: adding `docs/zh/**` does not force English URLs or files +to move. + +Starting with `content/index.md` is simpler only for the first page. Moving an established +corpus to `content/en/**` later creates a broad source rename, affects relative links and edit +history, and makes translation rollout harder to review. + +### URL shape is independent from canonical-host metadata + +`cleanUrls` controls the shape of every internal link, so postponing it creates avoidable URL +and redirect churn. Cloudflare Pages serves clean paths for generated `.html` and `index.html` +files, so the intended host supports this contract. + +By contrast, sitemap hostnames, canonical links, Open Graph URLs, and production robots rules +depend on the final origin. Those should be decided before publication, not invented during the +local scaffold. + +### Formatting is a content migration concern + +Adding a formatter after dozens of Markdown pages creates a repository-wide mechanical rewrite +that obscures meaningful history. A package-local EditorConfig, pinned formatter, and +`format:check` gate are small now and expensive to retrofit. + +Execution-time inspection found that modern neighboring InKCre repositories use Oxfmt. Its +official language support includes Markdown, TypeScript, JSON, and YAML, so the website should +reuse a pinned Oxfmt release instead of creating a project-local Prettier exception. + +### A required CI check must always report + +GitHub documents that a workflow skipped by path filtering can leave a required check pending. +For this small site, an always-run website check is cheaper and more reliable than a +path-filtered required workflow. If execution cost later becomes material, an always-reporting +change-detection job can replace it without changing the branch-protection contract. + +## Evolution Pre-mortem + +### Adding Chinese + +Add `website/content/zh/**` and a `zh` locale while keeping English at root. Do not enable the +locale switcher merely because one translated page exists. + +A disposable VitePress 2 fixture showed that the build succeeds even when the generated locale +switch points to a missing counterpart route. Therefore translation rollout needs one of: + +- route parity for the published locale set; or +- a deliberate locale-switch implementation that falls back safely. + +The minimal site should use only the root locale until that rule is implemented. + +### Expanding sections + +Navigation and sidebar data can remain inline while there is one page and split into +section-local configuration modules later. Do not create empty directories, placeholder pages, +or speculative navigation merely to reserve the future information architecture. + +### Moving a published page + +Keep redirect rules in `website/content/public/_redirects` when the first redirect is needed. +Use direct old-to-new mappings rather than chains, and test both the canonical destination and +the old URL in deployment smoke checks. + +### Versioning unstable product documentation + +Keep current documentation versionless at root. Do not introduce `/latest/`, `/v0/`, or version +subdomains before InKCre has an explicit multi-version support obligation. If historical +documentation later becomes necessary, preserve current routes and add an archive rather than +moving the current corpus. + +### Deploying + +Build once in the unprivileged verification job and deploy that exact artifact in a separate +privileged job. Before first publication, confirm the canonical hostname and add sitemap, +canonical/social metadata, production robots behavior, and preview `noindex` handling. + +Cloudflare's default cache behavior is sufficient for the first static site; custom global +cache rules are not an early requirement. + +## Disposable Route Experiment + +A VitePress `2.0.0-alpha.18` fixture used `srcDir: 'content'`, `cleanUrls: true`, an +`en/:rest* -> :rest*` rewrite, and only a root English locale. + +Observed output: + +- `content/en/index.md` produced `/index.html`, not `/en/index.html`; +- `content/en/manual/index.md` produced `/manual/index.html`; +- `content/en/manual/getting-started.md` produced + `/manual/getting-started.html`; +- generated internal links used clean public paths without `.html`; +- adding a `zh` locale produced `/zh/**`; +- a missing Chinese counterpart did not fail the production build. + +This validates the proposed source/public split and disproves an assumption that the normal +VitePress build alone enforces translation-route parity. + +## Intentionally Reversible Later + +No initial placeholder or abstraction is needed for: + +- search; +- custom theme components and CSS; +- split navigation/sidebar modules; +- edit links and last-updated display; +- PWA, Twoslash, LLM exports, analytics, or external search; +- data loaders and generated pages without an executable canonical owner; +- a dedicated external-link crawler. + +## Primary Evidence + +- [VitePress routing and rewrites](https://vitepress.dev/guide/routing) +- [VitePress internationalization](https://vitepress.dev/guide/i18n) +- [VitePress Markdown custom anchors](https://vitepress.dev/guide/markdown) +- [VitePress asset and public-directory behavior](https://vitepress.dev/guide/asset-handling) +- [VitePress's own locale-ready configuration](https://github.com/vuejs/vitepress/blob/430a890a17910593e26e9654b141ef3855ecceac/docs/.vitepress/config.ts) +- [Oxfmt language support](https://oxc.rs/docs/guide/usage/formatter/language-support) +- [Cloudflare Pages serving behavior](https://developers.cloudflare.com/pages/configuration/serving-pages/) +- [Cloudflare Pages redirects](https://developers.cloudflare.com/pages/configuration/redirects/) +- [GitHub Actions path-filter status behavior](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax) diff --git a/tasks/vitepress-documentation-site/implementation-plan.md b/tasks/vitepress-documentation-site/implementation-plan.md new file mode 100644 index 0000000..316eb62 --- /dev/null +++ b/tasks/vitepress-documentation-site/implementation-plan.md @@ -0,0 +1,256 @@ +# VitePress Documentation Site Implementation Plan + +Status: local website implemented and verified on 2026-07-28; production deployment is +authorized and in progress. English owns the public root and future Chinese content will use +`/zh/`. + +## Impact Handshake + +- **Address and Object**: add an isolated VitePress package, configuration, locale-ready content + root, formatting contract, and simple index under `website/**`; later add one root-level + website-check workflow and scoped dependency-updater configuration. +- **State Diff**: no site runtime or public build -> one locally buildable documentation site + whose content is a human-facing projection of Hub truth and whose first URLs can evolve + without a default-language migration. +- **Blast Radius**: Hub-local website development and future static publication move; canonical + `00-meta/**`, `10-prd/**`, and `20-product-tdd/**` content and Spoke-local implementations do + not. Website-only commits do not require Spoke shared-reference bumps. +- **Invariants**: canonical Hub owners stay authoritative; VitePress reads only website-owned + Markdown; the site does not imply a stable, productized User Manual, Developer Guide, About + surface, or marketing landing page. +- **Verification**: formatting, an explicit dependency audit, deterministic VitePress production + build with default dead-link failure, route-output assertions, manual preview acceptance, SVC + health/no-op, and `git diff --check`. + +## Confirmed Language Contract + +Sir confirmed: + +```text +English source: website/content/en/** +English public URLs: /** +future Chinese URLs: /zh/** +active locales now: English root only +``` + +This follows the language of the current Hub corpus and VitePress's own source/public split. + +The following are engineering defaults rather than additional content commitments: + +- deploy the official website at root with `base: '/'`; +- enable `cleanUrls` from the first build; +- use extensionless public-route links; +- keep future sections unpublished until real content exists; +- keep current documentation versionless. + +## Slice 0: Re-check Toolchain + +Immediately before mutation, run: + +```text +node --version +pnpm --version +npm view vitepress version dist-tags +npm view vitepress@next version dependencies +``` + +Resolve and audit the exact dependency graph in a disposable directory. Stop if the selected +graph has a high-severity advisory or the minimal fixture no longer builds. Keep this +network-dependent audit as an explicit dependency-selection and dependency-update check rather +than embedding it in the deterministic website `check` command. + +Prepared baseline from 2026-07-28: + +- Node `22.22.3`; +- pnpm `11.17.0`; +- exact VitePress `2.0.0-alpha.18`. + +These versions are implementation candidates, not timeless project truth. Re-check them instead +of floating on `latest` or `next`. + +## Slice 1: Establish The Isolated Website Package + +Create: + +```text +website/ + README.md + package.json + pnpm-lock.yaml + .editorconfig + .gitignore + .oxfmtrc.json + .vitepress/ + config.mts + content/ + en/ + index.md + scripts/ + verify-build.mjs +``` + +Use existing InKCre Node package conventions: + +- package-local `packageManager`; +- package-local `devEngines.runtime`; +- exact Node, pnpm, VitePress, and Oxfmt versions; +- package-local ignores for `node_modules`, VitePress cache, and build output; +- `pnpm install --frozen-lockfile` in CI; +- `dev`, `build`, `preview`, `format`, `format:check`, and deterministic `check` scripts. + +The `check` script runs formatting verification, the production build, and generated-route +verification. Keep the network-dependent advisory audit explicit and separate. + +Configure: + +```text +srcDir: 'content' +base: '/' +cleanUrls: true +rewrites: { 'en/:rest*': ':rest*' } +locales.root: English +``` + +Do not point VitePress at the repository root or canonical Hub directories. Do not add a public +directory until there is a real fixed-name asset or redirect to own. + +`website/README.md` records the route and authoring contract so future pages follow it without +requiring a formatter or URL migration. + +## Slice 2: Add The Minimal Website + +The initial index contains only: + +- repository/site identity; +- one concise description; +- links to the canonical Product PRD and Product TDD sources; +- optionally a link to the source repository. + +The page uses one H1 and a useful description. Internal links target public routes and omit +`.md` and `.html`; headings that become cross-page targets receive explicit anchors. + +Use the default VitePress theme with minimal title, description, language, and navigation +configuration. Keep the default dead-link behavior. Do not add: + +- marketing hero copy or feature grids; +- product CTAs; +- User Manual, Developer Guide, or About pages; +- empty future navigation or placeholder directories; +- copied Hub document trees; +- a search control for the one-page corpus; +- sitemap, canonical URLs, or deployment-origin metadata before the hostname is confirmed; +- PWA, LLM output, Twoslash, custom Markdown transforms, or custom theme components. + +Future website pages may summarize or reorganize Hub truth for their audience. They should link +to canonical sources when useful, but no build-time inclusion or freshness automation is +required. + +## Slice 3: Verify The Local Site + +Repository-level verification: + +```text +pnpm --dir website install --frozen-lockfile +pnpm --dir website check +pnpm --dir website audit --audit-level high +svc status --json +svc init --agent codex --json +git diff --check +``` + +Assert from the production output: + +- the index is emitted at `/`, not `/en/`; +- page language is the selected root locale; +- no default-language prefix leaks into generated URLs; +- generated page links are clean and extensionless; +- default dead-link checking remains enabled; +- only website-owned Markdown becomes routable. + +Manually inspect: + +- the production site through `pnpm --dir website preview`; +- narrow and wide viewport layout; +- keyboard navigation and visible focus; +- title, description, and language metadata; +- simple navigation with no speculative sections; +- canonical Hub documents remain unchanged. + +Do not add an SVC `dev` target in this slice. There is not yet repeated multi-worktree +coordination pressure or an instance-aware readiness contract to justify it. + +## Slice 4: Add The CI And Update Contract + +After local verification passes: + +1. add one website-check workflow using pinned action SHAs; +2. trigger it for relevant PR and branch events without workflow-level path filtering, so a + required check always reports; +3. declare least privilege, concurrency cancellation, and a finite timeout; +4. set up pnpm and Node from `website/package.json`, cache against + `website/pnpm-lock.yaml`, then run frozen install and `pnpm --dir website check`; +5. add a weekly dependency updater scoped to `/website`, without auto-merge; +6. include GitHub Actions digest updates in the existing repository-level updater contract; +7. upload static output only when a later deployment slice needs artifact handoff. + +Reuse InKCre's toolchain and dependency-update conventions, not the application-specific +client-web Pages workflow. Do not copy database E2E, SPA fallback, release identity, preview +cleanup, or Changesets logic. + +If CI cost later becomes meaningful, use an always-reporting change-detection job instead of +making a required workflow skippable. + +## Authorized Deployment Slice + +Publish through a Cloudflare Pages Direct Upload project named `inkcre-website`: + +1. add sitemap hostname, canonical/Open Graph URLs, production robots behavior, and explicit + `noindex` headers for `pages.dev` origins; +2. upload the exact artifact produced by the unprivileged website contract job; +3. in a separate production-environment job, idempotently ensure the Direct Upload project and + `inkcre.dev` custom domain through the Cloudflare Pages API; +4. deploy the downloaded artifact using the existing organization Cloudflare credentials; +5. smoke-test the immutable Pages deployment and `https://inkcre.dev`; +6. keep application deployment at `app.inkcre.dev` independent. + +External state in scope: + +- allow `InKCre/docs` to consume the existing selected organization secrets + `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`; +- set repository variable `CLOUDFLARE_PAGES_PROJECT=inkcre-website`; +- create or reconcile the `inkcre-website` Pages project with production branch `main`; +- attach `inkcre.dev`, allowing Cloudflare to create the apex DNS record and certificate; +- create a GitHub `production` deployment environment through the workflow. + +GitHub preparation completed on 2026-07-28: the selected organization secrets now also allow +`InKCre/docs`, the repository variable is set, and the `production` environment accepts +deployments only from `main`. The workflow remains responsible for idempotently reconciling the +Cloudflare Pages project and custom domain. + +When the first published route moves, add its direct permanent redirect under +`website/content/public/_redirects` and verify both old and new URLs. Do not add custom global +cache rules until measured behavior requires them. + +## Deferred Content Expansion + +Add User Manual, Developer Guide, About, and a fuller Landing only when real content and stable +product pressure exist. Candidate route namespaces are `/manual/`, `/developer/`, and `/about/`; +confirm each before its first page is published rather than creating placeholders now. + +When Chinese content enters scope, add `/zh/**` only after the published locale set has route +parity or a deliberate fallback-capable locale switch. A normal VitePress build does not detect +missing translated counterparts. + +Keep current documentation versionless. Add historical versions only when InKCre has a real +multi-version support obligation, without moving the current public routes. + +## Stop Conditions + +Return to discussion before continuing if: + +- website implementation would require changing canonical Hub truth; +- a dependency or VitePress prerelease migration changes the planned minimal surface; +- site setup starts to introduce root workspace metadata or unrelated repository tooling; +- dead-link repair would require publishing an internal page; +- implementation starts to create speculative product documentation; +- deployment or external-domain work enters scope without explicit authorization. diff --git a/tasks/vitepress-documentation-site/industry-benchmark.md b/tasks/vitepress-documentation-site/industry-benchmark.md new file mode 100644 index 0000000..aa0d5bc --- /dev/null +++ b/tasks/vitepress-documentation-site/industry-benchmark.md @@ -0,0 +1,262 @@ +# VitePress Documentation Site Industry Benchmark + +Date: 2026-07-28 + +## Purpose And Method + +This benchmark tests both whether the planned InKCre boundary is sound and which foundations +successful VitePress projects establish before content scale makes them expensive to change. It +is not a feature checklist or a justification of choices already made. + +The sample deliberately covers three different pressures: + +- a dedicated documentation repository: Vue documentation; +- documentation packages inside product repositories: VitePress, Vite, and Vitest; +- large generated or multi-surface documentation systems: VueUse and UnoCSS. + +All repository claims below were checked against the named default-branch commit and official +VitePress documentation. Popularity is only a filter for operational evidence, not proof that +every mechanism should be copied. At the time of inspection, the repositories ranged from about +3,200 to 82,000 GitHub stars. + +## Repository Comparison + +| Project | Snapshot | Site topology and source model | Build and publication | Relevant lesson | +|---|---|---|---|---| +| [Vue docs](https://github.com/vuejs/docs/tree/7681134fd8505e61a265d161d73d28acb3c74822) | `7681134` | The repository is the site. VitePress configuration lives in `.vitepress/`, while `srcDir: 'src'` keeps public Markdown in one content root. Navigation is explicit; theme and marketing integrations are extensive. | `dev`, `build`, `preview`, and Vue type-check scripts; VitePress build retains default dead-link failure. Netlify builds `.vitepress/dist`; there is no PR site-build workflow. | Closest positioning match. Adopt the bounded content root and typed configuration, but not its mature marketing/theme surface or provider-only validation. | +| [VitePress](https://github.com/vuejs/vitepress/tree/430a890a17910593e26e9654b141ef3855ecceac) | `430a890` | `docs/` is a workspace package that dogfoods the framework through `workspace:*`. Locale content, translation tracking, search, and metadata are split only because the project serves the framework itself in many languages. | Framework `check` covers formatting, build, types, unit, init, and browser E2E. Netlify separately builds the production docs and translation report. | Treat VitePress's tests as framework-owner obligations, not normal site requirements. Its minimal default-theme extension is more relevant than its locale and E2E machinery. | +| [Vite](https://github.com/vitejs/vite/tree/d17d739eedc4e8ede5973f1533826f27554c2078) | `d17d739` | `docs/` is a workspace package inside the product monorepo. Markdown is primary; small data loaders and a `buildEnd` hook produce blog/RSS data. Version data comes from the product package. | `test-docs` runs the VitePress build in GitHub CI. Netlify runs `ci-docs`, which builds product packages before the site because the docs consume them. | Strongest CI reference: make the production docs build a merge gate. Do not copy the product-build dependency, RSS hook, custom theme, or version-site redirects. | +| [Vitest](https://github.com/vitest-dev/vitest/tree/eb16ab80330c7f33cc894eede76633191358d7b4) | `eb16ab8` | `docs/` is a workspace package. Most content is handwritten, while CLI pages are generated from executable CLI definitions. The site adds PWA, custom theme, and build-time contributor data. | CI checks that generated CLI content is current. Netlify performs the full docs build, including a product build and network prefetch. | Generate only content with an executable canonical owner and a demonstrated drift problem. Avoid PWA and build-time network dependencies for the InKCre first slice. | +| [VueUse](https://github.com/vueuse/vueuse/tree/a7d9aba02985c8f71931b99ae903b458a8d652da) | `a7d9aba` | VitePress uses `packages/` as its site root. Function metadata, types, source files, demos, Git history, and Markdown transforms produce navigation and rich API pages. | A high-memory build first refreshes metadata and package outputs. Netlify publishes the result; repository CI primarily protects the library. | This is a scale-specific code-to-doc projection system. It is evidence against introducing generation before InKCre has many uniform executable documentation units. | +| [UnoCSS](https://github.com/unocss/unocss/tree/e28a47c557fe179935a37a4fbeb650292d0d1d5a) | `e28a47c` | `docs/` remains mostly handwritten with explicit navigation, while shared generated package data powers documentation, playground, and interactive surfaces. | Netlify builds product packages, docs, playground, and interactive apps together. CI spans operating systems and product tests. | Its local search and narrow dead-link exceptions are reusable later. Its multi-product build, interactive tools, and package-generation graph are outside InKCre's current pressure. | + +## Version And Toolchain Evidence + +The external sample has already moved to the VitePress 2 prerelease line: + +| Project | Declared VitePress version | +|---|---| +| Vue docs | `^2.0.0-alpha.17` | +| Vite | `^2.0.0-alpha.18` | +| Vitest | `2.0.0-alpha.16` | +| VueUse | `^2.0.0-alpha.17` through its workspace catalog | +| UnoCSS | `^2.0.0-alpha.17` through its workspace catalog | + +VitePress itself was at `2.0.0-alpha.18`. This supports the preflight conclusion that VitePress +2 is a credible implementation candidate, but does not justify floating on `next`. InKCre +should keep one exact version in `website/package.json` and its lockfile, then re-evaluate that +pin when implementation starts. + +The sample consistently declares pnpm and uses Node 20 or newer; current hosted builds commonly +use Node 24. InKCre's prepared Node `22.22.3` and pnpm `11.17.0` remain compatible candidates, +subject to the execution-time package check. + +## Cross-Project Findings + +### 1. A bounded site root is the common architectural seam + +Every project gives VitePress a deliberate source boundary: a dedicated repository root, a +`docs/` package, `srcDir`, or a package subtree. None treats unrelated repository Markdown as +implicitly public. + +This supports: + +```text +website/ package and site root + .vitepress/ site configuration + content/ only routable Markdown +``` + +It also supports the prior projection decision. Mature projects generate from code only when +their product topology makes code an appropriate canonical owner; they do not demonstrate a +general need to compile an internal collaboration Hub directly. + +### 2. The production build is the basic content contract + +VitePress's `ignoreDeadLinks` default is `false`: a production build fails on dead links unless +the site deliberately opts out. The sampled sites generally rely on this behavior instead of a +second internal-link checker. Vite goes further and executes the docs build in GitHub CI. + +For InKCre: + +- `pnpm --dir website check` should be deterministic and include the production build; +- do not set `ignoreDeadLinks: true`; +- add only narrow exceptions if a real non-page route later requires one; +- do not add browser E2E while the site is static Markdown plus the default theme. + +### 3. Build verification and deployment are separate concerns + +Most sampled sites publish through Netlify, while repository CI protects selected source +contracts. The official VitePress GitHub Pages example also separates a build job from the +privileged deployment job and gives the deployment job explicit permissions and concurrency. + +For InKCre: + +- add a PR/push site-build workflow after the local package works; +- use frozen installation and pinned action SHAs; +- keep deployment, credentials, domain metadata, previews, and post-deploy smoke in the later + deployment slice; +- build once and deploy the checked artifact when publication enters scope. + +### 4. Search follows corpus size + +Vite, Vitest, and UnoCSS use built-in local search. Vue docs, VitePress, and VueUse use Algolia +because their established corpora and traffic justify an external index. VitePress supports +local search without an external service. + +The one-page InKCre slice should show no search control. When reader-facing documentation pages +exist, local search is the default next step; Algolia requires separate scale or relevance +evidence. + +### 5. URL shape is an early contract; origin metadata follows the hostname + +Large sites commonly enable clean URLs, sitemap generation, canonical metadata, Open Graph +metadata, edit links, and last-updated timestamps. They do not all have the same activation +point: + +- public route shape, `base`, and internal-link conventions affect every page and should be + stable from the first build; +- Cloudflare Pages supports clean paths for the intended static output, so enable `cleanUrls` + in the first slice rather than migrating published links later; +- sitemap, canonical URLs, and `og:url` require the confirmed public hostname; +- `lastUpdated` requires useful Git history in the build checkout; +- edit links become useful when the site has reader-facing documentation pages. + +The initial index still needs correct language, title, description, responsive behavior, and +accessible default-theme interaction. + +### 6. Locale-ready source is cheaper than a later source-tree migration + +VitePress itself stores default English content under `docs/en/**`, rewrites it to the public +root, and places other locales under prefixes such as `/zh/`. This preserves root English URLs +while keeping source ownership explicit. + +InKCre should start with `website/content/en/**` and publish it at root, subject to Sir +confirming English as the default public language. Only the root locale should be active now. +A disposable build proved that VitePress does not fail when a locale switch points to a missing +translation, so a future locale needs route parity or a deliberate fallback-capable switch. + +### 7. Formatting, CI status, and dependency updates are lifecycle foundations + +Vue docs and VitePress establish formatting configuration and checks before their content +corpora grow. Vite and Vitest run documentation build or repository checks in CI. VitePress, +Vite, and neighboring InKCre packages also show least-privilege workflows, concurrency control, +frozen dependency installation, and automated dependency maintenance. + +These details are inexpensive in the first engineering slice and costly to retrofit: + +- add package-local EditorConfig and pinned formatter configuration; +- make formatting verification and the production build the deterministic `check`; +- use one always-reporting required workflow; +- avoid workflow-level path filters for a required check because a skipped workflow can remain + pending in GitHub; +- add a weekly updater scoped to `/website`, with no auto-merge, because the selected VitePress + version is a pinned prerelease. + +A dedicated external-link crawler is not a consistent baseline across the sample. Add one only +when outbound-link volume creates a demonstrated maintenance problem. + +The external sample establishes the need for an early formatting contract, not a mandatory +formatter brand. Execution-time inspection found that modern neighboring InKCre repositories +use Oxfmt, whose current language support includes Markdown and the site's configuration +formats. The implementation therefore reuses pinned Oxfmt rather than introducing Prettier as a +local exception. + +### 8. Generated content is justified by a canonical executable owner + +Vitest generates CLI documentation from CLI definitions. VueUse derives pages from function +metadata, source, types, and demos. UnoCSS shares generated package data with its docs and +interactive tools. Each generation graph exists because there are many uniform units and a +real executable owner. + +This does not justify a Hub-to-website mirror. InKCre has already accepted editorial projection, +and the current site has no repetitive executable documentation surface. If a future CLI, +schema, or API becomes canonical and produces many pages, add one narrow generator for that +surface and verify generated output freshness. + +## InKCre Engineering Baseline + +### Adopt in the first implementation slice + +- one independent `website/` package with its own exact tool declarations and lockfile; +- one explicit website-owned Markdown root through `srcDir: 'content'`, with default-language + source under `content/en/**` rewritten to public root routes; +- `base: '/'`, `cleanUrls: true`, extensionless public links, and stable route conventions; +- only the root locale until a second locale has route parity or safe fallback behavior; +- default VitePress theme, typed minimal configuration, and a simple index; +- package-local EditorConfig and pinned Oxfmt configuration; +- `dev`, `build`, `preview`, `format`, `format:check`, and deterministic `check` scripts; +- production build as the dead-link and static-rendering gate; +- one always-reporting GitHub Actions workflow after local verification, using frozen install, + least privilege, concurrency cancellation, timeout, and pinned action SHAs; +- a weekly dependency updater scoped to `/website`, without auto-merge; +- dependency audit as an explicit dependency-selection and update check; +- production-output assertions for root rewrite and clean-link behavior; +- manual wide/narrow viewport and keyboard review. + +### Add only when triggered + +- local search: when a reader-facing documentation corpus exists; +- edit links and last-updated data: when documentation pages warrant them; +- sitemap, canonical/Open Graph URLs, robots behavior, and deployment smoke: when the hostname + is confirmed; +- custom theme components: when a real brand or reader interaction cannot be expressed by the + default theme; +- generated pages: when an executable canonical owner and repetitive page family exist; +- source-controlled redirects: when the first published URL changes; +- version routing: when a real multi-version support obligation exists. + +### Do not include in the current plan + +- Algolia, PWA, offline caching, or browser E2E; +- Twoslash, Graphviz, tabs, group icons, or custom Markdown transforms; +- `vitepress-plugin-llms` or another website-derived agent corpus: the website is not InKCre's + canonical agent knowledge surface; +- a published second locale, translation tracking, version subdomains, or redirect fleets; +- contributor, sponsor, avatar, analytics, advertising, or marketing integrations; +- generated Hub mirrors, source-route manifests, hash/freshness gates, or global dead-link + suppression; +- product builds, playgrounds, interactive apps, or Spoke test matrices in the website pipeline. + +## Changes To The Prepared Plan + +The benchmark preserves the selected architecture and narrows the first slice: + +1. Keep `website/content` and the editorial projection boundary. +2. Put default-language source under `content/en/**`, rewrite it to public root routes, and + activate only the root locale. +3. Set the root deployment base, clean URLs, extensionless links, formatting contract, and + route conventions in the first slice. +4. Omit search from the one-page site; prefer local search when content expands. +5. Let VitePress production build enforce internal dead links, but add explicit output + assertions for route rewrites because the build does not prove locale parity. +6. Keep `check` deterministic. Run the network-dependent dependency audit explicitly during + dependency selection and updates instead of embedding it in every `check`. +7. Use `preview` for manual acceptance and later deployment smoke, not as a background process + inside the normal CI check. +8. Use an always-reporting CI workflow and a scoped weekly dependency updater from the first CI + slice. +9. Defer hostname-dependent metadata, not URL shape, until deployment is confirmed. + +## Primary Sources + +- [VitePress site configuration](https://vitepress.dev/reference/site-config) +- [VitePress routing and rewrites](https://vitepress.dev/guide/routing) +- [VitePress internationalization](https://vitepress.dev/guide/i18n) +- [VitePress asset handling](https://vitepress.dev/guide/asset-handling) +- [VitePress search](https://vitepress.dev/reference/default-theme-search) +- [VitePress deployment](https://vitepress.dev/guide/deploy) +- [VitePress's own locale-ready configuration](https://github.com/vuejs/vitepress/blob/430a890a17910593e26e9654b141ef3855ecceac/docs/.vitepress/config.ts) +- [Oxfmt language support](https://oxc.rs/docs/guide/usage/formatter/language-support) +- [Cloudflare Pages serving behavior](https://developers.cloudflare.com/pages/configuration/serving-pages/) +- [Cloudflare Pages redirects](https://developers.cloudflare.com/pages/configuration/redirects/) +- [GitHub Actions path-filter status behavior](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax) +- [Vue docs package](https://github.com/vuejs/docs/blob/7681134fd8505e61a265d161d73d28acb3c74822/package.json) +- [Vue docs configuration](https://github.com/vuejs/docs/blob/7681134fd8505e61a265d161d73d28acb3c74822/.vitepress/config.ts) +- [Vite docs package](https://github.com/vitejs/vite/blob/d17d739eedc4e8ede5973f1533826f27554c2078/docs/package.json) +- [Vite docs CI gate](https://github.com/vitejs/vite/blob/d17d739eedc4e8ede5973f1533826f27554c2078/.github/workflows/ci.yml) +- [Vitest docs package](https://github.com/vitest-dev/vitest/blob/eb16ab80330c7f33cc894eede76633191358d7b4/docs/package.json) +- [VueUse site configuration](https://github.com/vueuse/vueuse/blob/a7d9aba02985c8f71931b99ae903b458a8d652da/packages/.vitepress/config.ts) +- [VueUse Markdown transform](https://github.com/vueuse/vueuse/blob/a7d9aba02985c8f71931b99ae903b458a8d652da/packages/.vitepress/plugins/markdownTransform.ts) +- [UnoCSS site configuration](https://github.com/unocss/unocss/blob/e28a47c557fe179935a37a4fbeb650292d0d1d5a/docs/.vitepress/config.ts) +- [UnoCSS deployment configuration](https://github.com/unocss/unocss/blob/e28a47c557fe179935a37a4fbeb650292d0d1d5a/netlify.toml) diff --git a/tasks/vitepress-documentation-site/packet.md b/tasks/vitepress-documentation-site/packet.md new file mode 100644 index 0000000..89d1a3f --- /dev/null +++ b/tasks/vitepress-documentation-site/packet.md @@ -0,0 +1,154 @@ +# VitePress Documentation Site + +- **Objective**: establish and publish an independent VitePress website at `https://inkcre.dev` + as a human-facing projection of the InKCre shared-doc Hub. Keep only a simple index page while + the product is unstable, and preserve the future public-site information architecture without + prematurely writing productized guidance. +- **Guardrails**: keep this repository authoritative for shared product and cross-unit truth; + keep the VitePress project isolated under `website/`; do not turn the current index into a + marketing landing page; do not create placeholder User Manual, Developer Guide, or About + sections; do not treat website wording as a replacement owner for canonical Hub truth; do not + add manifest, hash, or route machinery merely to eliminate editorial drift; do not expose + `tasks/**`, `AGENTS.md`, generated SVC integration, local configuration, or other Hub-local + collaboration state; keep Spoke-local implementation, deployment, and task truth in each + Spoke; preserve the existing Hub-first and isolated-commit workflow. +- **Verification**: `svc status --json` remains healthy and `svc init --agent codex --json` + remains a no-op; VitePress reads only website-owned content under `website/`; formatting and + the simple home page pass a deterministic production check with default dead-link failure; + route-output assertions prove that the default language stays at root and links remain clean; + manual preview passes; the selected dependency graph passes an explicit audit; canonical Hub + documents remain unchanged; GitHub deploys the exact checked artifact to Cloudflare Pages; + `inkcre.dev` serves HTTPS, canonical metadata, sitemap, robots, and a real `404`; Pages preview + origins remain `noindex`; `git diff --check` passes. +- **Current Truth**: the repository now has an independent `website/` package with only + website-owned Markdown routable. English source begins under `content/en/**` and rewrites to + clean public root routes; only the English root locale is active. The package pins Node + `22.22.3`, pnpm `11.17.0`, VitePress `2.0.0-alpha.18`, and Oxfmt `0.60.0` in package metadata + and its lockfile. The simple index describes active development, links to canonical Hub + sources, and creates no speculative sections. Its deterministic check formats, builds with + default dead-link failure, and verifies generated language and URL contracts. One + always-reporting GitHub Actions workflow protects the website contract with pinned actions, + frozen install, least privilege, concurrency cancellation, and timeout. Dependabot maintains + `/website` npm dependencies and root GitHub Actions weekly without auto-merge. Product + behavior and language remain in `10-prd/**`; cross-unit contracts remain in + `20-product-tdd/**`; Hub/Spoke operations remain in `00-meta/**`; `tasks/**` remains volatile. + The website is an editorial projection and no canonical Hub document changed. The repository + can consume the existing organization Cloudflare account and API-token secrets, owns + `CLOUDFLARE_PAGES_PROJECT=inkcre-website`, and has a GitHub `production` environment restricted + to the `main` branch. +- **Next Step**: publish through the normal branch, pull-request, and main-branch workflow path; + monitor the Pages deployment and verify `inkcre.dev`. + +## Supporting Material + +- [Implementation plan](implementation-plan.md) +- [Preflight evidence and projection decision](preflight.md) +- [Industry benchmark and selected engineering baseline](industry-benchmark.md) +- [Evolution foundation audit](foundation-audit.md) + +## Confirmed Direction + +- The repository remains the InKCre shared documentation Hub. +- `website/` is an independent presentation, content, and build surface. +- The initial website is documentation-first and has only a simple home page. +- Website content is an editorial projection of Hub truth, not a byte-identical rendering. +- Ordinary editing judgment and source links are sufficient; zero-drift automation is not a + requirement. +- The website may later become the unified InKCre public site, but current implementation must + not imply product maturity that does not exist. +- Responsive layout, basic accessibility, essential page metadata, dead-link checks, and a + website-local content root remain valid first-slice requirements. +- Search remains a future requirement but is omitted from the one-page site. Built-in local + search is the default when a reader-facing documentation corpus exists. +- The website publishes at `inkcre.dev`; the application remains independently owned at + `app.inkcre.dev`. + +## Confirmed Language Contract + +Sir confirmed: + +```text +English source: website/content/en/** +English public URLs: /** +future Chinese URLs: /zh/** +active locales now: English root only +``` + +This matches the current English Hub corpus and avoids a later English source-tree and URL +migration. + +Candidate future section namespaces are `/manual/`, `/developer/`, and `/about/`. They remain +unpublished and absent from navigation; confirm each before its first real page rather than +creating placeholders now. + +## Content Topology + +```text +authoritative Hub truth + 00-meta/** Hub/Spoke operating contracts + 10-prd/** product truth and vocabulary + 20-product-tdd/** cross-unit technical contracts + | + | editorial projection, summary, and links + v +website/ public content, VitePress configuration, theme, navigation, assets + | + v +static documentation site +``` + +The website projection must not make these routable: + +```text +tasks/** +AGENTS.md +.agents/** +docs/index.md +svc.json +svc.local.json +other generated or machine-local collaboration state +``` + +## Deferred Information Architecture + +These sections describe the intended long-term role of the site. They are preserved for future +product pressure and must not be scaffolded with speculative content now. + +### Landing + +- Remain a simple project and documentation index during rapid development. +- Later, when product claims and availability are stable, it may become a fuller public landing + page. + +### User Manual + +- Task-oriented guidance for actual user journeys. +- Likely progression: start, concepts, collect, organize, retrieve/use, extensions, + troubleshooting. +- Procedures must be derived from verified client behavior, not inferred from PRD or technical + contracts. + +### Developer Guide + +- Unified orientation for repository topology, architecture, shared runtime, extension + development, contracts, and contribution. +- Spoke-local commands, implementation mechanics, deployment, and recovery remain linked to + their Spoke owners rather than copied into the Hub. + +### About + +- Organization mission, principles, repositories, governance, licensing, contribution, and + contact information. +- Durable organization claims require an explicit owner before publication. + +## Projection Decision + +- VitePress compiles website-owned Markdown only. +- Hub documents remain canonical inputs for human editing judgment, not build-time inputs. +- A website page may summarize or reorganize Hub truth and should link to a canonical source + when that link materially helps readers or maintainers. +- Editorial drift is accepted as a normal consequence of serving different audiences. +- No exact source-to-route manifest, wrapper fleet, generated mirror, source hash gate, or + cross-surface freshness mechanism is required. +- When drift exposes a real product or contract mismatch, update the canonical Hub owner first + and adjust the relevant website page in the same task when it is in scope. diff --git a/tasks/vitepress-documentation-site/preflight.md b/tasks/vitepress-documentation-site/preflight.md new file mode 100644 index 0000000..c346463 --- /dev/null +++ b/tasks/vitepress-documentation-site/preflight.md @@ -0,0 +1,166 @@ +# VitePress Documentation Site Preflight + +Date: 2026-07-28 + +All experiments ran in a disposable `/tmp` fixture copied from the current Hub. No website, +package, configuration, CI, or canonical Hub document was changed in the working repository. + +## Decision Outcome + +Directly building canonical Hub Markdown into website pages was technically feasible but was +not selected. + +Sir accepted normal editorial drift between canonical Hub truth and a human-facing website. +The selected architecture keeps all VitePress Markdown under `website/`; Hub documents inform +website content through ordinary editing judgment, summaries, and links. There is no +source-to-route manifest, wrapper fleet, generated mirror, source hash gate, or cross-surface +freshness mechanism. + +This is the simpler and deeper boundary: + +```text +Hub owns shared product and cross-unit truth. +Website owns public presentation and reader-oriented structure. +``` + +When a website page depends materially on a Hub claim, a normal source link is sufficient. +When a real mismatch is discovered, update the canonical Hub owner first and then adjust the +website page when it is in scope. + +## Reusable Environment Evidence + +- Local runtime: Node `22.22.3`, npm `10.9.8`, pnpm `11.17.0`, macOS arm64. +- Neighboring InKCre packages already use Node `22.22.3`, package-local pnpm declarations, + `devEngines.runtime`, frozen installs, and aggregate `check` commands. +- Registry state during preflight: + - VitePress stable: `1.6.4`; + - VitePress `next`: `2.0.0-alpha.18`; + - Vite stable: `8.1.5`. +- VitePress 1.6.4 built the disposable fixture, but its resolved dependency graph reported + three moderate and one high vulnerability. +- VitePress 2.0.0-alpha.18 built and previewed successfully, and + `pnpm audit --audit-level high` reported no known vulnerabilities. +- The prerelease must be pinned exactly and re-evaluated at execution time; implementation must + not float on the `next` tag. + +## Findings That Shaped The Plan + +- Root `README.md` is a repository entry and links to Hub-local task state, so it should not be + reused as the public index. +- A minimal site-owned index avoids changing README or weakening dead-link checks. +- Keeping VitePress source and dependencies inside `website/` removes the parent-source module + resolution seam and makes Hub-local files unreachable by construction. +- Development-server HTTP status is not useful publication evidence because VitePress serves an + SPA shell for unknown paths; production build and preview remain the meaningful checks. +- The client-web Cloudflare Pages workflow contains application-specific artifact identity, + SPA, preview-cleanup, and database concerns. The documentation site should reuse only the + Node/pnpm and pinned-action conventions, not that workflow's complexity. + +## Route And Locale Evolution Experiment + +A second disposable fixture tested the source and URL model that would otherwise be expensive +to retrofit. It used VitePress `2.0.0-alpha.18` with: + +```text +srcDir: 'content' +base: '/' +cleanUrls: true +rewrites: { 'en/:rest*': ':rest*' } +locales.root: English +``` + +The source tree contained: + +```text +content/en/index.md +content/en/manual/index.md +content/en/manual/getting-started.md +``` + +The production build passed and emitted: + +```text +.vitepress/dist/index.html +.vitepress/dist/manual/index.html +.vitepress/dist/manual/getting-started.html +``` + +It emitted no `/en/` output, used `en-US` page language, and generated clean internal links +without `.html`. Adding a Chinese locale and `content/zh/**` generated `/zh/**` while English +remained at root. + +The negative test was more important: when the English leaf page had no Chinese counterpart, +the locale switch still linked to `/zh/manual/getting-started`, and the production build still +passed. Therefore: + +- default-language source should start under `content/en/**`; +- only the root locale should be active initially; +- a future locale requires route parity or a fallback-capable locale switch; +- production build remains the dead-link baseline but is not a translation-parity check. + +Cloudflare Pages' documented clean-path behavior supports enabling `cleanUrls` from the first +build. Hostname-dependent sitemap, canonical, social, and robots metadata can still wait until +the public origin is confirmed. + +## Selected Verification Baseline + +Implementation should verify: + +```text +pnpm --dir website install --frozen-lockfile +pnpm --dir website check +svc status --json +svc init --agent codex --json +git diff --check +``` + +The deterministic website `check` command should run formatting verification followed by the +VitePress production build and its default dead-link check. The network-dependent advisory +audit remains an explicit dependency-selection and update check. Production-output assertions +verify that the default language stays at root and generated links use the selected clean URL +contract. Preview remains a manual acceptance and later deployment-smoke tool rather than a +background process in the normal check. + +Canonical Hub directories remain outside VitePress's source root and therefore need no +publication manifest or hash comparison. + +## Execution Result + +The authorized implementation completed on 2026-07-28 with these exact local package pins: + +```text +Node: 22.22.3 +pnpm: 11.17.0 +VitePress: 2.0.0-alpha.18 +Oxfmt: 0.60.0 +``` + +Oxfmt replaced the planned Prettier dependency after execution-time inspection showed that +modern neighboring InKCre repositories already use Oxfmt and its official language support +covers Markdown and the website's configuration formats. `0.60.0` was selected instead of the +newly published `0.61.0`: pnpm's supply-chain policy required release-age exceptions for the +new version and its platform bindings, while `0.60.0` is already used by neighboring +repositories and needs no exception. + +Implemented surfaces: + +- independent package, lockfile, ignores, formatter, configuration, content, and generated-route + verifier under `website/`; +- a single English index rewritten from `content/en/index.md` to `/`; +- one always-reporting website contract workflow with pinned actions; +- weekly Dependabot coverage for `/website` and root GitHub Actions. + +Verification passed: + +- frozen installation; +- Oxfmt check; +- VitePress production build and default dead-link gate; +- generated output checks for root `en-US`, no `/en/`, and no internal `.md` or `.html` links; +- deliberate negative probes for `/en?probe=1` and `guide.html?probe=1`, both correctly rejected; +- high-severity dependency audit with no known vulnerabilities; +- desktop `1280x720` and mobile `390x844` browser acceptance; +- root `200`, missing-route `404`, responsive layout, accessible landmarks and controls, and no + browser console warnings or errors. + +The independent final review accepted the implementation. Its two URL-assertion edge cases were +resolved by parsing and normalizing generated local URLs before validation. diff --git a/website/.editorconfig b/website/.editorconfig new file mode 100644 index 0000000..8b857ca --- /dev/null +++ b/website/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = 100 diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000..c76bc00 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vitepress/cache/ +.vitepress/dist/ diff --git a/website/.oxfmtrc.json b/website/.oxfmtrc.json new file mode 100644 index 0000000..81354bd --- /dev/null +++ b/website/.oxfmtrc.json @@ -0,0 +1,18 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "arrowParens": "always", + "ignorePatterns": [ + "node_modules/**", + ".vitepress/cache/**", + ".vitepress/dist/**", + "pnpm-lock.yaml" + ], + "printWidth": 100, + "proseWrap": "always", + "semi": false, + "singleQuote": true, + "sortPackageJson": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false +} diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts new file mode 100644 index 0000000..3af1a52 --- /dev/null +++ b/website/.vitepress/config.mts @@ -0,0 +1,50 @@ +import { defineConfig, type HeadConfig } from 'vitepress' + +const description = + 'Public documentation for InKCre, an actively developed system for reusable information.' +const siteOrigin = 'https://inkcre.dev' + +export default defineConfig({ + srcDir: 'content', + base: '/', + cleanUrls: true, + title: 'InKCre', + description, + rewrites: { + 'en/:rest*': ':rest*', + }, + locales: { + root: { + label: 'English', + lang: 'en-US', + title: 'InKCre', + description, + }, + }, + sitemap: { + hostname: siteOrigin, + }, + transformPageData(pageData) { + const url = new URL(pageData.relativePath.replace(/(?:(^|\/)index)?\.md$/, '$1'), siteOrigin) + .href + const title = + pageData.title && pageData.title !== 'InKCre' ? `${pageData.title} | InKCre` : 'InKCre' + const pageDescription = pageData.description || description + + ;((pageData.frontmatter.head ??= []) as HeadConfig[]).push( + ['link', { rel: 'canonical', href: url }], + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:site_name', content: 'InKCre' }], + ['meta', { property: 'og:locale', content: 'en_US' }], + ['meta', { property: 'og:url', content: url }], + ['meta', { property: 'og:title', content: title }], + ['meta', { property: 'og:description', content: pageDescription }], + ['meta', { name: 'twitter:card', content: 'summary' }], + ['meta', { name: 'twitter:title', content: title }], + ['meta', { name: 'twitter:description', content: pageDescription }] + ) + }, + themeConfig: { + socialLinks: [{ icon: 'github', link: 'https://github.com/InKCre/docs' }], + }, +}) diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..563fa0c --- /dev/null +++ b/website/README.md @@ -0,0 +1,82 @@ +# InKCre Website + +This package builds the public InKCre documentation site. Its Markdown is a reader-oriented +projection of the canonical shared product truth in the repository root; it does not replace that +truth or make Hub-local collaboration files public. + +## Commands + +Run commands from the repository root: + +```sh +pnpm --dir website dev +pnpm --dir website check +pnpm --dir website preview +``` + +The deterministic `check` command verifies formatting, builds the production site with VitePress's +dead-link checks enabled, and checks the generated route contract. Dependency advisories remain a +separate, network-dependent check: + +```sh +pnpm --dir website audit --audit-level high +``` + +## Content And Route Contract + +- VitePress reads only `content/`. +- English source lives under `content/en/` and is published at the public root. +- Future Chinese source will live under `content/zh/` and be published under `/zh/`. +- Only English is active until the Chinese route set is complete or the locale switch has a + deliberate fallback. +- Section indexes use trailing-slash routes, such as `/manual/`. +- Leaf pages use lowercase ASCII kebab-case routes without an extension, such as + `/manual/getting-started`. +- Source ordering belongs in navigation configuration, not numeric filename prefixes. +- Published routes are compatibility contracts. Move one only with a direct permanent redirect. + +Internal Markdown links target rewritten public routes and omit `.md` and `.html`. Relative links +are resolved from the rewritten route, not the source file location. + +## Page Authoring + +- Keep exactly one H1 per page. +- Add a concise page `description` in frontmatter. +- Give headings explicit custom anchors only when another page or external consumer needs a durable + deep link. +- Keep current product behavior tentative where the canonical Hub does not claim stability. +- Link to a canonical Hub source when it materially helps readers or maintainers. +- Do not create empty pages or navigation for future User Manual, Developer Guide, or About + sections. + +Update canonical Hub truth first when a public page reveals a real product or cross-unit contract +mismatch. + +## Publication + +`Website checks` builds and uploads `inkcre-website-dist`. The separate `Pages deployment` workflow +accepts only a successful artifact for the exact current `main` commit, downloads that artifact +without rebuilding it, and deploys it to the `inkcre-website` Cloudflare Pages Direct Upload +project. + +The deployment controller idempotently owns: + +- production branch `main`; +- custom domain `inkcre.dev`; +- canonical, sitemap, robots, and Open Graph metadata; +- `noindex` headers on production and immutable `pages.dev` origins; +- smoke checks for HTTPS, root HTML, real `404`, sitemap, robots, and canonical behavior. + +GitHub provides the deployment inputs without copying credentials into this package: + +```text +organization secrets: + CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN + +repository variable: + CLOUDFLARE_PAGES_PROJECT=inkcre-website +``` + +The Cloudflare token requires Pages Write access. The application at `app.inkcre.dev` remains an +independent deployment. diff --git a/website/content/en/index.md b/website/content/en/index.md new file mode 100644 index 0000000..b0e17e6 --- /dev/null +++ b/website/content/en/index.md @@ -0,0 +1,19 @@ +--- +title: InKCre +description: Public documentation index for the actively developed InKCre project. +--- + +# InKCre + +InKCre is under active development. The project is being built to collect information, organize it +into a reusable base, and make that information available to later retrieval, indexing, and +downstream workflows. + +This site is currently a compact entry point to the project's shared product and technical +documentation. + +## Project documentation + +- [Product requirements](https://github.com/InKCre/docs/blob/main/10-prd/index.md) +- [Cross-unit technical design](https://github.com/InKCre/docs/tree/main/20-product-tdd) +- [Documentation source](https://github.com/InKCre/docs) diff --git a/website/content/public/_headers b/website/content/public/_headers new file mode 100644 index 0000000..e8e8936 --- /dev/null +++ b/website/content/public/_headers @@ -0,0 +1,5 @@ +https://inkcre-website.pages.dev/* + X-Robots-Tag: noindex + +https://:version.inkcre-website.pages.dev/* + X-Robots-Tag: noindex diff --git a/website/content/public/robots.txt b/website/content/public/robots.txt new file mode 100644 index 0000000..1cb7083 --- /dev/null +++ b/website/content/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://inkcre.dev/sitemap.xml diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..574de7a --- /dev/null +++ b/website/package.json @@ -0,0 +1,27 @@ +{ + "name": "@inkcre/docs-website", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vitepress build", + "check": "pnpm format:check && pnpm build && pnpm verify:build", + "dev": "vitepress dev", + "format": "oxfmt .", + "format:check": "oxfmt --check .", + "preview": "vitepress preview", + "verify:build": "node scripts/verify-build.mjs" + }, + "devDependencies": { + "oxfmt": "0.60.0", + "vitepress": "2.0.0-alpha.18" + }, + "devEngines": { + "runtime": { + "name": "node", + "version": "22.22.3", + "onFail": "download" + } + }, + "packageManager": "pnpm@11.17.0" +} diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml new file mode 100644 index 0000000..f4b44bf --- /dev/null +++ b/website/pnpm-lock.yaml @@ -0,0 +1,1624 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + node: + specifier: runtime:22.22.3 + version: runtime:22.22.3 + oxfmt: + specifier: 0.60.0 + version: 0.60.0 + vitepress: + specifier: 2.0.0-alpha.18 + version: 2.0.0-alpha.18(postcss@8.5.23) + +packages: + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@docsearch/css@4.6.3': + resolution: {integrity: sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ==} + + '@docsearch/js@4.6.3': + resolution: {integrity: sha512-qUIX2b4Apew3tv4F0qhmgShsl/Lfw4m6mqv/5/5dWNxwTcDdLMp2s3YwZ+NMGh3IKCg0pBaXm7Q5VdyU5Rj+cQ==} + + '@docsearch/sidepanel-js@4.6.3': + resolution: {integrity: sha512-grGSmvXzG0if+mrzdIKykvpIAuEQ9u0sEJ2eLRRCaQfJvsWqh2C2/aY04bIzWvDh7myi5rvl8D+tUNsVrjYQ3A==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@iconify-json/simple-icons@1.2.92': + resolution: {integrity: sha512-hR0ozxR97t1dzWw+esoxFijZ15gagt7EIgF3CNifu2yICXhS7gnun4Y+j+odJQtNSl7wvqMdoLbViIShwe/fdw==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@oxfmt/binding-android-arm-eabi@0.60.0': + resolution: {integrity: sha512-1q4q4Jc8FlOMVojEisyFAVyl8h1yawNv6phjgmhGVEDeyeOdsSnSr9x0+D4mOnEKvpO5L4mxKZ/DP9X6U3A/Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.60.0': + resolution: {integrity: sha512-tD41I6nCt9k8SQXft0CSjjU9jg6SwG7uMu7PxodSEHXl+GDW0868oy6tTtoJkyUze8YKFgTpz/k5LuPUnFiGLw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.60.0': + resolution: {integrity: sha512-TTpzPug96Zxdyb46KvTyIUQDdsqbumXh2TKG9C23PCT0kF7JkW56Z/quPuG9rqOFKQIi1gpRNZ7DX18LwxXPnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.60.0': + resolution: {integrity: sha512-CnOoWgQ7L+JL/YQaRJ+NyATciSfcftncm7y3kqyte1cGtFEGnStaCd1TAyrinkfQ7nRBfHrTs1/vTwUJr3WF2Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.60.0': + resolution: {integrity: sha512-ychJo7S3hZxdO6eDZ9zM6F2lM9fpJS3EKS5CAUSWyprdLYxTu4gbaUKV/VBPTcMJwQa2Bpo+643y3OJ537pihA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.60.0': + resolution: {integrity: sha512-36IH5o55T2Fx7E0feDttt+mifxN6yk9pWv4KfhAIsP0dFnUq27331OwbpOsZdoXF9soOLWm7mQUz5+UUmyec4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.60.0': + resolution: {integrity: sha512-G1Ve7lAa6sFBolVI2LWHfEAqy0YKh4vnioH8uYO9kAEdgM7mR40IksIx9/Zk4+vbYew/sGa4J9Q4tZ3n9gXDHA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.60.0': + resolution: {integrity: sha512-LTQdRBf6uzj/h7Xk6lKzbGD2hrF/fK4YI9LIN1c0509tPUn8wRa3mCmrFQpEWJPLYGFrLFFMTYW1Ljj6VqW2Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.60.0': + resolution: {integrity: sha512-2JMo3XPxMPx3hiqddSZYyaH+fKJm6cz0u8n1naYjP/CdOQOZW34i8lKBUfmbWiuFvd6KoYXLmhAyBuvojsYS7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.60.0': + resolution: {integrity: sha512-L3C+nBD13lr306tr/PjM3RMll+BVqgFrIgUyoeHuai5oueJrRLgO3j+GO5/Cbhtkf5PSlHYTI1JY7iqBd1qa6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.60.0': + resolution: {integrity: sha512-M4MsmvqlxFiPtSRGyBYQSZxchEf463AOyd+Dh4/9xDpjWBsRtDUTDMFN5EdHinjVK1/eDJQ8MLpcYjpYayaCnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.60.0': + resolution: {integrity: sha512-OH+9UskYuxRB+GxqdGkVN8f5UpwhqG8YscNo1wl8+KJ62cd7wZdGga6iGLJIf8kibF1WBwvlfDUx3cez/VXwFg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.60.0': + resolution: {integrity: sha512-y7AAFutt9wFWBFOAn6+BHaV39usZmcr3YYH2385f+NHgPNpIF9HpqKp0jgUxPaUOCyG3oaX5VhJduL1Nw164rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.60.0': + resolution: {integrity: sha512-yKZ9+CXAI+1RO5nH/4Z/9M6DAsfOzd5bw/gtWk81KB4mpalMaRRSXfouc5/tHxazDmBek55HNPepNYBgaCew0Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.60.0': + resolution: {integrity: sha512-bCUGaF6hJOYnQzLJdHLZbvGsOd5oSvGAyJhPAKum2uyLYUuXmP8vqg690DWi2hqcnIoYpqSqCrjzE5aiUAgwQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.60.0': + resolution: {integrity: sha512-GrUeZOvzP30ExxfCuQiyofuUGI+OmvAgFwOO5w5p9mGPlxcyuqI+6Sy9fAKFFfLQrqKYWFgc5sYA2Unj/29nPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.60.0': + resolution: {integrity: sha512-WD4Q954kUl2TDJV/6q7UnE2rlKk047kXLJsr4bJ2mXRaAqNXcmV3nwKUsGCc3mz/jYDBnXtJEaBErJEybK8iQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.60.0': + resolution: {integrity: sha512-HqDekjr8JXzVDUP1YthDZ1Y3CBEcuZT4WX3B+1kaxj8CvZA8Y2YhcEsXqoSop3tVsgjACxjnFQFDkBo0r/jq1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.60.0': + resolution: {integrity: sha512-tz78yhmGPKboTMHCHSaUqXK8JrmoSejgDcWeqAtg2s07ZGKQ3rH5Jn8NuXPGNG33CDbY2e9NoQWXIVEmKO21Rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@shikijs/core@4.3.1': + resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.3.1': + resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.3.1': + resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.3.1': + resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.3.1': + resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} + engines: {node: '>=20'} + + '@shikijs/themes@4.3.1': + resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} + engines: {node: '>=20'} + + '@shikijs/transformers@4.3.1': + resolution: {integrity: sha512-z6ir0bGDgWcF2FduktEfPgIsdOtIlDiLAjFBgBzE42Q9xHbkkIXZtORHzlLVB71iZP9elEcqKg6keajvOUwE2A==} + engines: {node: '>=20'} + + '@shikijs/types@4.3.1': + resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@vitejs/plugin-vue@6.0.8': + resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.40': + resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} + + '@vue/compiler-dom@3.5.40': + resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==} + + '@vue/compiler-sfc@3.5.40': + resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==} + + '@vue/compiler-ssr@3.5.40': + resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==} + + '@vue/devtools-api@8.2.1': + resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} + + '@vue/devtools-kit@8.2.1': + resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==} + + '@vue/devtools-shared@8.2.1': + resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==} + + '@vue/reactivity@3.5.40': + resolution: {integrity: sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==} + + '@vue/runtime-core@3.5.40': + resolution: {integrity: sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==} + + '@vue/runtime-dom@3.5.40': + resolution: {integrity: sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==} + + '@vue/server-renderer@3.5.40': + resolution: {integrity: sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==} + + '@vue/shared@3.5.40': + resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} + + '@vueuse/core@14.3.0': + resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/integrations@14.3.0': + resolution: {integrity: sha512-76I5FT2ESvCmCaSwapI+a/u/CFtNXmzl9f9lNp1hRtx8vKB8hfiokJr8IvQqcQG5ckGXElyXK516b54ozV3MvA==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 || ^8 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@14.3.0': + resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==} + + '@vueuse/shared@14.3.0': + resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==} + peerDependencies: + vue: ^3.5.0 + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + focus-trap@8.2.2: + resolution: {integrity: sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node@runtime:22.22.3: + resolution: + type: variations + variants: + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-QAJqz3ioCOQ9IioLDg5xG5X/J5NGlreSW1Mpi8fIxgo= + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-aix-ppc64.tar.gz + targets: + - cpu: ppc64 + os: aix + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-Daf/dO+GETKMghLxeUM2hxOirZU/t9iajIoOrofCMgc= + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-darwin-arm64.tar.gz + targets: + - cpu: arm64 + os: darwin + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-RYMLp1L6DYksbc1kCUZmmAEpPKyCCjNZHe1ArAdRmOw= + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-darwin-x64.tar.gz + targets: + - cpu: x64 + os: darwin + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-zIvIKy3QtZXDuVpMPJyMNQkHz/ARr73uPRN56BLh4+M= + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-linux-arm64.tar.gz + targets: + - cpu: arm64 + os: linux + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-GPvhv91CBFrxXwIlauPllJDRuf+T38J6IjrMLJJ0AlA= + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-linux-armv7l.tar.gz + targets: + - cpu: armv7l + os: linux + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-qQ9SPPFk4cdhv0xZtcaqDZ1VKJJwGraUmeaD7h/Aa7w= + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-linux-ppc64le.tar.gz + targets: + - cpu: ppc64le + os: linux + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-S4TkPCnZDk+QGBXDmRBqSa9O5zDRKTOvgsgtTcRQMIw= + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-linux-s390x.tar.gz + targets: + - cpu: s390x + os: linux + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-x6ENaBbajqqnU03XPHHG4rLDkdu/hF42SQLRVmFd0bg= + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-linux-x64.tar.gz + targets: + - cpu: x64 + os: linux + - resolution: + archive: zip + bin: + node: node.exe + integrity: sha256-AL4SmgnohyzVLTu4u6EkEsVzPSIkEjpIKi3KSm+/JYY= + prefix: node-v22.22.3-win-arm64 + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-win-arm64.zip + targets: + - cpu: arm64 + os: win32 + - resolution: + archive: zip + bin: + node: node.exe + integrity: sha256-bI1U9jX+/033bCyoD0UzLrL/V9JSJu3ONlkuUaF37jM= + prefix: node-v22.22.3-win-x64 + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-win-x64.zip + targets: + - cpu: x64 + os: win32 + - resolution: + archive: zip + bin: + node: node.exe + integrity: sha256-e6QmD2nha6libQd8sRJPP7AfYEIa8qbHOWrrTi0Nja4= + prefix: node-v22.22.3-win-x86 + type: binary + url: https://nodejs.org/download/release/v22.22.3/node-v22.22.3-win-x86.zip + targets: + - cpu: x86 + os: win32 + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-Cch6y6+uZeGPzA6xiDdsElTKt03HaRGPS2BHnF+jJs8= + type: binary + url: https://unofficial-builds.nodejs.org/download/release/v22.22.3/node-v22.22.3-linux-arm64-musl.tar.gz + targets: + - cpu: arm64 + os: linux + libc: musl + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-0idpJYPWrhfEQCj9jxfLGzZyFnprqgNYGCQMu5226ko= + type: binary + url: https://unofficial-builds.nodejs.org/download/release/v22.22.3/node-v22.22.3-linux-x64-musl.tar.gz + targets: + - cpu: x64 + os: linux + libc: musl + version: 22.22.3 + hasBin: true + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + oxfmt@0.60.0: + resolution: {integrity: sha512-fViX6i+gJuZWY+jI/fnR6WRbRj70GZ9RlCd30MygJrHTUNc4DxvKHWw8vBjMjffv3PgU5qWDR0AzmojQByqaZA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + shiki@4.3.1: + resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} + engines: {node: '>=20'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + tabbable@6.5.0: + resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitepress@2.0.0-alpha.18: + resolution: {integrity: sha512-Lk1G2/QqSf+MwNLICl9fmzWOtoCEwjZoWgaQy41QvWTfcGpLE9XwJDbcCyES/9rj6R2g1zFqFvLVkYKLLDALFw==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vue@3.5.40: + resolution: {integrity: sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@docsearch/css@4.6.3': {} + + '@docsearch/js@4.6.3': {} + + '@docsearch/sidepanel-js@4.6.3': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@iconify-json/simple-icons@1.2.92': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.139.0': {} + + '@oxfmt/binding-android-arm-eabi@0.60.0': + optional: true + + '@oxfmt/binding-android-arm64@0.60.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.60.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.60.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.60.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.60.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.60.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.60.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.60.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.60.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.60.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.60.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.60.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.60.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.60.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.60.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.60.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.60.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.60.0': + optional: true + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@shikijs/core@4.3.1': + dependencies: + '@shikijs/primitive': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/primitive@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/transformers@4.3.1': + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/types': 4.3.1 + + '@shikijs/types@4.3.1': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.3': {} + + '@vitejs/plugin-vue@6.0.8(vite@8.1.5)(vue@3.5.40)': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.1.5 + vue: 3.5.40 + + '@vue/compiler-core@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.40 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.40': + dependencies: + '@vue/compiler-core': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/compiler-sfc@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.40 + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-ssr': 3.5.40 + '@vue/shared': 3.5.40 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.23 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.40': + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/devtools-api@8.2.1': + dependencies: + '@vue/devtools-kit': 8.2.1 + + '@vue/devtools-kit@8.2.1': + dependencies: + '@vue/devtools-shared': 8.2.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.2.1': {} + + '@vue/reactivity@3.5.40': + dependencies: + '@vue/shared': 3.5.40 + + '@vue/runtime-core@3.5.40': + dependencies: + '@vue/reactivity': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/runtime-dom@3.5.40': + dependencies: + '@vue/reactivity': 3.5.40 + '@vue/runtime-core': 3.5.40 + '@vue/shared': 3.5.40 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.40': + dependencies: + '@vue/compiler-ssr': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/shared@3.5.40': {} + + '@vueuse/core@14.3.0(vue@3.5.40)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.3.0 + '@vueuse/shared': 14.3.0(vue@3.5.40) + vue: 3.5.40 + + '@vueuse/integrations@14.3.0(focus-trap@8.2.2)(vue@3.5.40)': + dependencies: + '@vueuse/core': 14.3.0(vue@3.5.40) + '@vueuse/shared': 14.3.0(vue@3.5.40) + vue: 3.5.40 + optionalDependencies: + focus-trap: 8.2.2 + + '@vueuse/metadata@14.3.0': {} + + '@vueuse/shared@14.3.0(vue@3.5.40)': + dependencies: + vue: 3.5.40 + + birpc@2.9.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + csstype@3.2.3: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + entities@7.0.1: {} + + estree-walker@2.0.2: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + focus-trap@8.2.2: + dependencies: + tabbable: 6.5.0 + + fsevents@2.3.3: + optional: true + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + minisearch@7.2.0: {} + + nanoid@3.3.16: {} + + node@runtime:22.22.3: {} + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + oxfmt@0.60.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.60.0 + '@oxfmt/binding-android-arm64': 0.60.0 + '@oxfmt/binding-darwin-arm64': 0.60.0 + '@oxfmt/binding-darwin-x64': 0.60.0 + '@oxfmt/binding-freebsd-x64': 0.60.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.60.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.60.0 + '@oxfmt/binding-linux-arm64-gnu': 0.60.0 + '@oxfmt/binding-linux-arm64-musl': 0.60.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.60.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.60.0 + '@oxfmt/binding-linux-riscv64-musl': 0.60.0 + '@oxfmt/binding-linux-s390x-gnu': 0.60.0 + '@oxfmt/binding-linux-x64-gnu': 0.60.0 + '@oxfmt/binding-linux-x64-musl': 0.60.0 + '@oxfmt/binding-openharmony-arm64': 0.60.0 + '@oxfmt/binding-win32-arm64-msvc': 0.60.0 + '@oxfmt/binding-win32-ia32-msvc': 0.60.0 + '@oxfmt/binding-win32-x64-msvc': 0.60.0 + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + property-information@7.2.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + shiki@4.3.1: + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/engine-javascript': 4.3.1 + '@shikijs/engine-oniguruma': 4.3.1 + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + tabbable@6.5.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@2.1.0: {} + + trim-lines@3.0.1: {} + + tslib@2.8.1: + optional: true + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@8.1.5: + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.23 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + vitepress@2.0.0-alpha.18(postcss@8.5.23): + dependencies: + '@docsearch/css': 4.6.3 + '@docsearch/js': 4.6.3 + '@docsearch/sidepanel-js': 4.6.3 + '@iconify-json/simple-icons': 1.2.92 + '@shikijs/core': 4.3.1 + '@shikijs/transformers': 4.3.1 + '@shikijs/types': 4.3.1 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 6.0.8(vite@8.1.5)(vue@3.5.40) + '@vue/devtools-api': 8.2.1 + '@vue/shared': 3.5.40 + '@vueuse/core': 14.3.0(vue@3.5.40) + '@vueuse/integrations': 14.3.0(focus-trap@8.2.2)(vue@3.5.40) + focus-trap: 8.2.2 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 4.3.1 + vite: 8.1.5 + vue: 3.5.40 + optionalDependencies: + postcss: 8.5.23 + transitivePeerDependencies: + - '@types/node' + - '@vitejs/devtools' + - async-validator + - axios + - change-case + - drauu + - esbuild + - fuse.js + - idb-keyval + - jiti + - jwt-decode + - less + - nprogress + - qrcode + - sass + - sass-embedded + - sortablejs + - stylus + - sugarss + - terser + - tsx + - typescript + - universal-cookie + - yaml + + vue@3.5.40: + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-sfc': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/server-renderer': 3.5.40 + '@vue/shared': 3.5.40 + + zwitch@2.0.4: {} diff --git a/website/scripts/configure-pages.mjs b/website/scripts/configure-pages.mjs new file mode 100644 index 0000000..c29eb74 --- /dev/null +++ b/website/scripts/configure-pages.mjs @@ -0,0 +1,123 @@ +import { appendFile } from 'node:fs/promises' + +const apiToken = process.env.CLOUDFLARE_API_TOKEN +const accountId = process.env.CLOUDFLARE_ACCOUNT_ID +const projectName = process.env.CLOUDFLARE_PAGES_PROJECT +const domainName = process.env.CLOUDFLARE_PAGES_DOMAIN || 'inkcre.dev' +const stage = process.argv[2] || 'all' + +if (!apiToken || !accountId || !projectName) { + throw new Error('Cloudflare Pages configuration requires token, account, and project inputs') +} +if (!['all', 'project', 'domain'].includes(stage)) { + throw new Error(`Unknown Cloudflare Pages configuration stage: ${stage}`) +} + +const apiBase = `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages` + +async function request(path, init = {}) { + let lastError + + for (let attempt = 1; attempt <= 4; attempt += 1) { + try { + const response = await fetch(`${apiBase}${path}`, { + ...init, + headers: { + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/json', + ...init.headers, + }, + signal: AbortSignal.timeout(15000), + }) + const payload = await response.json() + + if (response.status === 429 || response.status >= 500) { + throw new Error(`Cloudflare API returned retryable HTTP ${response.status}`) + } + + return { response, payload } + } catch (error) { + lastError = error + if (attempt < 4) { + await new Promise((resolve) => setTimeout(resolve, attempt * 1000)) + } + } + } + + throw lastError +} + +function assertSuccess(operation, response, payload) { + if (!response.ok || !payload.success) { + const details = payload.errors?.map((error) => error.message).join('; ') || 'unknown error' + throw new Error(`${operation} failed with HTTP ${response.status}: ${details}`) + } +} + +const projectPath = `/projects/${encodeURIComponent(projectName)}` +let projectResult = await request(projectPath) + +if (projectResult.response.status === 404) { + projectResult = await request('/projects', { + method: 'POST', + body: JSON.stringify({ + name: projectName, + production_branch: 'main', + }), + }) + assertSuccess('Create Pages project', projectResult.response, projectResult.payload) +} else { + assertSuccess('Read Pages project', projectResult.response, projectResult.payload) +} + +if (projectResult.payload.result?.source) { + throw new Error(`${projectName} is not a Direct Upload Pages project`) +} + +if (projectResult.payload.result?.production_branch !== 'main') { + projectResult = await request(projectPath, { + method: 'PATCH', + body: JSON.stringify({ production_branch: 'main' }), + }) + assertSuccess('Set Pages production branch', projectResult.response, projectResult.payload) +} + +if (stage === 'project') { + console.log(`Cloudflare Pages project ${projectName} is configured.`) + process.exit(0) +} + +const domainPath = `${projectPath}/domains/${encodeURIComponent(domainName)}` +let domainResult = await request(domainPath) + +if (domainResult.response.status === 404) { + domainResult = await request(`${projectPath}/domains`, { + method: 'POST', + body: JSON.stringify({ name: domainName }), + }) + assertSuccess('Attach Pages custom domain', domainResult.response, domainResult.payload) +} else { + assertSuccess('Read Pages custom domain', domainResult.response, domainResult.payload) +} + +const domainStatus = domainResult.payload.result?.status +if (['blocked', 'deactivated', 'error'].includes(domainStatus)) { + throw new Error(`${domainName} entered unusable Cloudflare Pages status: ${domainStatus}`) +} + +console.log(`Cloudflare Pages project ${projectName} and domain ${domainName} are configured.`) + +if (process.env.GITHUB_STEP_SUMMARY) { + await appendFile( + process.env.GITHUB_STEP_SUMMARY, + [ + '## Cloudflare Pages configuration', + '', + `- Project: \`${projectName}\``, + `- Production branch: \`main\``, + `- Custom domain: \`${domainName}\``, + `- Domain status before delivery: \`${domainStatus}\``, + '', + ].join('\n') + ) +} diff --git a/website/scripts/verify-build.mjs b/website/scripts/verify-build.mjs new file mode 100644 index 0000000..81f49e3 --- /dev/null +++ b/website/scripts/verify-build.mjs @@ -0,0 +1,121 @@ +import { readdir, readFile } from 'node:fs/promises' +import { relative } from 'node:path' +import { fileURLToPath } from 'node:url' + +const distUrl = new URL('../.vitepress/dist/', import.meta.url) +const indexUrl = new URL('index.html', distUrl) +const canonicalOrigin = 'https://inkcre.dev' + +async function collectHtml(directoryUrl) { + const entries = await readdir(directoryUrl, { withFileTypes: true }) + const files = [] + + for (const entry of entries) { + const entryUrl = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directoryUrl) + + if (entry.isDirectory()) { + files.push(...(await collectHtml(entryUrl))) + } else if (entry.name.endsWith('.html')) { + files.push(entryUrl) + } + } + + return files +} + +function fail(message) { + throw new Error(`Website build contract failed: ${message}`) +} + +function publicPathForOutput(routePath) { + if (routePath === 'index.html') { + return '/' + } + + if (routePath.endsWith('/index.html')) { + return `/${routePath.slice(0, -'index.html'.length)}` + } + + return `/${routePath.slice(0, -'.html'.length)}` +} + +const indexHtml = await readFile(indexUrl, 'utf8').catch(() => { + fail('the public root did not produce index.html') +}) + +if (!indexHtml.includes('`)) { + fail('the public root has no canonical production URL') +} + +if (!indexHtml.includes(``)) { + fail('the public root has no Open Graph production URL') +} + +const sitemap = await readFile(new URL('sitemap.xml', distUrl), 'utf8').catch(() => { + fail('the production build did not emit sitemap.xml') +}) + +if (!sitemap.includes(`${canonicalOrigin}/`)) { + fail('the sitemap does not contain the canonical public root') +} + +const robots = await readFile(new URL('robots.txt', distUrl), 'utf8').catch(() => { + fail('the production build did not emit robots.txt') +}) + +if (!robots.includes(`Sitemap: ${canonicalOrigin}/sitemap.xml`)) { + fail('robots.txt does not advertise the canonical sitemap') +} + +const headers = await readFile(new URL('_headers', distUrl), 'utf8').catch(() => { + fail('the production build did not emit Cloudflare Pages headers') +}) + +if ( + !headers.includes('https://inkcre-website.pages.dev/*') || + !headers.includes('https://:version.inkcre-website.pages.dev/*') || + !headers.includes('X-Robots-Tag: noindex') +) { + fail('Cloudflare Pages origins are not protected from indexing') +} + +const htmlFiles = await collectHtml(distUrl) +const distPath = fileURLToPath(distUrl) + +for (const fileUrl of htmlFiles) { + const filePath = fileURLToPath(fileUrl) + const routePath = relative(distPath, filePath) + const html = await readFile(fileUrl, 'utf8') + + if (routePath === 'en/index.html' || routePath.startsWith('en/')) { + fail(`the default locale leaked into the public route: ${routePath}`) + } + + const pageUrl = new URL(publicPathForOutput(routePath), 'https://website.invalid') + + for (const match of html.matchAll(/\b(?:href|src)="([^"]+)"/g)) { + const generatedUrl = new URL(match[1], pageUrl) + + if (generatedUrl.origin !== pageUrl.origin) { + continue + } + + if (/^\/en(?:\/|$)/.test(generatedUrl.pathname)) { + fail(`the default locale leaked into a generated link in ${routePath}`) + } + + if (/\.(?:html|md)$/.test(generatedUrl.pathname)) { + fail(`a generated internal link exposes a source or output extension in ${routePath}`) + } + } +} + +console.log(`Verified ${htmlFiles.length} generated HTML files and the English root route.`) diff --git a/website/scripts/verify-pages-deployment.mjs b/website/scripts/verify-pages-deployment.mjs new file mode 100644 index 0000000..22e172b --- /dev/null +++ b/website/scripts/verify-pages-deployment.mjs @@ -0,0 +1,114 @@ +import { appendFile } from 'node:fs/promises' + +const deploymentId = process.env.CLOUDFLARE_PAGES_DEPLOYMENT_ID +const deploymentUrl = process.env.CLOUDFLARE_PAGES_DEPLOYMENT_URL +const canonicalOrigin = new URL(process.env.INKCRE_WEBSITE_ORIGIN || 'https://inkcre.dev') + +if (!deploymentId) { + throw new Error('Wrangler did not report a Cloudflare Pages deployment ID') +} +if (!deploymentUrl) { + throw new Error('Wrangler did not report a Cloudflare Pages deployment URL') +} + +const pagesOrigin = new URL(deploymentUrl) +if (pagesOrigin.protocol !== 'https:' || !pagesOrigin.hostname.endsWith('.pages.dev')) { + throw new Error(`Unexpected Cloudflare Pages deployment URL: ${pagesOrigin.origin}`) +} + +async function fetchPage(url) { + return fetch(url, { + redirect: 'error', + signal: AbortSignal.timeout(10000), + }) +} + +async function verifyOriginOnce(origin, expectNoindex) { + const rootResponse = await fetchPage(new URL('/', origin)) + const rootBody = await rootResponse.text() + + if (rootResponse.status !== 200) { + throw new Error(`${origin.origin}/ returned HTTP ${rootResponse.status}`) + } + if (!rootResponse.headers.get('content-type')?.startsWith('text/html')) { + throw new Error(`${origin.origin}/ did not return HTML`) + } + if (!rootBody.includes('`)) { + throw new Error(`${origin.origin}/ did not declare ${canonicalOrigin.href} as canonical`) + } + + const robotsHeader = rootResponse.headers.get('x-robots-tag') || '' + if (expectNoindex !== robotsHeader.toLowerCase().includes('noindex')) { + throw new Error(`${origin.origin}/ returned an unexpected X-Robots-Tag: ${robotsHeader}`) + } + + const missingResponse = await fetchPage(new URL('/__inkcre_pages_missing__', origin)) + if (missingResponse.status !== 404) { + throw new Error(`${origin.origin} missing route returned HTTP ${missingResponse.status}`) + } + + const sitemapResponse = await fetchPage(new URL('/sitemap.xml', origin)) + const sitemap = await sitemapResponse.text() + if (sitemapResponse.status !== 200 || !sitemap.includes(`${canonicalOrigin.href}`)) { + throw new Error(`${origin.origin}/sitemap.xml does not expose the canonical root`) + } + + const robotsResponse = await fetchPage(new URL('/robots.txt', origin)) + const robots = await robotsResponse.text() + if ( + robotsResponse.status !== 200 || + !robots.includes(`Sitemap: ${canonicalOrigin.href}sitemap.xml`) + ) { + throw new Error(`${origin.origin}/robots.txt does not expose the canonical sitemap`) + } +} + +async function verifyOrigin(origin, { attempts, delayMs, expectNoindex }) { + let lastError + + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + await verifyOriginOnce(origin, expectNoindex) + return + } catch (error) { + lastError = error + if (attempt < attempts) { + await new Promise((resolve) => setTimeout(resolve, delayMs)) + } + } + } + + throw lastError +} + +await verifyOrigin(pagesOrigin, { + attempts: 5, + delayMs: 1000, + expectNoindex: true, +}) + +await verifyOrigin(canonicalOrigin, { + attempts: 60, + delayMs: 10000, + expectNoindex: false, +}) + +console.log(`Verified Pages deployment ${deploymentId} and ${canonicalOrigin.origin}.`) + +if (process.env.GITHUB_STEP_SUMMARY) { + await appendFile( + process.env.GITHUB_STEP_SUMMARY, + [ + '## Cloudflare Pages deployment', + '', + `- Deployment ID: \`${deploymentId}\``, + `- Immutable URL: ${pagesOrigin.origin}`, + `- Canonical URL: ${canonicalOrigin.origin}`, + '- HTTPS, metadata, sitemap, robots, noindex, and 404 smoke: passed', + '', + ].join('\n') + ) +}