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
30 changes: 20 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## What This Is

Build-time aggregator that downloads pre-built static doc sites from GitHub Release artifacts, injects a persistent branded chrome (56px top nav), and produces a single static deployment. Served by nginx in Docker. Can also run as a web-fragment inside an Angular SSR gateway.
Build-time aggregator that downloads pre-built static doc sites from GitHub Release artifacts, wraps them in a persistent branded masthead, and produces a single static deployment. Served by nginx in Docker. Can also run as a web-fragment inside an Angular SSR gateway.

## Commands

Expand Down Expand Up @@ -44,7 +44,8 @@ apps.json (registry)
→ [1] Fetch Release artifacts OR build local repos → apps/{slug}/
→ [2] Copy non-HTML assets → public/{slug}/ (Astro serves as static)
→ [3] astro build: [...path].astro enumerates all HTML via getStaticPaths
→ transformSubAppHtml() rewrites URLs + injects chrome
→ transformSubAppHtml() rewrites URLs + splits the document,
which Base.astro then re-hosts (masthead + head/body)
→ dist/
```

Expand All @@ -59,34 +60,43 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`, `--path-p

### Core Data Flow

`apps.json` → `scripts/fetch-apps.js` downloads `dist.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 injects chrome → static output in `dist/`.
`apps.json` → `scripts/fetch-apps.js` downloads `dist.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/`.

### Key Source Files

- `src/pages/[...path].astro` — Catchall route, renders every sub-app page. Injects `<ClientRouter />` for Astro client-side transitions.
- `src/pages/[...path].astro` — Catchall route, renders every sub-app page through `Base.astro`
- `src/pages/index.astro` — Landing catalog page
- `src/utils/apps.js` — `getAppPages()` enumerates sub-app HTML (manifest-driven or filesystem crawl)
- `src/utils/transform.js` — `transformSubAppHtml()`: URL rewriting, chrome injection, headless transforms
- `src/templates/chrome.js` — Chrome HTML template + client-side SPA router script
- `src/utils/transform.js` — `transformSubAppHtml()`: URL rewriting, document splitting (head/body/title/body-class), headless transforms
- `src/layouts/Base.astro` — The one document shell: head, fonts, marketplace CSS, theme script, `<ClientRouter />`
- `src/components/Chrome.astro` — 56px fixed top bar (standalone mode only)
- `src/components/Masthead.astro` — Persistent Knowledge base header + Library/current-app sub-nav (all pages, both modes)
- `src/templates/chrome.js` — Inline theme script + shadow-DOM compat styles injected by the layout
- `scripts/build-vite.js` — Build orchestrator (3-step pipeline)
- `scripts/fetch-apps.js` — GitHub Release artifact downloader

### Two Modes

**Non-headless** (standalone): Injects chrome bar, SPA router, theme toggle. Client-side router intercepts navigation, fetches HTML, swaps DOM content.
Both modes render the same document: the masthead (`Masthead.astro`) — branding plus the Library / current-app sub-navigation — on every page, and nothing else chrome-like. There is no fixed top bar and no app switcher; the masthead is the navigation.

**Headless** (web-fragment): Strips dark mode class, marks `data-mp-headless="true"`, injects shadow DOM compat styles. No chrome bar. Designed for embedding in a web-fragments gateway.
**Non-headless** (standalone): Plain marketplace pages. Navigation is Astro's `<ClientRouter />` (view transitions).

**Headless** (web-fragment): Marks `data-mp-headless="true"` and injects shadow DOM compat styles. Designed for embedding in a web-fragments gateway.

### Light Only

The marketplace has no dark mode: no theme toggle, no persisted theme, no `dark` class, no dark palette. `transformSubAppHtml()` strips any sub-app theme bootstrap and `dark` body class so an embedding host's theme cannot bleed into the fragment.

### URL Rewriting

`transform.js` rewrites all relative and root-relative URLs to absolute `/{prefix}/{slug}/...` paths. Runs before chrome injection to prevent double-rewriting. Removes `<base>` tags.
`transform.js` rewrites all relative and root-relative URLs to absolute `/{prefix}/{slug}/...` paths. Runs before the document is split so nothing is double-rewritten. Removes `<base>` tags.

## Contract for Doc Apps

Apps registered in `apps.json` must comply with:
- `contract/schema.json` — JSON Schema for `marketplace.json` manifest
- `contract/HEADLESS_RULES.md` — Structural requirements (headless HTML, relative paths, `data-mp-headless` attribute)
- `contract/STYLE_GUIDE.md` — Design tokens, typography, dark mode support
- `contract/STYLE_GUIDE.md` — Design tokens and typography (light only — the marketplace has no dark mode)

## Testing

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# knowledge-base

> A unified documentation portal — wraps independently-maintained doc apps into a
> single deployable with a persistent branded chrome, and can be embedded as a
> single deployable with a persistent branded masthead, and can be embedded as a
> web fragment inside a host app.

