Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<project>/` 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,
Expand All @@ -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/<project>/` 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.
Expand Down
3 changes: 1 addition & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/src/data/projects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -135,6 +138,8 @@ projects:
source: github-path
path: docs
rootPage: Getting-Started.md
exclude:
- index.md
targetFrameworks:
- net8.0
- net9.0
Expand Down
14 changes: 11 additions & 3 deletions src/src/lib/docs/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '.*')}$`);
Expand Down Expand Up @@ -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'),
);
}
Expand All @@ -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'),
);
}
Expand Down
42 changes: 41 additions & 1 deletion src/tests/unit/docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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', () => {
Expand Down
Loading