From c91206ddff74fb2d2abacb11109ffecc2a3a788e Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 17 Sep 2026 17:35:28 +0200 Subject: [PATCH] fix(docs): restore GitHub Pages assets and navigation Use the public Pages origin and /scope base for production and PR builds, prefix internal Markdown and MDX links, and repair API reference landing links. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/static.yml | 8 +- website/AGENTS.md | 29 ++++- website/README.md | 32 ++++- website/astro.config.mjs | 10 +- website/package.json | 1 + .../docs/guides/prioritizing-requests.mdx | 2 +- .../docs/guides/submitting-requests-api.mdx | 4 +- .../src/content/docs/reference/rest-api.md | 6 +- website/src/content/docs/resources/faq.md | 2 +- website/src/plugins/remark-base-path.mjs | 38 ++++++ website/src/plugins/remark-base-path.test.mjs | 123 ++++++++++++++++++ 11 files changed, 233 insertions(+), 22 deletions(-) create mode 100644 website/src/plugins/remark-base-path.mjs create mode 100644 website/src/plugins/remark-base-path.test.mjs diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index ca6213942..452dcd2e8 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -54,14 +54,18 @@ jobs: astro-${{ runner.os }}- - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline + - name: Test site plugins + run: pnpm test - name: Setup Pages id: pages if: github.ref == 'refs/heads/main' uses: actions/configure-pages@v5 - name: Build site env: - SITE: ${{ steps.pages.outputs.origin }} - BASE_PATH: ${{ steps.pages.outputs.base_path }} + # The Pages API can report an isolated hostname instead of the public URL. + # Use the public project URL for both pull-request and production builds. + SITE: https://microsoft.github.io + BASE_PATH: /scope run: pnpm run build - name: Upload artifact if: github.ref == 'refs/heads/main' diff --git a/website/AGENTS.md b/website/AGENTS.md index 3b2cee212..9d6e68c0b 100644 --- a/website/AGENTS.md +++ b/website/AGENTS.md @@ -15,8 +15,11 @@ A static documentation site published to GitHub Pages. `website/`) - Deployed by `.github/workflows/static.yml` (build + deploy jobs), which builds from `website/` via a `working-directory` default and - `website/**` path filters; `site` and `base` are driven by - `actions/configure-pages` outputs with safe localhost defaults + `website/**` path filters; both PR and production builds explicitly use + `SITE=https://microsoft.github.io` and `BASE_PATH=/scope`. + Do not derive these from `actions/configure-pages` outputs, which can + report an isolated hostname instead of the public project URL. + Local development keeps localhost and `/` defaults. ## Where things live @@ -29,6 +32,8 @@ A static documentation site published to GitHub Pages. - `src/plugins/remark-http-snippets.mjs` — custom remark plugin that expands fenced ` ```http ` blocks into multi-language Starlight `` (curl, JS fetch, Python, Go, Java, C#) +- `src/plugins/remark-base-path.mjs`: prefixes internal Markdown URLs and + literal MDX `href`/`src` attributes with the configured deployment base - `astro.config.mjs` — sidebar, plugins, `markdown.remarkPlugins`, `starlight-openapi` config - `dist/` — build output (gitignored) @@ -125,6 +130,8 @@ Plain JSON examples (response shapes, profile config) stay as ### REST API reference page +- Link to `/reference/api/` for the generated reference landing page. + `/reference/api/operations/` is not a page. - **Auto-generated** per-endpoint pages live under `/reference/api/...` (built from `src/openapi/scope-openapi.json` by `starlight-openapi`). @@ -132,6 +139,13 @@ Plain JSON examples (response shapes, profile config) stay as with cross-links to the auto-generated pages and the live Swagger. Don't duplicate the per-endpoint detail there. +### Internal links + +Use site-root paths such as `/getting-started/access/` in Markdown links +and literal MDX `href`/`src` attributes. The base-path remark plugin adds +`/scope` in the public build while keeping local root deployments working. +Do not hard-code the deployment prefix in content or code examples. + ### Sidebar Sidebar order is set in `astro.config.mjs`. Adding a new page @@ -154,15 +168,18 @@ groups are spread via `...openAPISidebarGroups`. ```sh pnpm install +pnpm test # plugin regressions, using Node's built-in test runner pnpm run build # writes dist/ pnpm run dev # local preview at http://localhost:4321 pnpm run refresh:openapi # generate the OpenAPI snapshot from scope-core ``` -A green `pnpm run build` is the gate. As of the last edit it produces -**164 pages** (≈ 23 hand-written + the rest auto-generated from the -OpenAPI spec). A drop in page count usually means a content collection -file failed to parse. +Both `pnpm test` and `pnpm run build` must pass. The public build uses +`SITE=https://microsoft.github.io BASE_PATH=/scope pnpm run build`; +exercise that configuration when changing links or deployment settings, +not just the local `/` default. The current snapshot produces **200 +pages**, including the generated API reference. An unexpected drop in +page count can indicate a content collection file failed to parse. ## Workflow diff --git a/website/README.md b/website/README.md index 157407545..a54ad0414 100644 --- a/website/README.md +++ b/website/README.md @@ -24,6 +24,7 @@ The product and this documentation site live in │ │ └── resources/ │ ├── openapi/scope-openapi.json # artifact generated from the API registry │ ├── plugins/ +│ │ ├── remark-base-path.mjs # applies the deployment base to internal links │ │ └── remark-http-snippets.mjs # turns ```http blocks into multi-language tabs │ └── content.config.ts ├── astro.config.mjs # sidebar, plugins, starlight-openapi config @@ -42,12 +43,21 @@ Sidebar order is defined in `astro.config.mjs`, not by directory order. | `pnpm dev` | Start local dev server at `localhost:4321` | | `pnpm build` | Build the production site to `./dist/` | | `pnpm preview` | Preview the production build locally | +| `pnpm test` | Test site plugins with Node's built-in test runner | | `pnpm refresh:openapi` | Generate the OpenAPI snapshot from `scope-core` | ## Authoring docs - Use `.md` for plain Markdown, `.mdx` whenever the page contains JSX (e.g. Starlight ``). +- Write internal Markdown links and literal MDX `href`/`src` attributes + relative to the site root, such as `/getting-started/access/`. + [src/plugins/remark-base-path.mjs](src/plugins/remark-base-path.mjs) + adds the configured base path at build time. Do not hard-code `/scope` + in content. External URLs, relative links, fragments, and code examples + are left unchanged. +- Link to `/reference/api/` for the generated API reference landing page. + `/reference/api/operations/` has endpoint pages beneath it, but no index. - Write HTTP examples as a single fenced ` ```http ` block — the custom remark plugin in [src/plugins/remark-http-snippets.mjs](src/plugins/remark-http-snippets.mjs) @@ -71,9 +81,25 @@ where to look in scope-core for any given topic. Pushed builds deploy to GitHub Pages via [../.github/workflows/static.yml](../.github/workflows/static.yml). The workflow builds from this `website/` directory (via a -`working-directory` default and `website/**` path filters). `site` -and `base` are driven by `actions/configure-pages` outputs with safe -localhost defaults. +`working-directory` default and `website/**` path filters). Both pull-request +and production builds use `SITE=https://microsoft.github.io` and +`BASE_PATH=/scope`, matching the public +[documentation URL](https://microsoft.github.io/scope/). +`actions/configure-pages` still configures deployment, but its reported +hostname and base path are not used to generate URLs: it can report an +isolated Pages hostname instead of the public project URL. + +Local development defaults to the site root (`/`). To reproduce the +public deployment locally, run these commands from `website/`: + +```sh +pnpm test +SITE=https://microsoft.github.io BASE_PATH=/scope pnpm build +SITE=https://microsoft.github.io BASE_PATH=/scope pnpm preview +``` + +Open `/scope/` on the preview server. Keep `BASE_PATH` the same for the +build and preview so assets, navigation, and search use the same URLs. ## Learn more diff --git a/website/astro.config.mjs b/website/astro.config.mjs index a8d4ae9a6..e6d7d4179 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -8,12 +8,13 @@ import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { execSync } from 'node:child_process'; +import remarkBasePath from './src/plugins/remark-base-path.mjs'; import remarkHttpSnippets from './src/plugins/remark-http-snippets.mjs'; // https://astro.build/config // `site` and `base` are driven by the GitHub Pages deployment URL in CI -// (set via env vars from `actions/configure-pages` outputs), with safe -// defaults for local development. +// (set explicitly in the deployment workflow), with safe defaults +// for local development. // // `DOC_PORT` is read from the `.env` file that `worktree-env` writes to // the git repo root (so each worktree binds to a unique dev/preview @@ -47,13 +48,14 @@ function readDotEnv(name) { return undefined; } const docPort = Number(readDotEnv('DOC_PORT')) || 4300; +const base = process.env.BASE_PATH || '/'; export default defineConfig({ site: process.env.SITE || `http://localhost:${docPort}`, - base: process.env.BASE_PATH || '/', + base, server: { port: docPort }, markdown: { - remarkPlugins: [remarkHttpSnippets], + remarkPlugins: [[remarkBasePath, { base }], remarkHttpSnippets], }, integrations: [ starlight({ diff --git a/website/package.json b/website/package.json index 776e612d4..513b77f94 100644 --- a/website/package.json +++ b/website/package.json @@ -8,6 +8,7 @@ "start": "worktree-env && astro dev", "build": "astro build", "preview": "worktree-env && astro preview", + "test": "node --test src/plugins/*.test.mjs", "astro": "astro", "refresh:openapi": "cd .. && pnpm --filter api generate:openapi" }, diff --git a/website/src/content/docs/guides/prioritizing-requests.mdx b/website/src/content/docs/guides/prioritizing-requests.mdx index 1916af4a0..779b36220 100644 --- a/website/src/content/docs/guides/prioritizing-requests.mdx +++ b/website/src/content/docs/guides/prioritizing-requests.mdx @@ -30,7 +30,7 @@ queue position updates as soon as you save. The Portal exposes Priority is editable while a request is **pending** or **paused**. The exact endpoint shape (single vs bulk) is in the -[REST API reference](/reference/api/operations/). At a glance: +[REST API reference](/reference/api/). At a glance: - Single-request priority change. - Bulk priority change for a list of request IDs. diff --git a/website/src/content/docs/guides/submitting-requests-api.mdx b/website/src/content/docs/guides/submitting-requests-api.mdx index 2a1a533a1..1d7c18db8 100644 --- a/website/src/content/docs/guides/submitting-requests-api.mdx +++ b/website/src/content/docs/guides/submitting-requests-api.mdx @@ -25,7 +25,7 @@ https://your-scope.example.com/api-docs An auto-generated reference of every endpoint, built from the same OpenAPI spec, also lives in this site at -[REST API reference](/reference/api/operations/). +[REST API reference](/reference/api/). This page covers the common workflows. @@ -201,7 +201,7 @@ idempotency on top by caching the returned `id` if you need it. `profileId`) so a later edit to the profile doesn't change what your pipeline runs. - **Poll sparingly**: prefer the SSE log stream to status polling. -- **Use the reference**: the [REST API reference](/reference/api/operations/) +- **Use the reference**: the [REST API reference](/reference/api/) is the source of truth for exact request and response shapes. Your deployment may also provide Swagger UI at `/api-docs`. diff --git a/website/src/content/docs/reference/rest-api.md b/website/src/content/docs/reference/rest-api.md index 4da51a13b..0176a117c 100644 --- a/website/src/content/docs/reference/rest-api.md +++ b/website/src/content/docs/reference/rest-api.md @@ -6,7 +6,7 @@ description: Overview of the Scope REST API and pointer to the auto-generated en The Scope REST API mirrors what you can do in the Portal and the `scope` CLI. The full per-endpoint reference is **auto-generated from the committed OpenAPI snapshot** and lives under -[REST API reference](/reference/api/operations/). +[REST API reference](/reference/api/). For interactive exploration, your deployment may provide Swagger UI and a raw OpenAPI document: @@ -75,7 +75,7 @@ Generate and fetch per-request evaluation reports. The API also exposes `agents`, `models`, `mcp-servers`, `skills`, `extensions`, `insights`, and `report-templates`. See the generated -[REST API reference](/reference/api/operations/) for endpoint-level +[REST API reference](/reference/api/) for endpoint-level details. ## Request status & outcome @@ -118,5 +118,5 @@ published reference. ## See also -- [REST API reference (auto-generated)](/reference/api/operations/) +- [REST API reference (auto-generated)](/reference/api/) - [Submitting requests (REST API)](/guides/submitting-requests-api/) diff --git a/website/src/content/docs/resources/faq.md b/website/src/content/docs/resources/faq.md index 1e3828a05..4f59149b7 100644 --- a/website/src/content/docs/resources/faq.md +++ b/website/src/content/docs/resources/faq.md @@ -77,6 +77,6 @@ profiles, and more — all from the terminal. See ## Where's the API reference? -The auto-generated [REST API reference](/reference/api/operations/) is +The auto-generated [REST API reference](/reference/api/) is built from the committed OpenAPI snapshot. A narrative overview of the resource groups is at the [REST API overview](/reference/rest-api/). diff --git a/website/src/plugins/remark-base-path.mjs b/website/src/plugins/remark-base-path.mjs new file mode 100644 index 000000000..dbd487f7b --- /dev/null +++ b/website/src/plugins/remark-base-path.mjs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { visit } from 'unist-util-visit'; + +export default function remarkBasePath({ base = '/' } = {}) { + const prefix = `/${base.replace(/^\/+|\/+$/g, '')}`; + + function withBase(url) { + if (!url.startsWith('/') || url.startsWith('//')) return url; + const pathname = url.split(/[?#]/, 1)[0]; + if (pathname === prefix || pathname.startsWith(`${prefix}/`)) return url; + return `${prefix}${url}`; + } + + return function transformer(tree) { + if (prefix === '/') return; + + visit(tree, (node) => { + if (node.type === 'link' || node.type === 'image' || node.type === 'definition') { + node.url = withBase(node.url); + } + + // MDX anchors and images keep literal attributes outside Markdown link nodes. + if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') { + for (const attribute of node.attributes) { + if ( + attribute.type === 'mdxJsxAttribute' && + (attribute.name === 'href' || attribute.name === 'src') && + typeof attribute.value === 'string' + ) { + attribute.value = withBase(attribute.value); + } + } + } + }); + }; +} diff --git a/website/src/plugins/remark-base-path.test.mjs b/website/src/plugins/remark-base-path.test.mjs new file mode 100644 index 000000000..ebc692751 --- /dev/null +++ b/website/src/plugins/remark-base-path.test.mjs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import remarkBasePath from './remark-base-path.mjs'; + +function transform(children, base = '/scope') { + const tree = { type: 'root', children }; + remarkBasePath({ base })(tree); + return tree; +} + +test('prefixes Markdown links, images, and reference definitions', () => { + const tree = transform([ + { type: 'link', url: '/getting-started/access/', children: [] }, + { type: 'image', url: '/images/scope.svg', alt: 'Scope' }, + { type: 'definition', identifier: 'api', url: '/reference/api/?view=full#requests' }, + { type: 'link', url: '/', children: [] }, + ]); + + assert.deepEqual(tree.children.map((node) => node.url), [ + '/scope/getting-started/access/', + '/scope/images/scope.svg', + '/scope/reference/api/?view=full#requests', + '/scope/', + ]); +}); + +test('prefixes literal MDX href and src attributes, including nested links', () => { + const tree = transform([ + { + type: 'mdxJsxFlowElement', + name: 'section', + attributes: [], + children: [ + { + type: 'mdxJsxTextElement', + name: 'a', + attributes: [{ type: 'mdxJsxAttribute', name: 'href', value: '/guides/defining-criteria/' }], + children: [], + }, + { + type: 'mdxJsxFlowElement', + name: 'img', + attributes: [{ type: 'mdxJsxAttribute', name: 'src', value: '/images/scope.svg' }], + children: [], + }, + ], + }, + ]); + + assert.equal(tree.children[0].children[0].attributes[0].value, '/scope/guides/defining-criteria/'); + assert.equal(tree.children[0].children[1].attributes[0].value, '/scope/images/scope.svg'); +}); + +test('leaves external, relative, fragment, and already-prefixed URLs unchanged', () => { + const urls = [ + 'https://example.com/reference/', + '//example.com/reference/', + 'mailto:docs@example.com', + '../reference/', + '#requests', + '?view=full', + '', + '/scope', + '/scope/', + '/scope?view=full', + '/scope#requests', + '/scope/reference/api/', + ]; + const tree = transform(urls.map((url) => ({ type: 'link', url, children: [] }))); + + assert.deepEqual(tree.children.map((node) => node.url), urls); +}); + +test('does not mistake a similar path for the configured base', () => { + const tree = transform([{ type: 'link', url: '/scope-other/', children: [] }]); + assert.equal(tree.children[0].url, '/scope/scope-other/'); +}); + +test('preserves code examples, MDX expressions, spreads, and unrelated attributes', () => { + const children = [ + { type: 'code', lang: 'http', value: 'GET /api/v1/requests' }, + { type: 'inlineCode', value: '/api/v1/requests' }, + { + type: 'mdxJsxTextElement', + name: 'a', + attributes: [ + { type: 'mdxJsxAttribute', name: 'href', value: { type: 'mdxJsxAttributeValueExpression', value: 'url' } }, + { type: 'mdxJsxExpressionAttribute', value: '...props' }, + { type: 'mdxJsxAttribute', name: 'title', value: '/reference/' }, + { type: 'mdxJsxAttribute', name: 'download', value: null }, + ], + children: [], + }, + ]; + const original = structuredClone(children); + transform(children); + + assert.deepEqual(children, original); +}); + +test('normalizes base paths and only prefixes URLs once', () => { + for (const base of ['/scope', '/scope/', 'scope', '/nested/scope/']) { + const tree = transform([{ type: 'link', url: '/reference/api/', children: [] }], base); + remarkBasePath({ base })(tree); + const prefix = base.includes('nested') ? '/nested/scope' : '/scope'; + assert.equal(tree.children[0].url, `${prefix}/reference/api/`); + } +}); + +test('leaves root deployments unchanged', () => { + for (const base of ['/', '']) { + const children = [{ type: 'link', url: '/reference/api/', children: [] }]; + const original = structuredClone(children); + transform(children, base); + assert.deepEqual(children, original); + } + const tree = { type: 'root', children: [{ type: 'link', url: '/reference/api/', children: [] }] }; + remarkBasePath()(tree); + assert.equal(tree.children[0].url, '/reference/api/'); +});