---
Expand Down Expand Up @@ -279,12 +279,12 @@ knowledge-base/
│ │ ├── index.astro ← Landing catalog
│ │ └── [...path].astro ← Catch-all: renders every sub-app page
│ ├── layouts/Base.astro
│ ├── components/ ← AppCard, AppIcon, Chrome
│ ├── templates/chrome.js ← Chrome HTML + client SPA router
│ ├── components/ ← AppCard, AppIcon, Masthead
│ ├── templates/shadow-compat.js ← Shadow-DOM design-token styles
│ ├── styles/marketplace.css ← Design tokens + Tailwind
│ └── utils/
│ ├── apps.js ← getAppPages() page enumeration
│ └── transform.js ← URL rewriting + chrome/headless injection
│ └── transform.js ← URL rewriting + sub-app document splitting
├── scripts/
│ ├── build-vite.js ← Build orchestrator
│ ├── fetch-apps.js ← GitHub Release download + extract
Expand Down
5 changes: 3 additions & 2 deletions contract/HEADLESS_RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ At minimum the following CSS custom properties must be present:
--text-heading, --text-body, --text-muted
```

Copy the `@theme` block and `:root` / `.dark` variable declarations from
[`src/input.css`](../src/input.css) in this repository.
Copy the `@theme` block and `:root` variable declarations from
[`src/styles/marketplace.css`](../src/styles/marketplace.css) in this repository.
There is deliberately no `.dark` set — the marketplace is light-only.

---

Expand Down
30 changes: 11 additions & 19 deletions contract/STYLE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,16 @@ Ensure Inter is loaded from Google Fonts or bundled:

## Dark mode

Dark mode is **class-based**: add `class="dark"` to `<html>` (default in standalone builds).
The marketplace propagates the user's theme preference to all embedded apps.
**There is none.** The marketplace is light-only: no theme toggle, no persisted
preference, no dark palette.

Required dark-mode variable overrides (copy from `src/input.css`):
Do not ship a dark-mode bootstrap. When an app is integrated, the build strips any
`localStorage`-based theme script and removes a `dark` class from `<body>`, so a
dark theme would be dropped at integration time anyway — and inside a web fragment
it would clash with the host application's own theme.

```css
.dark {
--bg-page: #0a0d14;
--bg-card: #111827;
--bg-strong: #1f2937;
--bg-subtle: #1a2236;
--border: #1f2937;
--text-heading: #ffffff;
--text-body: #d1d5db;
--text-muted: #9ca3af;
}
```
An app may still define `.dark` styles for its **standalone** deployment; they will
simply never activate inside the marketplace.

---

Expand All @@ -77,9 +70,8 @@ Required dark-mode variable overrides (copy from `src/input.css`):
- Fixed left sidebar, **64px wide** (`w-64`)
- Background: `var(--bg-card)`
- Border right: `1px solid var(--border)`
- `id="sidebar"` required for marketplace chrome injection
- In **headless** mode: `top: 0` (marketplace chrome handles the offset)
- In **standalone** mode: `top: 56px` (offset by site header)
- `id="sidebar"` required so the marketplace can position it
- `top: 0` — the marketplace renders no fixed top bar, so nothing needs offsetting

---

Expand Down Expand Up @@ -151,7 +143,7 @@ padding: 0.75rem 1rem;

- [ ] Only brand color tokens used (no raw hex brand colors)
- [ ] Inter font loaded
- [ ] Both light and dark CSS variable sets defined
- [ ] Light CSS variable set defined (no dark set — the marketplace is light-only)
- [ ] `id="sidebar"` present on the left navigation
- [ ] `id="content"` or `<main>` wraps the prose area
- [ ] No inline styles that override design tokens with hardcoded values
97 changes: 0 additions & 97 deletions src/components/Chrome.astro

This file was deleted.

81 changes: 81 additions & 0 deletions src/components/Masthead.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
// src/components/Masthead.astro
//
// Persistent Knowledge base masthead. Rendered by every page through Base.astro
// (landing catalog, packaged sub-app pages, iframe entries) in both headless and
// standalone builds, so the branding and the way back to the catalog never
// disappear once a user opens a documentation app.
//
// Sub-navigation:
// • "Library" → the catalog of all documentation sites
// • a dynamic second entry naming the app currently being viewed
// The entry matching the current location is inert text with aria-current="page"
// rather than a link to itself.
import AppIcon from './AppIcon.astro';

interface App {
slug: string;
name: string;
icon?: string;
}

interface Props {
apps: App[];
activeSlug?: string;
base?: string;
/** True when the current page is the active app's own index (its crumb is then inert). */
appRoot?: boolean;
}

const { apps, activeSlug = '', base = '/knowledge-base', appRoot = false } = Astro.props;

const activeApp = apps.find(a => a.slug === activeSlug);
const onLibrary = !activeApp;
---

<div id="mp-masthead" class="mp-masthead">
<div class="mp-masthead-inner">
<div class="mp-masthead-eyebrow animate-fade-up">
<span class="mp-masthead-badge">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
</svg>
</span>
<span class="mp-masthead-kicker">Docs</span>
</div>

{/* On the catalog the masthead IS the page heading; on a documentation page the
sub-app supplies its own <h1>, so the masthead title stays non-heading text. */}
{onLibrary
? <h1 class="mp-masthead-title animate-fade-up delay-1">Knowledge base</h1>
: <p class="mp-masthead-title animate-fade-up delay-1">Knowledge base</p>
}

<p class="mp-masthead-desc animate-fade-up delay-2">
Browse and access all documentation sites. Each app is independently
maintained and deployed — one consistent experience.
</p>

<nav class="mp-masthead-nav" aria-label="Knowledge base sections">
{onLibrary
? <span class="mp-masthead-link active" aria-current="page">Library</span>
: <a class="mp-masthead-link" href={`${base}/`}>Library</a>
}

{activeApp && (appRoot
? (
<span class="mp-masthead-link active" aria-current="page">
<AppIcon icon={activeApp.icon || 'book-open'} size={14} />
<span>{activeApp.name}</span>
</span>
)
: (
<a class="mp-masthead-link active" href={`${base}/${activeApp.slug}/`}>
<AppIcon icon={activeApp.icon || 'book-open'} size={14} />
<span>{activeApp.name}</span>
</a>
)
)}
</nav>
</div>
</div>
Loading
Loading