Skip to content
Open
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
5 changes: 5 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ on:
push:
branches: [main]
workflow_dispatch:
# Docs are fetched from the product repos' HEADs at build time, so a
# scheduled run is the only thing that picks up doc changes that land
# there without a corresponding push to this repo.
schedule:
- cron: '17 9 * * *'

permissions:
contents: read
Expand Down
24 changes: 13 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

Static documentation site for Major Context products, built with Astro. Currently hosts documentation for [Moat](https://github.com/majorcontext/moat), with architecture designed to support multiple products.
Static documentation site for Major Context products, built with Astro. Hosts documentation for three products: [Moat](https://github.com/majorcontext/moat), [Keep](https://github.com/majorcontext/keep), and [Gatekeeper](https://github.com/majorcontext/gatekeeper).

**Key characteristic**: Documentation content is fetched from source repositories at build time, not stored in this repo.

Expand All @@ -38,6 +38,8 @@ bun run test:lighthouse # Run Lighthouse performance tests
```bash
bun run fetch:docs # Fetch all product documentation from GitHub
bun run fetch:moat # Fetch only Moat documentation
bun run fetch:keep # Fetch only Keep documentation
bun run fetch:gatekeeper # Fetch only Gatekeeper documentation
bun run check:assets # Verify required assets exist (logo.svg, favicons)
```

Expand All @@ -55,7 +57,7 @@ bun run validate:links # Check for broken internal/external links
3. **Link rewriting**: Markdown links (`../concepts/file.md`) are rewritten to Astro routes (`/moat/concepts/slug`)
4. **Static generation**: Astro builds static HTML with proper routing

**Critical**: Never manually edit files in `src/content/moat/` — they're overwritten on every build.
**Critical**: Never manually edit files in `src/content/{moat,keep,gatekeeper}/` — they're overwritten on every build.

### Multi-Product System

Expand All @@ -64,7 +66,7 @@ The site is architected to host documentation for multiple products:
- **Product registry**: `src/lib/products.ts` defines all products with their GitHub repos and doc paths
- **Content collections**: Each product gets its own collection in `src/content/config.ts`
- **Dynamic routing**: Pages use `[category]/[slug]` pattern to support any product structure
- **Navigation config**: `src/config/navigation.ts` defines sidebar structure (currently Moat-only)
- **Navigation config**: `src/config/navigation.ts` builds sidebar structure per product (`buildNavigation(docs, productId)`), shared across Moat, Keep, and Gatekeeper

To add a new product:
1. Add entry to `src/lib/products.ts`
Expand Down Expand Up @@ -114,7 +116,7 @@ BaseLayout.astro

`scripts/fetch-docs.ts` handles fetching docs from GitHub:

**Authentication**: Uses `gh` CLI (GitHub CLI). In CI, set `MOAT_DOCS_TOKEN` secret for private repos.
**Authentication**: Uses `gh` CLI (GitHub CLI). CI runs authenticated with the built-in `GH_TOKEN` (`github.token`) — sufficient since all three product repos are public; a broader-scoped token would be needed if a source repo went private.

**Fetching logic**:
1. Calls GitHub API via `gh api` to list directory contents
Expand Down Expand Up @@ -143,7 +145,7 @@ See `docs/style-guide.md` for complete guidelines. Key principles:
- Background: `stone-100` (warm paper)
- Sidebar: `stone-200`
- Text: `stone-800` / `stone-600` (muted)
- Accent: `sky-700` (links, active states)
- Accent: per-product (`sky-700` Moat, `amber-700` Keep, `emerald-700` Gatekeeper) for links and active states; `sky-700` is the site-wide default outside product pages
- Code blocks: `stone-900` background

**Spacing philosophy**: Prefer consistent Tailwind spacing (4, 6, 8, 12). Avoid arbitrary values except for specific design needs (e.g., tracking).
Expand All @@ -166,7 +168,7 @@ See `docs/style-guide.md` for complete guidelines. Key principles:

**Adding a navigation item**: Edit `src/config/navigation.ts` and add to appropriate section.

**Modifying page layout**: Edit `src/layouts/DocsLayout.astro` for global changes, or `src/pages/moat/[category]/[slug].astro` for content-specific changes.
**Modifying page layout**: Edit `src/layouts/DocsLayout.astro` for global changes, or `src/pages/{moat,keep,gatekeeper}/[category]/[slug].astro` for content-specific changes.

**Styling H2 headings**: All H2 styling is in DocsLayout's prose classes. Look for `prose-h2:` prefixes.

Expand All @@ -175,15 +177,15 @@ See `docs/style-guide.md` for complete guidelines. Key principles:
- Scroll offset: `scroll-mt-24` in DocsLayout prose classes
- Right padding to prevent overlap: `lg:pr-72` on main element

**Testing with real content**: Run `bun run fetch:moat` to pull latest Moat docs from GitHub, then `bun run dev`.
**Testing with real content**: Run `bun run fetch:docs` (or a per-product variant like `bun run fetch:moat`) to pull latest docs from GitHub, then `bun run dev`.

## CI/CD

GitHub Actions workflow in `.github/workflows/`:
GitHub Actions workflows in `.github/workflows/`:
- Runs validation on push and PRs
- Builds and deploys to GitHub Pages
- Requires `MOAT_DOCS_TOKEN` secret if source repos are private
- Builds and deploys to GitHub Pages on push to `main`, manual dispatch, and a daily schedule (`deploy.yml`) — the schedule exists because docs are fetched from the product repos' HEADs at build time, and nothing else triggers a rebuild when only a docs repo changes
- Uses the built-in `GH_TOKEN`; a custom token secret is only needed if a source repo becomes private

## OG Images

Dynamic OG (social share) images generated at `/og/[...path].png` using `@vercel/og`. Images render page title and Moat branding.
Dynamic OG (social share) images generated at `/og/[...path].png` using `@vercel/og`. Images render page title, description, and a per-product brand color (sky for Moat, amber for Keep, emerald for Gatekeeper; see `BRAND_COLORS` in `src/pages/og/[...path].png.ts`).
7 changes: 4 additions & 3 deletions src/lib/gatekeeper-llms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { SITE_URL, MAIN_CATEGORIES, OPTIONAL_CATEGORIES } from './llms-shared';
export const INTRO = `Gatekeeper is a standalone credential-injecting TLS-intercepting proxy. Route HTTPS \
traffic through Gatekeeper and it transparently injects authentication headers based on \
hostname matching — clients never see raw credentials. Pluggable credential sources cover \
environment variables, static values, AWS Secrets Manager, GCP Secret Manager, GitHub App \
tokens, and RFC 8693 token exchange. Includes an MCP relay, network policy enforcement, \
and OpenTelemetry observability. Open source, actively developed.`;
environment variables, static values, host commands, AWS Secrets Manager, GCP Secret Manager, \
GCP service account tokens, GitHub App tokens, and RFC 8693 token exchange. Includes an MCP \
relay, a Postgres data plane, LLM policy evaluation, network policy enforcement, and \
OpenTelemetry observability. Open source, actively developed.`;
29 changes: 29 additions & 0 deletions src/pages/gatekeeper/[category]/[slug].md.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
import { slugify } from '../../../utils/strings';
import { SITE_URL } from '../../../lib/gatekeeper-llms';

export async function getStaticPaths() {
const docs = await getCollection('gatekeeper');

return docs.map((doc) => {
const parts = doc.id.split('/');
const category = parts[0];
const fileName = parts[1];
const slug = slugify(fileName);

return {
params: { category, slug },
props: { doc },
};
});
}

const FOOTER = `\n\n---\n[← Gatekeeper documentation index](${SITE_URL}/gatekeeper/llms.txt)\n`;

export const GET: APIRoute = ({ props }) => {
const { doc } = props as { doc: Awaited<ReturnType<typeof getCollection<'gatekeeper'>>>[number] };
return new Response(doc.body + FOOTER, {
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
});
};
32 changes: 31 additions & 1 deletion src/pages/gatekeeper/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
<pre class="astro-code github-dark-dimmed mb-16 overflow-x-auto p-6 rounded-md" style="background-color:#22272e;color:#adbac7" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#768390"># Install</span></span>
<span class="line"><span style="color:#dcbdfb">$</span><span style="color:#96d0ff"> go install github.com/majorcontext/gatekeeper/cmd/gatekeeper@latest</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390"># ...or pull the container image</span></span>
<span class="line"><span style="color:#dcbdfb">$</span><span style="color:#96d0ff"> docker pull ghcr.io/majorcontext/gatekeeper:latest</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390"># Start the proxy</span></span>
<span class="line"><span style="color:#dcbdfb">$</span><span style="color:#96d0ff"> gatekeeper --config gatekeeper.yaml</span></span>
<span class="line"></span>
Expand Down Expand Up @@ -48,7 +51,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">03</div>
<h2 class="text-sm font-normal mb-1.5">Pluggable Sources</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Env vars and static values for development. AWS Secrets Manager, GCP Secret Manager, GitHub App tokens, and RFC 8693 token exchange for production.
Env vars and static values for development. Host commands, AWS Secrets Manager, GCP Secret Manager, GCP service account tokens, GitHub App tokens, and RFC 8693 token exchange for production.
</p>
</div>

Expand Down Expand Up @@ -96,6 +99,33 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
</div>
</div>

<h2 class="text-sm font-semibold uppercase tracking-widest text-stone-600 dark:text-stone-400 mb-4">Data Plane &amp; Policy</h2>
<div class="grid md:grid-cols-2 border border-stone-300 dark:border-stone-700 mb-16">
<div class="border-b border-stone-300 dark:border-stone-700 md:border-r py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">09</div>
<h2 class="text-sm font-normal mb-1.5">Postgres Data Plane</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
A second listener speaks the Postgres wire protocol, authenticating clients with their run token in place of a database password. Gatekeeper resolves the real per-branch Neon password and completes SCRAM-SHA-256 upstream.
</p>
</div>

<div class="border-b border-stone-300 dark:border-stone-700 py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">10</div>
<h2 class="text-sm font-normal mb-1.5">LLM Policy</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Anthropic API responses are evaluated against <a href="/keep" class="underline hover:text-emerald-700 dark:hover:text-emerald-400">Keep</a> policy rules before reaching the client. Denials are blocked fail-closed, including streamed SSE responses.
</p>
</div>

<div class="py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">11</div>
<h2 class="text-sm font-normal mb-1.5">Host Gateway</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Maps a synthetic in-container hostname to the host machine's real IP. Containers reach host services through the same credential injection and network policy as any other request.
</p>
</div>
</div>

<div class="mb-16">
<h2 class="text-sm font-semibold uppercase tracking-widest text-stone-600 dark:text-stone-400 pb-3 mb-4 border-b border-stone-300 dark:border-stone-700">Why This Exists</h2>

Expand Down
29 changes: 29 additions & 0 deletions src/pages/keep/[category]/[slug].md.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
import { slugify } from '../../../utils/strings';
import { SITE_URL } from '../../../lib/keep-llms';

export async function getStaticPaths() {
const docs = await getCollection('keep');

return docs.map((doc) => {
const parts = doc.id.split('/');
const category = parts[0];
const fileName = parts[1];
const slug = slugify(fileName);

return {
params: { category, slug },
props: { doc },
};
});
}

const FOOTER = `\n\n---\n[← Keep documentation index](${SITE_URL}/keep/llms.txt)\n`;

export const GET: APIRoute = ({ props }) => {
const { doc } = props as { doc: Awaited<ReturnType<typeof getCollection<'keep'>>>[number] };
return new Response(doc.body + FOOTER, {
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
});
};
23 changes: 21 additions & 2 deletions src/pages/og/[...path].png.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@ import { ImageResponse } from '@vercel/og';
interface Props {
title: string;
description: string;
brandColor: string;
}

// Tailwind `-700` hex values for each product's accent color (see
// src/lib/products.ts `color`), used for the OG image brand square.
// Falls back to Moat's sky-700 so the site homepage and any unrecognized
// path render identically to before this map existed.
const BRAND_COLORS: Record<string, string> = {
moat: '#0369a1', // sky-700
keep: '#b45309', // amber-700
gatekeeper: '#047857', // emerald-700
};
const DEFAULT_BRAND_COLOR = BRAND_COLORS.moat;

export async function getStaticPaths() {
const moatDocs = await getCollection('moat');
const keepDocs = await getCollection('keep');
Expand All @@ -19,6 +31,7 @@ export async function getStaticPaths() {
props: {
title: 'Major Context',
description: 'Safe infrastructure for AI agents',
brandColor: DEFAULT_BRAND_COLOR,
},
},
// Moat homepage
Expand All @@ -27,6 +40,7 @@ export async function getStaticPaths() {
props: {
title: 'Moat',
description: 'Let agents break things safely',
brandColor: BRAND_COLORS.moat,
},
},
// Keep homepage
Expand All @@ -35,6 +49,7 @@ export async function getStaticPaths() {
props: {
title: 'Keep',
description: 'Policy engine for AI agent tool calls',
brandColor: BRAND_COLORS.keep,
},
},
// Gatekeeper homepage
Expand All @@ -43,6 +58,7 @@ export async function getStaticPaths() {
props: {
title: 'Gatekeeper',
description: 'Credential-injecting TLS-intercepting proxy',
brandColor: BRAND_COLORS.gatekeeper,
},
},
// All Moat documentation pages
Expand All @@ -58,6 +74,7 @@ export async function getStaticPaths() {
props: {
title: doc.data.title,
description: doc.data.description || 'Moat Documentation',
brandColor: BRAND_COLORS.moat,
},
};
}),
Expand All @@ -74,6 +91,7 @@ export async function getStaticPaths() {
props: {
title: doc.data.title,
description: doc.data.description || 'Keep Documentation',
brandColor: BRAND_COLORS.keep,
},
};
}),
Expand All @@ -90,6 +108,7 @@ export async function getStaticPaths() {
props: {
title: doc.data.title,
description: doc.data.description || 'Gatekeeper Documentation',
brandColor: BRAND_COLORS.gatekeeper,
},
};
}),
Expand All @@ -99,7 +118,7 @@ export async function getStaticPaths() {
}

export const GET: APIRoute = async ({ props, params }) => {
const { title, description } = props as Props;
const { title, description, brandColor } = props as Props;
const pathStr = (params as { path: string }).path || 'home';
const footerUrl = `majorcontext.com/${pathStr.split('/')[0]}`;

Expand Down Expand Up @@ -176,7 +195,7 @@ export const GET: APIRoute = async ({ props, params }) => {
style: {
width: 40,
height: 40,
backgroundColor: '#0369a1',
backgroundColor: brandColor || DEFAULT_BRAND_COLOR,
borderRadius: 4,
},
},
Expand Down
Loading