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
21 changes: 12 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`.

### Core Data Flow

`apps.json` → `scripts/fetch-apps.js` downloads `kb-docs.tar.gz` per app `apps/{slug}/` → Astro's `src/pages/[...path].astro` catchall uses `getStaticPaths()` from `src/utils/apps.js` to enumerate every HTML file → `src/utils/transform.js` rewrites URLs and splits the document → `Base.astro` re-hosts the parts → static output in `dist/`.
`apps.json` (a source per entry) → `scripts/build-vite.js` stages each artifact (downloading via `scripts/fetch-apps.js` for a `repo`) → reads its `kb-docs.json` → copies one directory per declared app into `apps/{slug}/` → Astro's `src/pages/[...path].astro` catchall uses `getStaticPaths()` from `src/utils/apps.js` to enumerate every HTML file → `src/utils/transform.js` rewrites URLs and splits the document → `Base.astro` re-hosts the parts → static output in `dist/`.

### Key Source Files

Expand All @@ -76,21 +76,23 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`.
- `src/components/Masthead.astro` — Persistent Knowledge base header + Library/current-app sub-nav (all pages, both modes)
- `src/components/AppCard.astro`, `src/components/AppIcon.astro` — Catalog card and its icon
- `src/templates/shadow-compat.js` — Shadow-DOM design-token styles, injected into the body by the layout
- `src/utils/config.js` — `PATH_PREFIX`/`BASE_PATH` and `isHeadlessBuild()` — the build-wide constants
- `src/utils/single-page.js` — Bundle manifest reading/validation + registry expansion, shared by both fetch paths and by Astro
- `src/utils/config.js` — `PATH_PREFIX`/`BASE_PATH`, `isHeadlessBuild()` and `REGISTRY_FILE` — the build-wide constants
- `src/utils/registry.js` — Registry validation, manifest reading/validation, expansion map. Shared by the build and by Astro so both resolve the same registry
- `scripts/build-vite.js` — Build orchestrator (4 steps: prepare, hoist, copy assets, astro build)
- `scripts/fetch-apps.js` — GitHub Release artifact downloader
- `scripts/fetch-apps.js` — GitHub Release artifact downloader. Only *obtains* an artifact; installing it is one shared path in `build-vite.js`
- `scripts/artifacts.js` — Safe tarball extraction + tree copy, shared by both fetch paths. Validates archive members (no traversal, no absolute paths, no symlinks) before anything is written, and replaces the old `cp -r`/`tar` shell-outs so the build runs on Windows
- `scripts/hoist-inline-scripts.js` — Moves inline `<script>` bodies in sub-app HTML into files before the Astro build, so the deployment can serve `script-src 'self'`. Needed because bundles published before the action stopped emitting an inline mermaid bootstrap still contain one. A sub-app's dark-mode bootstrap is deleted here rather than hoisted — light only, and hoisting would put it beyond the reach of `transform.js`
- `actions/publish-single-page-docs/` — Reusable GitHub Action that turns a repo's markdown into a single-page bundle

### Three Onboarding Types
### Onboarding Types

An `apps.json` entry is one of:
A registry entry names a **source** (`repo` + optional `version`, `prebuilt`, or `localPath`) and nothing else — no slug, name, description, icon or tags. Those come from the artifact's `kb-docs.json`, and the build rejects an entry that carries them. The one exception is an `iframe` entry, which has no artifact to read them from.

The registry file is `apps.json` by default; `KB_REGISTRY` points the build at another one, which is how a deployment repo owns its own list.

