From e22307442777aa66787038c43ed6f37f9cbc1129 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Fri, 25 Sep 2026 16:46:53 +0100 Subject: [PATCH 1/2] fix: zod docs now have index.md --- justfile | 3 +-- package.json | 2 +- src/src/data/projects.yml | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/justfile b/justfile index 7943429..c190e0b 100644 --- a/justfile +++ b/justfile @@ -60,8 +60,7 @@ test-dist: bun run test:dist # Produce a production build (runs data sync first). -build: - bun run data:sync +build: live-data-sync bun run build # Full CI validation chain used by the shared build pipeline (data sync → typecheck → build → checks). diff --git a/package.json b/package.json index 528b335..a0da9a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "purview-dev", - "version": "0.3.1", + "version": "0.3.2", "private": true, "description": "Purview-Dev public website and unified documentation portal (workspace root).", "license": "MIT", diff --git a/src/src/data/projects.yml b/src/src/data/projects.yml index a306e03..28cecd7 100644 --- a/src/src/data/projects.yml +++ b/src/src/data/projects.yml @@ -134,7 +134,6 @@ projects: docs: source: github-path path: docs - rootPage: Getting-Started.md targetFrameworks: - net8.0 - net9.0 From c43f860a4a540959146e90e6391e36bdb36512da Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Fri, 25 Sep 2026 17:28:52 +0100 Subject: [PATCH 2/2] fix: docs now support correct root page mapping --- README.md | 29 ++++++++++++++++++++++++ src/src/data/projects.yml | 6 +++++ src/src/lib/docs/aggregate.ts | 14 +++++++++--- src/tests/unit/docs.test.ts | 42 ++++++++++++++++++++++++++++++++++- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e32fab4..99e933f 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,10 @@ the offending property, the expected shape, and a remediation hint. `readmeAsIndex: true`), - `source: wiki` for a GitHub wiki, - `source: readme` for projects documented by their README. + + Use `rootPage` to name the page served at `/docs//` and `exclude` + to drop files from the aggregation — see + [Documentation landing pages](#documentation-landing-pages). 4. Add relationships with `related`, or `supersededBy`/`supersedes` for archived projects. 5. Run `just validate` — the manifest schema, catalogue page, project page, @@ -224,10 +228,35 @@ docs (in-repo `docs/`, GitHub wikis via a shallow clone, or the README), then: - injects front matter (title, description, owners, status, last review date, source repository, edit URL), +- selects the page served at `/docs//` from `docs.rootPage`, otherwise + the conventional `index.md`/`Home.md`/`readme.md`, otherwise the repository + README when `readmeAsIndex: true`, - converts GitHub alert blockquotes to Starlight asides, - rewrites relative links and images so they resolve inside the portal, - honours `_Sidebar.md` ordering when the source provides it. +### Documentation landing pages + +`docs.exclude` removes source files from the aggregation *before* the landing +page is selected. That ordering is what keeps a designated `rootPage` in charge +when the repository also ships a conventional `index.md` — for example the +`index.md` stub required by a Backstage/TechDocs catalogue: + +```yaml +docs: + source: github-path + path: docs/wiki + rootPage: Getting-Started.md + exclude: + - _Sidebar.md + - index.md +``` + +Without the exclusion the aggregation raises a `DocsValidationError`: naming a +non-index page in `docs.rootPage` while an `index.md`/`Home.md`/`readme.md` +already exists is otherwise treated as a misconfiguration. Exclusion patterns +match the file's base name (not its repository path) and accept `*` wildcards. + Each documentation page shows its owning project, lifecycle status, staleness (last review older than 365 days, configurable in `src/lib/docs/staleness.ts`), and an edit link to the source repository. diff --git a/src/src/data/projects.yml b/src/src/data/projects.yml index 28cecd7..07d24bc 100644 --- a/src/src/data/projects.yml +++ b/src/src/data/projects.yml @@ -96,8 +96,11 @@ projects: docs: source: github-path path: docs/wiki + rootPage: Getting-Started.md exclude: - _Sidebar.md + - Home.md + - index.md targetFrameworks: - net8.0 - net9.0 @@ -134,6 +137,9 @@ projects: docs: source: github-path path: docs + rootPage: Getting-Started.md + exclude: + - index.md targetFrameworks: - net8.0 - net9.0 diff --git a/src/src/lib/docs/aggregate.ts b/src/src/lib/docs/aggregate.ts index 6e7519f..a58e3e6 100644 --- a/src/src/lib/docs/aggregate.ts +++ b/src/src/lib/docs/aggregate.ts @@ -171,7 +171,15 @@ function parseWikiSidebar(sidebar: string | undefined): string[] { return order; } -function isExcluded(fileName: string, exclude: string[] | undefined): boolean { +/** + * Whether a source file is removed by `docs.exclude`. Patterns are matched + * against the file's base name (`index.md`, `_Sidebar.md`) rather than its + * repository path, and `*` matches any run of characters. Exclusions are + * applied before the docs root page is selected, so excluding the conventional + * `index.md`/`Home.md`/`readme.md` is what lets `docs.rootPage` name a + * different landing page. + */ +export function isExcluded(fileName: string, exclude: string[] | undefined): boolean { return (exclude ?? []).some((pattern) => { if (pattern.includes('*')) { const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`); @@ -229,7 +237,7 @@ export function selectDocsRootFile( ` docs.rootPage: "${configuredRootPage}" selects a non-index page while a conventional root page already exists.`, ` Existing root page: ${relativeToDocRoot(defaultIndex.path, docRoot)}`, '', - 'Remediation: remove docs.rootPage or set it to the existing index.md/Home.md/readme.md root page.', + 'Remediation: remove docs.rootPage, point it at the existing index.md/Home.md/readme.md root page, or — when docs.rootPage must win — add that existing root page to docs.exclude (e.g. `exclude: [index.md]`).', ].join('\n'), ); } @@ -242,7 +250,7 @@ export function selectDocsRootFile( `Invalid docs configuration for ${projectId}`, ` docs: no root page exists under "${docRoot || '.'}".`, '', - 'Remediation: add index.md/Home.md/readme.md or set docs.rootPage to the markdown file that should render at /docs/{project}/.', + 'Remediation: add index.md/Home.md/readme.md, or set docs.rootPage to the markdown file that should render at /docs/{project}/ — required when docs.exclude removed the conventional root page.', ].join('\n'), ); } diff --git a/src/tests/unit/docs.test.ts b/src/tests/unit/docs.test.ts index c6371f8..358357d 100644 --- a/src/tests/unit/docs.test.ts +++ b/src/tests/unit/docs.test.ts @@ -3,7 +3,7 @@ import type { DocLinkContext } from '../../src/lib/docs/links'; import { describe, expect, test } from 'bun:test'; -import { DocsValidationError, selectDocsRootFile } from '../../src/lib/docs/aggregate'; +import { DocsValidationError, isExcluded, selectDocsRootFile } from '../../src/lib/docs/aggregate'; import { convertGithubAlerts, parseGithubAlerts } from '../../src/lib/docs/alerts'; import { extractDescription, @@ -194,6 +194,25 @@ describe('docs root page selection', () => { expect(selected.slugAliases.get('getting-started')).toBe('index'); }); + test('honours docs.exclude before selecting a configured non-index root', () => { + // `docs.exclude: [index.md]` drops the conventional root page before root + // selection runs, which is what lets docs.rootPage name a different landing + // page in repositories that ship a Backstage/TechDocs index.md. + const sourceFiles = [ + rawDoc('docs/index.md'), + rawDoc('docs/Getting-Started.md'), + rawDoc('docs/API.md'), + ]; + const included = sourceFiles.filter( + (file) => !isExcluded(file.path.split('/').pop() ?? '', ['index.md']), + ); + const selected = selectDocsRootFile('value-objects', included, 'docs', 'Getting-Started.md'); + + expect(selected.index.path).toBe('docs/Getting-Started.md'); + expect(selected.regular.map((file) => file.path)).toEqual(['docs/API.md']); + expect(selected.slugAliases.get('getting-started')).toBe('index'); + }); + test('rejects docs without a root page', () => { expect(() => selectDocsRootFile('demo', [rawDoc('docs/Guide.md')], 'docs', undefined)).toThrow( DocsValidationError, @@ -216,6 +235,27 @@ describe('docs root page selection', () => { ), ).toThrow(/conventional root page already exists/); }); + + test('points at the docs.exclude escape hatch when a conventional root exists', () => { + expect(() => + selectDocsRootFile( + 'demo', + [rawDoc('docs/index.md'), rawDoc('docs/Getting-Started.md')], + 'docs', + 'Getting-Started.md', + ), + ).toThrow(/docs\.exclude/); + }); +}); + +describe('docs exclude matching', () => { + test('matches base file names exactly and supports wildcards', () => { + expect(isExcluded('index.md', ['index.md'])).toBe(true); + expect(isExcluded('Index.md', ['index.md'])).toBe(false); + expect(isExcluded('_Sidebar.md', ['*.md'])).toBe(true); + expect(isExcluded('Getting-Started.md', ['index.md', '_Sidebar.md'])).toBe(false); + expect(isExcluded('index.md', undefined)).toBe(false); + }); }); describe('resolveDocRelativeLink', () => {