diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7ab821f..9776904 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.gitignore b/.gitignore index a6408bc..1282f70 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ dist/ # generated types .astro/ +# Lighthouse CI report output +.lighthouseci/ # dependencies node_modules/ diff --git a/CLAUDE.md b/CLAUDE.md index 38b625a..f799c55 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. @@ -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) ``` @@ -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 @@ -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` @@ -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 @@ -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). @@ -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. @@ -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`). diff --git a/astro.config.mjs b/astro.config.mjs index a4eb956..1e36a46 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -25,6 +25,20 @@ export default defineConfig({ ], shikiConfig: { theme: 'github-dark-dimmed', + transformers: [ + { + name: 'bump-comment-contrast', + // github-dark-dimmed renders comments as #768390 on a #22272e + // background -- a 3.87:1 contrast ratio, short of the 4.5:1 WCAG + // AA minimum for body text. Lighten the same hue just enough + // (4.61:1) to pass while keeping the muted "comment" look. + span(node) { + if (node.properties?.style === 'color:#768390') { + node.properties.style = 'color:#84909b'; + } + }, + }, + ], }, }, output: 'static', diff --git a/lighthouserc.json b/lighthouserc.json index 99ac3e4..eaa4f0c 100644 --- a/lighthouserc.json +++ b/lighthouserc.json @@ -15,7 +15,8 @@ "categories:performance": ["error", {"minScore": 0.9}], "categories:accessibility": ["error", {"minScore": 0.9}], "categories:best-practices": ["error", {"minScore": 0.9}], - "categories:seo": ["error", {"minScore": 0.9}] + "categories:seo": ["error", {"minScore": 0.9}], + "network-dependency-tree-insight": ["warn", {"minScore": 0.9}] } }, "upload": { diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 7de38a5..91c2c66 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -94,8 +94,11 @@ const organizationSchema = { - - + diff --git a/src/layouts/DocsLayout.astro b/src/layouts/DocsLayout.astro index 0152f80..bcea84e 100644 --- a/src/layouts/DocsLayout.astro +++ b/src/layouts/DocsLayout.astro @@ -145,7 +145,6 @@ const navigationSections = buildNavigation(docs, product.id); id="sidebar" class="w-64 bg-stone-200 dark:bg-stone-800 fixed top-0 pt-[57px] h-screen overflow-y-auto z-40 transform -translate-x-full md:translate-x-0 transition-transform duration-300 ease-in-out" - aria-hidden="true" >
# Install
+ # Install
$ go install github.com/majorcontext/gatekeeper/cmd/gatekeeper@latest
-# Start the proxy
+# ...or pull the container image
+$ docker pull ghcr.io/majorcontext/gatekeeper:latest
+
+# Start the proxy
$ gatekeeper --config gatekeeper.yaml
-# Make a request — credential injected automatically
+# Make a request — credential injected automatically
$ curl --proxy http://127.0.0.1:9080 --cacert ca.crt https://api.github.com/user
Credentials
- 01
+ 01
TLS Interception
Per-host certificates generated on demand from your CA. The proxy terminates TLS, reads the request, injects the header, and forwards over a fresh upstream TLS connection.
@@ -37,7 +40,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 02
+ 02
Hostname-Based Injection
Map upstream hosts to credentials in YAML. Headers are added only for matching requests — everything else passes through untouched.
@@ -45,15 +48,15 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 03
+ 03
Pluggable Sources
- 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.
- 04
+ 04
Network Policy
Permissive by default, strict allowlist when you need it. Block egress to anything outside your declared host list.
@@ -64,7 +67,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
Integration
- 05
+ 05
Standard HTTP Proxy
Any client that honors HTTPS_PROXY can route through Gatekeeper. No SDK or library required.
@@ -72,7 +75,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 06
+ 06
MCP Relay
Forward Model Context Protocol requests to upstream MCP servers with credential injection and SSE streaming preserved.
@@ -80,7 +83,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 07
+ 07
OpenTelemetry
Traces, metrics, and logs configured entirely via standard OTEL_* environment variables. Canonical log line per request.
@@ -88,7 +91,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 08
+ 08
Go Library
Embed the proxy directly in your Go tooling. Full programmatic control over config, credential sources, and lifecycle.
@@ -96,6 +99,33 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
+ Data Plane & Policy
+
+
+ 09
+ Postgres Data Plane
+
+ 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.
+
+
+
+
+ 10
+ LLM Policy
+
+ Anthropic API responses are evaluated against Keep policy rules before reaching the client. Denials are blocked fail-closed, including streamed SSE responses.
+
+
+
+
+ 11
+ Host Gateway
+
+ 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.
+
+
+
+
Why This Exists
diff --git a/src/pages/keep/[category]/[slug].md.ts b/src/pages/keep/[category]/[slug].md.ts
new file mode 100644
index 0000000..5582b28
--- /dev/null
+++ b/src/pages/keep/[category]/[slug].md.ts
@@ -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>>[number] };
+ return new Response(doc.body + FOOTER, {
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
+ });
+};
diff --git a/src/pages/keep/index.astro b/src/pages/keep/index.astro
index 2bd1ca5..06958e1 100644
--- a/src/pages/keep/index.astro
+++ b/src/pages/keep/index.astro
@@ -17,17 +17,17 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
Ships as a Go library, MCP relay, and LLM gateway.
Works standalone or composed with Moat.
- # Install
+ # Install
$ brew tap majorcontext/tap
$ brew install keep
-# Evaluate a call against your rules
+# Evaluate a call against your rules
$ keep eval --rules rules.yaml call.json
Policy
- 01
+ 01
Declarative Rules
YAML rule files define what to deny, redact, or log. CEL expressions for matching with full access to call structure.
@@ -35,7 +35,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 02
+ 02
Redaction
Strip sensitive fields from tool call arguments before they leave the agent. Pattern-based or path-based targeting.
@@ -43,7 +43,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 03
+ 03
Scoped Evaluation
Rules target specific tools, namespaces, or call patterns. Broad defaults with surgical overrides.
@@ -51,7 +51,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 04
+ 04
Audit Logging
Every evaluation is logged with full context: the call, matched rules, and outcome. Structured JSON for downstream analysis.
@@ -62,7 +62,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
Integration
- 05
+ 05
MCP Relay
Drop-in gateway for Claude Code and other MCP clients. Intercepts tool calls, applies policy, forwards approved calls.
@@ -70,7 +70,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 06
+ 06
LLM Gateway
HTTP proxy between agent and LLM provider. Decomposes tool-use responses and evaluates each call against policy.
@@ -78,7 +78,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 07
+ 07
Go Library
Embed Keep directly in your Go tooling. Full API for rule loading, evaluation, and result inspection.
@@ -86,7 +86,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 08
+ 08
CEL Expressions
Rules use Common Expression Language for matching. Type-safe, sandboxed, and familiar to anyone who's used Firebase or Kubernetes.
diff --git a/src/pages/moat/index.astro b/src/pages/moat/index.astro
index 0fbb770..4582cb4 100644
--- a/src/pages/moat/index.astro
+++ b/src/pages/moat/index.astro
@@ -16,11 +16,11 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
Works with Claude, Codex, and Gemini.
Credential injection for GitHub, GitLab, AWS, OpenAI, npm, SSH, 1Password, and more.
- # Install
+ # Install
$ brew tap majorcontext/tap
$ brew install moat
-# Grant credentials and run Claude safely
+# Grant credentials and run Claude safely
$ moat grant anthropic
$ moat grant github
$ moat claude .
@@ -28,7 +28,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
Safety
- 01
+ 01
Sandboxed Execution
Every agent runs in an isolated container—Docker, Apple containers, or gVisor. No host access.
@@ -36,7 +36,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 02
+ 02
Network-Layer Credentials
OAuth tokens and API keys are injected at the proxy layer. Agents never see raw secrets.
@@ -44,7 +44,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 03
+ 03
Network Policies
Permissive or strict firewall mode. Whitelist allowed hosts, block everything else.
@@ -52,7 +52,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 04
+ 04
Tamper-Proof Audit
Hash-chained audit logs with cryptographic verification. Export proof bundles for compliance.
@@ -63,7 +63,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
Developer Experience
- 05
+ 05
Declarative Config
One agent.yaml defines runtime, credentials, services, and network policy.
@@ -71,7 +71,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 06
+ 06
Service Dependencies
PostgreSQL, MySQL, and Redis sidecars auto-provisioned with injected credentials.
@@ -79,7 +79,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 07
+ 07
Snapshots & Recovery
Automatic workspace snapshots on commits, builds, and idle. Point-in-time restore without stopping.
@@ -87,7 +87,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
- 08
+ 08
Parallel Worktrees
Run multiple agents on separate git branches simultaneously. No workspace conflicts.
diff --git a/src/pages/og/[...path].png.ts b/src/pages/og/[...path].png.ts
index 8aca3dd..71f1bb4 100644
--- a/src/pages/og/[...path].png.ts
+++ b/src/pages/og/[...path].png.ts
@@ -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 = {
+ 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');
@@ -19,6 +31,7 @@ export async function getStaticPaths() {
props: {
title: 'Major Context',
description: 'Safe infrastructure for AI agents',
+ brandColor: DEFAULT_BRAND_COLOR,
},
},
// Moat homepage
@@ -27,6 +40,7 @@ export async function getStaticPaths() {
props: {
title: 'Moat',
description: 'Let agents break things safely',
+ brandColor: BRAND_COLORS.moat,
},
},
// Keep homepage
@@ -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
@@ -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
@@ -58,6 +74,7 @@ export async function getStaticPaths() {
props: {
title: doc.data.title,
description: doc.data.description || 'Moat Documentation',
+ brandColor: BRAND_COLORS.moat,
},
};
}),
@@ -74,6 +91,7 @@ export async function getStaticPaths() {
props: {
title: doc.data.title,
description: doc.data.description || 'Keep Documentation',
+ brandColor: BRAND_COLORS.keep,
},
};
}),
@@ -90,6 +108,7 @@ export async function getStaticPaths() {
props: {
title: doc.data.title,
description: doc.data.description || 'Gatekeeper Documentation',
+ brandColor: BRAND_COLORS.gatekeeper,
},
};
}),
@@ -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]}`;
@@ -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,
},
},
diff --git a/src/scripts/mobile-menu.ts b/src/scripts/mobile-menu.ts
index 4c2ce1a..81ce7a1 100644
--- a/src/scripts/mobile-menu.ts
+++ b/src/scripts/mobile-menu.ts
@@ -1,19 +1,47 @@
const toggle = document.getElementById('mobile-menu-toggle');
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
+const sidebarLinks = sidebar ? Array.from(sidebar.querySelectorAll('a')) : [];
+
+// The sidebar is only actually off-screen (hidden) below the md breakpoint
+// while closed -- at md+ `md:translate-x-0` keeps it permanently visible.
+const desktopQuery = window.matchMedia('(min-width: 768px)');
+
+function isOpen(): boolean {
+ return !sidebar?.classList.contains('-translate-x-full');
+}
+
+/**
+ * Keeps aria-hidden and focusability of the sidebar's nav links in sync with
+ * whether it's actually visible. Without this, the sidebar was aria-hidden
+ * unconditionally while its links stayed in the tab order -- a WCAG
+ * aria-hidden-focus violation -- and remained wrong at md+ where the
+ * sidebar is always visible.
+ */
+function syncSidebarAccessibility(): void {
+ const hidden = !desktopQuery.matches && !isOpen();
+ sidebar?.setAttribute('aria-hidden', String(hidden));
+ sidebarLinks.forEach((link) => {
+ if (hidden) {
+ link.setAttribute('tabindex', '-1');
+ } else {
+ link.removeAttribute('tabindex');
+ }
+ });
+}
function openSidebar(): void {
sidebar?.classList.remove('-translate-x-full');
overlay?.classList.remove('hidden');
toggle?.setAttribute('aria-expanded', 'true');
- sidebar?.setAttribute('aria-hidden', 'false');
+ syncSidebarAccessibility();
}
function closeSidebar(): void {
sidebar?.classList.add('-translate-x-full');
overlay?.classList.add('hidden');
toggle?.setAttribute('aria-expanded', 'false');
- sidebar?.setAttribute('aria-hidden', 'true');
+ syncSidebarAccessibility();
}
toggle?.addEventListener('click', () => {
@@ -34,11 +62,13 @@ document.addEventListener('keydown', (e) => {
});
// Close sidebar on navigation (mobile only)
-const links = sidebar?.querySelectorAll('a');
-links?.forEach(link => {
+sidebarLinks.forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
+
+syncSidebarAccessibility();
+desktopQuery.addEventListener('change', syncSidebarAccessibility);