- **default (packaged)** — a repo publishes a headless static site as `kb-docs.tar.gz` carrying a `kb-docs.json` manifest. Every HTML file becomes a route unless the manifest lists `pages`.
- **`type: "iframe"`** — no artifact; a single route renders a full-viewport `<iframe>` for an external URL. Explicit stopgap (issue #10).
- **`type: "single-page"`** — one release artifact holding *many* docs, published by `actions/publish-single-page-docs` from plain markdown. The entry carries **no per-doc metadata** (`{ "repo": …, "type": "single-page", "version": "latest" }`); the build reads the artifact's `kb-docs.json` and **expands** the entry into one app per doc, extracting each into `apps/{slug}/`. The expansion is recorded in `apps/.single-page.json` and spliced back into the registry by `loadRegistry()` so Astro sees the same registry the build did. Slugs must be globally unique — `resolveRegistry()` fails the build otherwise. Rendering: masthead, no sidebar, content in a centred `main.kb-single-page` reading column. See issue #35 and `contract/SINGLE_PAGE.md`.
- **markdown bundles** — an artifact published by `actions/publish-single-page-docs` from plain markdown, holding one doc per app. Structurally identical to a packaged site: same asset name, same manifest, same install path. Rendering differs only where the artifact does — an app whose directory holds exactly one HTML file and declares no `pages` is rendered in a centred `main.kb-single-page` reading column with no sidebar. See issue #35 and `contract/SINGLE_PAGE.md`.

### Two Modes

Expand Down Expand Up @@ -128,7 +130,7 @@ Apps registered in `apps.json` must comply with:
Self-contained Playwright E2E — `npm test` auto-starts everything (no external gateway):

1. **:3000 fragment** — `scripts/setup-test-apps.mjs` writes a hermetic `apps.json` that
registers the vendored `tests/fixtures/docs-example.dist.tar.gz` twice
registers the vendored `tests/fixtures/docs-example.kb-docs.tar.gz` (two apps)
(slugs `user-guide` + `guide-mirror`, for cross-app nav), an iframe entry pinned
`"headless": false`, and the generated single-page bundle fixture. `build:headless` builds it;
`tests/fragment-server.mjs` serves `dist/` mirroring the production **nginx** rewrites
Expand Down Expand Up @@ -159,7 +161,7 @@ commands listed in `AGENTS.md`:
- `support/fragment.js` — shadow-DOM traversal + reframed-body wait/query helpers.

Two build-pipeline pieces support this: `apps.json` entries may carry a `prebuilt` path
(tarball or dist dir) consumed by `scripts/build-vite.js` (`preparePrebuilt`) for hermetic
(tarball or unpacked directory) consumed by `scripts/build-vite.js` (`stageEntry`) for hermetic
offline builds; and the build copies the knowledge base stylesheet — identified as the local
stylesheet the landing page loads — to a stable `dist/style.css` alias. Pages themselves
reference the content-hashed bundle Astro injects, so nothing depends on that filename.
Expand All @@ -173,6 +175,7 @@ in the committed `apps.json` without breaking CI, which only has this repo.
## Environment Variables

- `GITHUB_TOKEN` — GitHub API auth for fetching Release artifacts
- `KB_REGISTRY` — registry file to build from, relative to the project root. Default `apps.json`. Read through `REGISTRY_FILE` in `src/utils/config.js`, never inline.
- `KB_HEADLESS` — `true` produces web-fragment output; **anything else, including unset, means standalone**. `scripts/build-vite.js` always exports an explicit value, so the default only applies when `astro build`/`astro dev` runs directly. Read it through `isHeadlessBuild()`, never inline. A per-app `"headless"` in `apps.json` overrides it in either direction.
- `AWS_REGION`, `ECR_REPOSITORY`, `ECS_CLUSTER`, `ECS_SERVICE` — deployment config
- `KB_EXAMPLE_ARTIFACT` — overrides the packaged artifact `scripts/setup-test-apps.mjs` registers
Expand Down
38 changes: 21 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,30 @@ route patterns hard-code the same string.

## The registry: `apps.json`

Each entry registers one doc app. A `slug` is required; the source is one of
`repo` (+ optional `version`), `localPath`, `prebuilt`, or an `iframe` URL:
Each entry says **where an artifact comes from, and nothing else**. The slug,
name, description, icon, tags and page list all come from the artifact's own
`kb-docs.json` (see [`contract/ARTIFACT.md`](contract/ARTIFACT.md)), so one entry
may register several apps and onboarding a new doc never edits this repository
again. The build rejects an entry that carries display fields.

```jsonc
[
{
"slug": "my-app",
"name": "My App Documentation",
"description": "What this app documents.",
"icon": "book-open",
"tags": ["guide"],

// pick ONE source:
"repo": "AbsaOSS/my-docs", "version": "latest", // GitHub Release artifact
// "localPath": "../my-docs", // build from local checkout
// "prebuilt": "tests/fixtures/my-docs.kb-docs.tar.gz" // prebuilt tarball or dist dir
}
// pick ONE source per entry:
{ "repo": "AbsaOSS/my-docs", "version": "latest" }, // GitHub Release artifact
{ "localPath": "../my-docs" }, // pack from a local checkout
{ "prebuilt": "tests/fixtures/my-docs.kb-docs.tar.gz" } // tarball or unpacked directory
]
```

`version` accepts `latest` (the default) or a pinned tag. `localPath` runs the
checkout's pack command — `npm run pack:kb` unless the entry sets `"pack"` — and
then reads the `kb-docs.tar.gz` it leaves behind; `"pack": false` skips that when
the artifact is already built. An entry may also set `"headless"` to pin one app
against the build flag.

`KB_REGISTRY` points the build at a different registry file, which is how a
deployment repository owns its own list without forking this one.

Add `"optional": true` to a `prebuilt`/`localPath` entry whose artifact lives
outside this repo — a sibling checkout, say. The build then **skips it with a
warning** when the artifact is absent instead of failing, so CI and fresh clones
Expand Down Expand Up @@ -170,7 +174,7 @@ to headless HTML — GFM tables, task lists, footnotes, highlighted code and
vendored-mermaid diagrams — packs every doc into one `kb-docs.tar.gz` with a
`kb-docs.json` manifest, and attaches it to the repo's latest release.

The registry entry then carries **no per-doc metadata at all**:
The registry entry is the same two lines every artifact gets:

```jsonc
{
Expand Down Expand Up @@ -209,7 +213,7 @@ own apps for a real deployment.
## Testing

E2E tests use Playwright. Everything is hermetic — built from
`tests/fixtures/docs-example.dist.tar.gz` (no network, token, or sibling repo).
`tests/fixtures/docs-example.kb-docs.tar.gz` (no network, token, or sibling repo).

| Command | Layer |
|---|---|
Expand Down Expand Up @@ -371,7 +375,7 @@ knowledge-base/
│ └── utils/
│ ├── apps.js ← loadRegistry() + getAppPages() page enumeration
│ ├── config.js ← PATH_PREFIX / BASE_PATH + isHeadlessBuild()
│ ├── single-page.js manifest parsing + registry expansion
│ ├── registry.js ← registry rules + manifest parsing + expansion
│ └── transform.js ← parse5 URL rewriting + sub-app document splitting
├── scripts/
│ ├── build-vite.js ← Build orchestrator
Expand Down
25 changes: 2 additions & 23 deletions apps.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
[
{
"slug": "user-guide",
"name": "User Guide",
"description": "Primary docs app — the vendored docs-example fixture used as the integration guinea pig.",
"icon": "book-open",
"tags": [
"guide",
"getting-started"
],
"prebuilt": "tests/fixtures/docs-example.dist.tar.gz"
},
{
"slug": "guide-mirror",
"name": "Guide Mirror",
"description": "Second registered app (same artifact, different slug) for cross-app navigation tests.",
"icon": "book-open",
"tags": [
"mirror",
"cross-app"
],
"prebuilt": "tests/fixtures/docs-example.dist.tar.gz"
"prebuilt": "tests/fixtures/docs-example.kb-docs.tar.gz"
},
{
"slug": "external-docs",
Expand All @@ -35,12 +16,10 @@
"headless": false
},
{
"type": "single-page",
"prebuilt": "tests/fixtures/single-page-bundle"
},
{
"type": "single-page",
"prebuilt": "../knowledge-base-example-single-page/dist.tar.gz",
"prebuilt": "../knowledge-base-example-single-page/kb-docs.tar.gz",
"optional": true
}
]
2 changes: 1 addition & 1 deletion scripts/artifacts.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* artifacts.js — safe extraction and copying of sub-app artifacts.
*
* Every artifact this build consumes is a `dist.tar.gz` produced by *another*
* Every artifact this build consumes is a `kb-docs.tar.gz` produced by *another*
* repository's release pipeline, so its contents are untrusted input: a
* compromised (or merely careless) doc repo must not be able to write outside
* the staging directory, and must not be able to smuggle a symlink into the
Expand Down
Loading