---
name: andrewadhikari-com
description: Use when the user asks to publish, edit, list, or delete content on andrewadhikari.com — including labs, notes, cards, mocks, mistakes, tracks, and images. Provides REST endpoints to create / read / update / delete content; loads auth token from ~/.config/adkari/token or $ADKARI_TOKEN. Refers to the live site for current schema.
---

# andrewadhikari.com — content publishing API

You are managing content for **andrewadhikari.com**, a personal study OS for
cert prep (CKA, AFOQT, Terraform, etc.). This skill teaches you the live REST
API for publishing and editing content. Every endpoint is on the production
domain `https://andrewadhikari.com`. There is no staging.

After every successful write the site rebuilds via Cloudflare Pages — the new
content is live in roughly 30–60 seconds. Tell the user "queued — live in
~60s", do not claim it is live the instant the API returns 200.

## 1. When to use this skill

Trigger on any of:

- "publish a lab on \<topic\>" / "draft a note about \<topic\>"
- "publish cards for \<topic\>" / "make a flashcard deck for \<topic\>"
- "publish a mock exam for \<track\>"
- "log a mistake from a mock"
- "add a new track" / "update the CKA track"
- "list my labs" / "show me cards for \<track\>" / "what labs are in \<domain\>"
- "update the order of \<lab\>" / "fix the description on \<lab\>"
- "delete the lab on \<topic\>" / "remove this card deck"
- "upload this image to my CDN" / "embed this screenshot in the lab"
- Any general "publish to my site" / "add this to andrewadhikari.com" intent

If a request looks like editing files in the repo *locally* (e.g. "open
src/content/labs/foo.md and change the order"), prefer this API over
filesystem edits — the API runs the same Zod validation the build does, so a
202/200 means it will deploy.

## 1a. Tool surface — prefer MCP, fall back to bash

If you see `mcp__andrewadhikari__*` tools listed in your tool catalog
(e.g. `mcp__andrewadhikari__list_tracks`,
`mcp__andrewadhikari__publish_content`, …), use those exclusively. They are:

- Typed (JSON-Schema validated by Claude Code before the call leaves)
- Token-safe (the token lives in the user's host `~/.config/adkari/token`
  or `$ADKARI_TOKEN` and is read by the MCP server process — it never
  enters your sandboxed bash environment)
- Faster (no shell / curl round-trip per call)

Mapping from API → MCP tool:

| API call                                         | MCP tool                          |
| ------------------------------------------------ | --------------------------------- |
| `GET /api/tracks`                                | `list_tracks`                     |
| `GET /api/content?collection=…`                  | `list_content`                    |
| `GET /api/content/<path>`                        | `read_content`                    |
| `POST /api/content` (single file)                | `publish_content`                 |
| `POST /api/content` (bulk Trees commit)          | `publish_bulk`                    |
| `POST /api/content` (new track + seed children)  | `create_track`                    |
| `PATCH /api/content/<path>`                      | `patch_content`                   |
| `DELETE /api/content/<path>`                     | `delete_content`                  |
| `POST /api/upload` (multipart)                   | `upload_image` (`{ filePath }`)   |

If the MCP tools aren't available on this machine (the user hasn't run
`npm run install-mcp` yet, or they're on a fresh box), fall back to the
bash workflow described in section 2 onward — `curl` + the same token
resolution rules. **Don't mix the two paths in a single session** — pick
MCP if available, otherwise bash, and stay there.

Sections 4 (schemas) and 6 (workflows, including Workflow 0 from-scratch)
still apply verbatim — MCP only changes the *how* of issuing the call.
The *what* you publish is identical.

## 1b. Build-cost discipline (READ THIS)

Every git commit triggers a Cloudflare Pages build (~2 min) and a CDN cache
invalidation. Multiple commits in a row queue → the live site stays stale
for the duration of the queue. We've seen 26-build queues from a single
authoring session — that's ~50 min of stale site for what should have been
one commit.

**Rules:**

1. **New track from scratch?** Use `create_track`. It bundles the track
   file + every seed child (labs, cards, mocks, notes) into ONE atomic
   commit. Auto-fills `track: <slug>` on each child so you can't forget
   the cross-reference.
2. **Publishing >1 file in any other shape?** Use `publish_bulk`. One
   `files: [...]` array → one commit → one build, regardless of how many
   files.
3. **Genuine one-off ad-hoc write?** That's the only legitimate case for
   `publish_content` (single).
4. **Mid-session realization that you need more files?** Finish the current
   bulk plan, *then* add the new file as a follow-up bulk — don't fall
   back to single-file calls "just for the new one."

**Commit message convention.** Bulk commits should describe the session,
not enumerate files:

- ✅ `feat: hello cheatsheet — 8 labs + 3 cards`
- ❌ `publish: 12 files`
- ❌ `publish lesson 3 of 8`

`create_track` produces this format by default; for `publish_bulk` you
pass it via the `message` arg.

## 2. Authentication

All endpoints require `Authorization: Bearer <token>`. The token is a GitHub
fine-grained personal access token belonging to user `t-rhex`, scoped to the
single repo `t-rhex/andrewadhikari.com` with `Contents: Read and write` and
`Metadata: Read`.

**Resolve the token in this order:**

1. `$ADKARI_TOKEN` env var
2. `~/.config/adkari/token` file (single line, mode 600)

```bash
TOKEN="${ADKARI_TOKEN:-$(cat "$HOME/.config/adkari/token" 2>/dev/null)}"
if [ -z "$TOKEN" ]; then
  echo "missing ADKARI_TOKEN — see SETUP.md in andrewadhikari.com repo" >&2
  exit 1
fi
```

If no token is found, **stop**. First check whether you're in a sandboxed
environment (see section 2a) — if so, switch to plan-and-handoff mode
instead of asking the user to expose the token. Otherwise tell the user to
follow the "Optional: programmatic content publishing" section of
`SETUP.md` in the repo, then retry. Do not attempt requests without a
token.

Failure modes for auth:

- `401 unauthorized` — missing or invalid token. Token file empty or env
  var unset.
- `403 forbidden` — token belongs to a different GitHub user. Wrong PAT.

## 2a. Sandboxed environments (Claude Code Bash sandbox / cowork)

When this skill runs inside a Bash sandbox that does NOT mount the user's
home directory (the typical "cowork" scenario in Claude Code), neither
`$ADKARI_TOKEN` nor `~/.config/adkari/token` will resolve. You will hit a
permission error when trying to read the token. **Don't proceed by asking
for the token in chat — it gets logged.** Don't ask the user to copy the
token into the sandbox-mounted project directory either — even short-lived
exposure on disk is unnecessary risk.

Instead, switch to **plan-and-handoff mode**:

1. **Detect the constraint.** If you can't read either source after trying
   both, say so explicitly. Don't silently call the API without a token —
   you'll get a 401 anyway.

2. **Write a runnable script to the sandbox-shared directory.** Pick a
   location the user has write access to AND that the sandbox can read,
   typically a project directory the user mentions or
   `~/Documents/Claude/Projects/Learn/_runme.sh`. The script:
   - Loads the token from `$HOME/.config/adkari/token` ITSELF (the user's
     real shell will resolve `$HOME` correctly)
   - Has the full `curl` command(s) ready — the JSON payload, the URL,
     headers, content-type
   - Writes responses to `_runme.out.json` for the user to share back

   ```bash
   #!/usr/bin/env bash
   set -euo pipefail
   TOKEN="${ADKARI_TOKEN:-$(cat "$HOME/.config/adkari/token" 2>/dev/null)}"
   if [ -z "$TOKEN" ]; then
     echo "no token at \$ADKARI_TOKEN or ~/.config/adkari/token" >&2
     exit 1
   fi
   curl -sS -X POST https://andrewadhikari.com/api/content \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     --data @./_runme.payload.json \
     | tee _runme.out.json
   ```

3. **Tell the user exactly two things:**
   - "Run `bash <path-to-script>` in your real terminal."
   - "Paste the contents of `_runme.out.json` back here when it finishes."

4. **Resume planning from the response.** Once the user pastes the
   `commit_sha` + `site_url`, you have the same information you'd have had
   from a direct call. Continue the workflow normally (e.g. for Workflow 0,
   move on to step 3 after the track is created).

5. **For multi-step workflows, batch the script.** Instead of one
   round-trip per API call, write a script that does several commands in
   sequence (e.g. all five steps of Workflow 0). The user runs it once;
   you parse the combined output. Saves them clicking "go" repeatedly.

**Never:**

- Ask the user to paste their token into chat (logged in conversation
  history, no clean revocation).
- Tell them to copy the token into a sandbox-mounted directory ("just
  temporarily" rots — they forget to delete it).
- Try to invent a token, hardcode one in the script, or `curl` without
  auth and hope the API doesn't notice.

## 3. Endpoints

Base URL: `https://andrewadhikari.com`

| Method | Path                         | Purpose                                            |
| ------ | ---------------------------- | -------------------------------------------------- |
| POST   | `/api/content`               | Create one or many files (atomic single commit)    |
| GET    | `/api/content?collection=…`  | List files in a collection (filterable)            |
| GET    | `/api/content/<path>`        | Read a single file (frontmatter + body)            |
| PATCH  | `/api/content/<path>`        | Update a file (frontmatter merge, optional body)   |
| DELETE | `/api/content/<path>`        | Delete a file                                      |
| GET    | `/api/tracks`                | Flat track list with domains (prompt grounding)    |
| POST   | `/api/upload`                | Upload image to R2 (multipart, returns CDN URL)    |

`<path>` is the repo-relative path of the file, e.g.
`src/content/labs/2026-05-07-pod-lifecycle.md`. URL-encode segments if needed.

### 3.1 POST /api/content — create

JSON body, single-file shape:

```json
{
  "collection": "labs",
  "filename": "2026-05-07-pod-lifecycle.md",
  "frontmatter": {
    "title": "Pod lifecycle",
    "track": "cka",
    "domain": "workloads",
    "type": "standard",
    "tags": ["kubernetes", "pods"],
    "description": "How pods are scheduled, started, and terminated.",
    "order": 26
  },
  "body": "## Why\n\nPods are the smallest deployable unit...",
  "overwrite": false,
  "message": "publish: Pod lifecycle (cka/workloads)"
}
```

- `filename` is **optional** — server generates `<YYYY-MM-DD>-<kebab(title)>.md`
  for markdown collections, `<track>-<kebab(title)>.yaml` for YAML data
  collections.
- `body` is required for markdown collections (`labs`, `notes`, `mistakes`,
  `tracks`) and must be `null` for YAML data collections (`cards`, `mocks`).
- `overwrite` defaults to `false`. When `false`, existing path → `409`.
- `message` is the git commit message. Optional; server picks a sensible
  default.

JSON body, bulk shape (atomic single commit via Trees API):

```json
{
  "files": [
    { "collection": "labs", "frontmatter": { "title": "...", "track": "cka", "domain": "workloads" }, "body": "..." },
    { "collection": "cards", "filename": "cka-pods.yaml", "frontmatter": { "title": "Pods", "track": "cka", "cards": [{"q":"...","a":"..."}] }, "body": null }
  ],
  "message": "publish: 5 labs from k8s-labs"
}
```

Response 200:

```json
{
  "commit_sha": "abc123…",
  "html_url": "https://github.com/t-rhex/andrewadhikari.com/commit/abc123",
  "files": [
    { "path": "src/content/labs/2026-05-07-pod-lifecycle.md",
      "site_url": "https://andrewadhikari.com/labs/pod-lifecycle" }
  ]
}
```

Errors:

- `400 bad_request` — body shape invalid (missing `collection`, etc.)
- `401 unauthorized` — auth failed
- `403 forbidden` — wrong GitHub user
- `409 conflict` — file exists and `overwrite=false`
- `422 schema_invalid` — frontmatter fails the live Zod schema. Body has the
  Zod message — read it, fix the field, retry.
- `502 github_error` — upstream GitHub API failure
- `500 internal_error` — anything else

`curl` example:

```bash
curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "collection": "labs",
  "frontmatter": {
    "title": "Pod lifecycle",
    "track": "cka",
    "domain": "workloads"
  },
  "body": "## Why\n\nPods are the smallest deployable unit in Kubernetes...\n"
}
JSON
```

### 3.2 PATCH /api/content/&lt;path&gt; — update

Body:

```json
{
  "frontmatter": { "order": 99, "tags": ["kubernetes", "pods", "lifecycle"] },
  "body": "## Why\n\nUpdated body...",
  "message": "update: bump order on Pod lifecycle"
}
```

- `frontmatter` is shallow-merged into the existing frontmatter. To clear a
  field, pass it as `null`.
- `body` is **optional** — omit to keep the existing body.
- `message` optional.

Response 200 mirrors the create response.

Errors: 401, 403, 404 (path not found), 422 (merged frontmatter still has to
pass Zod), 502.

`curl`:

```bash
curl -sS -X PATCH \
  https://andrewadhikari.com/api/content/src/content/labs/2026-05-07-pod-lifecycle.md \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"frontmatter":{"order":99}}'
```

### 3.3 DELETE /api/content/&lt;path&gt; — delete

No body required. Response 200 returns the commit info. 404 if path missing.

```bash
curl -sS -X DELETE \
  https://andrewadhikari.com/api/content/src/content/labs/2026-05-07-pod-lifecycle.md \
  -H "Authorization: Bearer $TOKEN"
```

### 3.4 GET /api/content?collection=… — list

Query params:

| Param        | Required | Notes                                                      |
| ------------ | -------- | ---------------------------------------------------------- |
| `collection` | yes      | One of `labs`, `notes`, `tracks`, `mistakes`, `cards`, `mocks` |
| `track`      | no       | Filter to entries whose frontmatter `track` matches        |
| `domain`     | no       | Filter to entries whose frontmatter `domain` matches       |
| `limit`      | no       | Default 100                                                |
| `offset`     | no       | Default 0                                                  |
| `fields`     | no       | Comma-separated; returns slim records                      |

Response 200:

```json
{
  "collection": "labs",
  "count": 28,
  "items": [
    { "slug": "pod-lifecycle",
      "path": "src/content/labs/2026-05-07-pod-lifecycle.md",
      "title": "Pod lifecycle",
      "track": "cka",
      "domain": "workloads",
      "order": 26 }
  ]
}
```

Body content is **not** returned — use the single-file GET for that.

```bash
curl -sS \
  "https://andrewadhikari.com/api/content?collection=labs&track=cka&domain=workloads" \
  -H "Authorization: Bearer $TOKEN"
```

### 3.5 GET /api/content/&lt;path&gt; — read

Response 200:

```json
{
  "path": "src/content/labs/2026-05-07-pod-lifecycle.md",
  "frontmatter": { "title": "Pod lifecycle", "track": "cka", "domain": "workloads", "...": "..." },
  "body": "## Why\n\nPods are..."
}
```

Errors: 401, 403, 404.

### 3.6 GET /api/tracks — track list (prompt grounding)

This is the helper to call **first** when you do not know which track or
domain a topic belongs to. Cheaper than listing the tracks collection.

```json
{
  "tracks": [
    {
      "slug": "cka",
      "title": "CKA — Certified Kubernetes Administrator",
      "exam_date": "2026-09-15",
      "status": "active",
      "description": "Kubernetes administration cert prep.",
      "domains": [
        { "id": "cluster-architecture", "name": "Cluster Architecture, Installation & Configuration", "weight": 25 },
        { "id": "workloads", "name": "Workloads & Scheduling", "weight": 15 }
      ]
    }
  ]
}
```

```bash
curl -sS https://andrewadhikari.com/api/tracks -H "Authorization: Bearer $TOKEN"
```

### 3.7 POST /api/upload — image upload

Multipart form. Field name: `file`. Returns the CDN URL on success.

```bash
curl -sS -X POST https://andrewadhikari.com/api/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/path/to/screenshot.png"
# → {"url":"https://cdn.andrewadhikari.com/img/<hash>.png"}
```

Embed the URL directly in markdown bodies: `![alt](https://cdn.andrewadhikari.com/img/<hash>.png)`.

Allowed types: PNG, JPEG, GIF, WebP, SVG. Anything else → 400.

## 4. Content schema

All schemas live in `src/content/config.ts` in the repo. The API runs the
exact same Zod validation the build does — so "valid via API" means "builds".

### labs / notes — `noteSchema`

Fields:

| Field           | Type                              | Notes                                          |
| --------------- | --------------------------------- | ---------------------------------------------- |
| `title`         | string (required)                 |                                                |
| `slug`          | string (optional)                 | Astro derives from filename if omitted         |
| `date`          | date (required)                   | API auto-fills with today if missing           |
| `updated`       | date (optional)                   |                                                |
| `tags`          | string[] (default `[]`)           |                                                |
| `category`      | `labs` \| `notes` \| `drafts`     | API auto-fills from `collection`               |
| `draft`         | boolean (default `false`)         |                                                |
| `description`   | string (optional)                 |                                                |
| `track`         | string (optional but recommended) | **Required** for labs to appear in courseware  |
| `domain`        | string (optional but recommended) | **Required** for labs to appear in courseware  |
| `type`          | `standard` \| `cheatsheet`        | Default `standard`                             |
| `study_minutes` | int > 0 (optional)                |                                                |
| `order`         | int (optional)                    | Sort order within (track, domain)              |
| `prereq`        | string[] (optional)               | Slugs of prerequisite labs                     |
| `lead`          | boolean (optional)                | When `true`, the first paragraph gets a serif drop-cap. Use sparingly — feature posts only. |

Realistic minimal example (`src/content/labs/2026-05-07-pod-lifecycle.md`):

```markdown
---
title: Pod lifecycle
date: 2026-05-07
category: labs
track: cka
domain: workloads
order: 26
description: How pods are scheduled, started, and terminated.
---

## Why

Pods are the smallest deployable unit in Kubernetes...
```

### tracks — `trackSchema`

| Field          | Type                                       | Notes                          |
| -------------- | ------------------------------------------ | ------------------------------ |
| `title`        | string                                     | Required                       |
| `exam_date`    | date                                       | Required                       |
| `status`       | `active` \| `paused` \| `passed` \| `abandoned` | Default `active`           |
| `description`  | string (optional)                          |                                |
| `domains`      | array of `{id, name, weight}`              | At least 1, weights are 0–100  |
| `resources`    | array of `{label, url}` (default `[]`)     | URL must be valid              |
| `accent_color` | hex string `#RRGGBB` (optional)            | Per-track accent override      |

Realistic minimal example (`src/content/tracks/cka.md`):

```markdown
---
title: CKA — Certified Kubernetes Administrator
exam_date: 2026-09-15
status: active
description: Kubernetes administration cert prep.
domains:
  - id: cluster-architecture
    name: Cluster Architecture, Installation & Configuration
    weight: 25
  - id: workloads
    name: Workloads & Scheduling
    weight: 15
  - id: services-networking
    name: Services & Networking
    weight: 20
---

CKA prep notes — covers installation, troubleshooting, and operations.
```

### cards — `cardSetSchema` (YAML data collection)

| Field         | Type                          | Notes                              |
| ------------- | ----------------------------- | ---------------------------------- |
| `title`       | string                        | Required                           |
| `track`       | string                        | Required                           |
| `domain`      | string (optional)             |                                    |
| `description` | string (optional)             |                                    |
| `cards`       | array of `{q, a, hints?}`     | At least 1                         |

Realistic minimal example (`src/content/cards/cka-pods.yaml`):

```yaml
title: Pods — quick recall
track: cka
domain: workloads
description: Lifecycle, restart policies, init containers.
cards:
  - q: What's the smallest deployable unit in Kubernetes?
    a: A Pod.
    hints:
      - Not a container directly.
  - q: What restart policy does a Job use by default?
    a: OnFailure.
```

When POSTing cards, pass the entire shape above as `frontmatter` and set
`body: null`.

### mocks — `mockSchema` (YAML data collection)

| Field                | Type                                 | Notes                              |
| -------------------- | ------------------------------------ | ---------------------------------- |
| `title`              | string                               | Required                           |
| `track`              | string                               | Required                           |
| `description`        | string (optional)                    |                                    |
| `time_limit_minutes` | int > 0                              | Required                           |
| `passing_score`      | number 0–1                           | Required (e.g. `0.66`)             |
| `questions`          | array of question objects            | At least 1                         |

Question shape: `{ q, choices: string[≥2], correct: int, explanation?, domain? }`.
`correct` is the **index** into `choices` and must be in range.

Realistic minimal example:

```yaml
title: CKA mock — workloads
track: cka
description: 10 questions on workload scheduling and pod lifecycle.
time_limit_minutes: 20
passing_score: 0.66
questions:
  - q: What field controls how many old ReplicaSets a Deployment keeps?
    choices:
      - revisionHistoryLimit
      - rollbackLimit
      - historySize
    correct: 0
    explanation: Default is 10.
    domain: workloads
```

### mistakes — `mistakeSchema`

| Field            | Type                                    | Notes                          |
| ---------------- | --------------------------------------- | ------------------------------ |
| `title`          | string                                  | Required                       |
| `track`          | string                                  | Required                       |
| `domain`         | string (optional)                       |                                |
| `date`           | date                                    | Required                       |
| `source_type`    | `mock` \| `flashcard` \| `manual`       | Required                       |
| `source_ref`     | string (optional)                       | E.g. `cka-mock-1#q4`           |
| `correct_answer` | string                                  | Required                       |
| `my_answer`      | string                                  | Required                       |

Realistic minimal example (`src/content/mistakes/2026-05-07-revision-history-limit.md`):

```markdown
---
title: revisionHistoryLimit, not rollbackLimit
track: cka
domain: workloads
date: 2026-05-07
source_type: mock
source_ref: cka-mock-workloads#q1
correct_answer: revisionHistoryLimit
my_answer: rollbackLimit
---

I keep guessing `rollbackLimit`. The field is `revisionHistoryLimit`,
default 10.
```

## 4a. Markdown features (use these in lab/note bodies)

The site has a few custom remark plugins. Use them — they make the published
content much richer than plain markdown.

### Wikilinks: `[[slug]]` and `[[slug|label]]`

Inside a lab/note body, link to another lab or note by slug:

```markdown
This builds on [[pod-lifecycle]] and the rules in [[scheduler-internals|Scheduler internals]].
```

The remark plugin resolves `pod-lifecycle` against the `labs` and `notes`
collections, renders an internal link, and walks all notes at build time to
generate a "Referenced by" backlinks footer on the target page.

When unsure of a slug, **list the collection first** with the API and pick
the real slug — never guess. A wikilink to a non-existent slug renders with a
hairline strikethrough so you can spot the typo, but it pollutes backlinks.

### Inline cards: `:::card front=… back=…`

Anywhere in a lab/note body, drop in:

```markdown
:::card front="What's the smallest deployable unit in K8s?" back="A Pod."
:::

:::card front="Default Job restart policy?" back="OnFailure."
hints=["Not Always", "Set in spec.template.spec.restartPolicy"]
:::
```

The build:
1. Renders a small inline card preview at that spot in the prose
2. Adds these to a virtual deck `<note-slug>--inline` so they appear in the
   spaced-repetition queue
3. Surfaces a "Drill cards in this note (N)" button at the end of the note

**Use inline cards aggressively** in labs covering memorisable facts
(commands, defaults, specific numbers, exam-trivia). They drill review
without forcing you to maintain a parallel `cards/<deck>.yaml` file.

### Cheatsheets (`type: cheatsheet`)

A standard lab is read top-to-bottom. A cheatsheet is *scanned*. Set
`type: cheatsheet` in frontmatter and the page renders:

- CSS columns (multi-column dense reference)
- Sans body (no serif), 14 px, tight rhythm
- No reading-progress bar, no right-rail ToC
- Print-optimised — `window.print()` produces a clean PDF cheatsheet

Use cheatsheets for: kubectl one-liners, Terraform CLI flags, AWS service
quick reference, exam-day formula sheets. **Do not** use cheatsheets for
explanatory tutorials — those are standard labs.

```yaml
---
title: kubectl quick reference
track: cka
type: cheatsheet
description: One-liners every CKA candidate should have in muscle memory.
---
```

### Drop-cap (`lead: true`)

Editorial flair for "feature" labs you want to read more like an essay. The
first paragraph gets a large serif drop-cap. Use sparingly — one or two per
track at most. Looks bad on technical reference content.

## 5. Filename + path conventions

- Markdown collections (`labs`, `notes`, `tracks`, `mistakes`):
  `src/content/<collection>/<filename>.md`. Filenames may include a
  `YYYY-MM-DD-` date prefix; the routing layer strips it for the public slug.
- YAML data collections (`cards`, `mocks`):
  `src/content/<collection>/<filename>.yaml`.
- The `filename` field in API requests is optional — the server generates one
  if missing.

## 6. Common workflows

### Workflow 0: Pursue a new cert track from scratch

Triggered by intents like:

- "I'm starting <CERT> today, set up the track"
- "Build me an AWS Security Specialty prep skeleton"
- "Bootstrap CKAD prep — track, first labs, a card deck, a mock"

> **MCP shortcut:** if the `mcp__andrewadhikari__create_track` tool is
> available, do this whole workflow in **one** call. Pass the track
> frontmatter + arrays of `labs`/`cards`/`mocks`/`notes` and it produces
> one atomic commit and one Pages build. The bash steps below are the
> fallback when MCP isn't installed — and even then you should bundle
> step 3+4+5 into one bulk `POST /api/content` to keep the commit count
> down. **Never** publish step 2/3/4/5 as separate single-file calls.

This is a composite workflow. Do the steps in order. Use the bulk POST shape
(section 3.1) where it cuts commits.

**Step 1 — Confirm the track doesn't already exist.**

```bash
curl -sS https://andrewadhikari.com/api/tracks -H "Authorization: Bearer $TOKEN" \
  | jq '.tracks[].slug'
```

If the slug already exists, switch to "extend an existing track" (workflow 1).

**Step 2 — Create the track.**

You need: title, exam_date, status (`active`), description, domains array.

Domain rules:
- 4–7 domains is typical for a vendor cert
- Use the cert vendor's official curriculum domain names verbatim where you
  can — exam-trivia tracks the vendor's vocabulary
- Each domain has `id` (kebab-case slug, used as a foreign key in lab
  frontmatter) and `weight` (0–100)
- **Weights MUST sum to 100.** If you don't know the exact weights, infer
  from the official exam guide; if you can't, distribute evenly and round
  the last domain to absorb the remainder

Optionally include `accent_color` (hex) — picks the per-track color used on
the days-to-exam number and the active-route rule. Omit for default
NVIDIA green.

```bash
curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "collection": "tracks",
  "filename": "aws-security.md",
  "frontmatter": {
    "title": "AWS Certified Security — Specialty",
    "exam_date": "2026-11-04",
    "status": "active",
    "description": "AWS specialty cert. IAM, KMS, VPC design, GuardDuty/CloudTrail, incident response.",
    "accent_color": "#ea580c",
    "domains": [
      { "id": "threat-detection",  "name": "Threat Detection & Incident Response", "weight": 14 },
      { "id": "logging-monitoring","name": "Security Logging & Monitoring",        "weight": 18 },
      { "id": "infra-security",    "name": "Infrastructure Security",              "weight": 20 },
      { "id": "iam",               "name": "Identity & Access Management",         "weight": 16 },
      { "id": "data-protection",   "name": "Data Protection (KMS, encryption, secrets)", "weight": 18 },
      { "id": "governance",        "name": "Management & Security Governance",     "weight": 14 }
    ]
  },
  "body": "# AWS Security — Specialty Track\n\nStrategy notes go here..."
}
JSON
```

**Step 3 — Bulk-publish one starter lab per domain.**

The point: the homepage hero shows "<N> days until <track>" for the
closest-exam active track, and a track is empty until it has at least one
lab per domain (otherwise the course-player shows zero progress). One
cohesive bulk commit:

```bash
curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "files": [
    { "collection": "labs",
      "frontmatter": {
        "title": "IAM evaluation logic — the order operations actually run in",
        "track": "aws-security", "domain": "iam",
        "order": 1, "study_minutes": 25,
        "tags": ["iam", "policies"],
        "description": "SCPs vs identity-based vs resource-based vs permission boundaries — the precedence rules."
      },
      "body": "## Why this matters\n\n...\n\n:::card front=\"Which policy type wins by default — SCP or identity-based?\" back=\"Both must allow. SCPs are a deny ceiling; an explicit deny anywhere wins.\"\n:::\n"
    },
    { "collection": "labs",
      "frontmatter": {
        "title": "KMS key policies vs grants",
        "track": "aws-security", "domain": "data-protection",
        "order": 1, "study_minutes": 20,
        "description": "When to use a key policy, when to use a grant, when to use both."
      },
      "body": "..."
    }
  ],
  "message": "publish: AWS Security — first labs (one per starter domain)"
}
JSON
```

For each lab in step 3:
- `track` and `domain` are MANDATORY (without them the lab won't appear in
  the course player)
- `order: 1` — first lesson per domain. Subsequent labs in the same domain
  should increment (`order: 2`, `order: 3`, …). Don't skip numbers
- Use **inline cards** (`:::card`) liberally for memorisable facts. Each
  inline card joins the SR queue automatically
- Use **wikilinks** when a lab references another (`[[scheduler-internals]]`)
- Aim for 200–600 words of body for a starter lab. Plenty of room to grow

**Step 4 — Publish a starter card deck per domain you'll drill heavily.**

Cards are for "I need this in muscle memory" facts (commands, defaults,
specific numbers). 10–25 cards per deck is the sweet spot.

```bash
curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "collection": "cards",
  "filename": "aws-security-iam-policies.yaml",
  "frontmatter": {
    "title": "IAM policies — quick recall",
    "track": "aws-security",
    "domain": "iam",
    "description": "The 4 policy types and their precedence rules.",
    "cards": [
      { "q": "Name the four IAM policy types in evaluation order.", "a": "1. SCP, 2. Resource-based, 3. Identity-based, 4. Permission boundary." },
      { "q": "Default behavior when no policy explicitly allows?", "a": "Implicit deny." },
      { "q": "What does an SCP block?", "a": "All accounts in scope — even the root user. SCP is a deny ceiling." }
    ]
  },
  "body": null
}
JSON
```

**Step 5 — Optionally publish one starter mock.**

Mocks should mirror exam difficulty. Small starter mock (5–10 questions) is
good for a baseline; full mocks (50–60 questions) come closer to the exam
date.

```bash
curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "collection": "mocks",
  "filename": "aws-security-iam-baseline.yaml",
  "frontmatter": {
    "title": "AWS Security — IAM baseline",
    "track": "aws-security",
    "description": "10-question baseline before serious IAM drilling.",
    "time_limit_minutes": 20,
    "passing_score": 0.7,
    "questions": [
      {
        "q": "Which IAM policy element specifies AWS resources?",
        "choices": ["Effect", "Resource", "Condition", "Action"],
        "correct": 1,
        "explanation": "Resource is the ARN(s) the statement applies to.",
        "domain": "iam"
      }
    ]
  },
  "body": null
}
JSON
```

**Step 6 — Report to the user.**

Tell them:
- The track URL: `https://andrewadhikari.com/tracks/<slug>/`
- Number of labs / cards / mocks created
- Suggest the user open `/me/login/` in the browser to sign in (if they
  haven't) so they can highlight + bookmark while reading
- "Queued — live in ~60s"

Do NOT chain step 3 + step 4 + step 5 into one mega-commit. The user usually
wants to see the track render in the dashboard before committing to a card
deck, and the bulk shape's atomicity isn't worth the loss of incremental
visibility. Three separate commits (track / labs / cards+mock) is the
pattern.

### Workflow 1: Publish a single new lab

1. `GET /api/tracks` to discover valid track slugs and the domains under
   each. Do **not** invent track or domain values.
2. Pick the right `(track, domain)` for the topic. If unsure, ask the user.
3. Generate the markdown body and frontmatter. Always include `track` and
   `domain` for labs.
4. `POST /api/content` with `collection: "labs"`.
5. Read the response, report the `site_url` to the user, and tell them
   "queued — live in ~90s" (CI build + Pages deploy + CDN propagation).

```bash
TOKEN="${ADKARI_TOKEN:-$(cat "$HOME/.config/adkari/token")}"
curl -sS https://andrewadhikari.com/api/tracks -H "Authorization: Bearer $TOKEN"
# → pick the right (track, domain)

curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "collection": "labs",
  "frontmatter": {
    "title": "Pod lifecycle",
    "track": "cka",
    "domain": "workloads",
    "order": 26,
    "tags": ["kubernetes", "pods"]
  },
  "body": "## Why\n\nPods are the smallest deployable unit in Kubernetes.\n\n## What\n\n- Pending → Running → Succeeded/Failed\n- Restart policies: Always, OnFailure, Never\n"
}
JSON
```

### Workflow 2: Publish a flashcard deck

Same pattern, but `collection: "cards"`, `body: null`, and the frontmatter is
the **entire** `cardSetSchema` shape including the `cards: [...]` array.

```bash
curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "collection": "cards",
  "frontmatter": {
    "title": "Pods — quick recall",
    "track": "cka",
    "domain": "workloads",
    "cards": [
      { "q": "Smallest deployable unit?", "a": "A Pod." },
      { "q": "Default Job restart policy?", "a": "OnFailure." }
    ]
  },
  "body": null
}
JSON
```

### Workflow 3: Update an existing lab's order

```bash
curl -sS -X PATCH \
  https://andrewadhikari.com/api/content/src/content/labs/2026-05-07-pod-lifecycle.md \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"frontmatter":{"order":99}}'
```

If you don't know the exact filename, list the collection first:

```bash
curl -sS \
  "https://andrewadhikari.com/api/content?collection=labs&track=cka&domain=workloads" \
  -H "Authorization: Bearer $TOKEN"
```

### Workflow 4: List all CKA cluster-architecture labs

```bash
curl -sS \
  "https://andrewadhikari.com/api/content?collection=labs&track=cka&domain=cluster-architecture" \
  -H "Authorization: Bearer $TOKEN"
```

### Workflow 5: Bulk-publish multiple labs in one commit

Use the bulk shape to keep the commit history clean — single commit, atomic.

```bash
curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "files": [
    { "collection": "labs",
      "frontmatter": { "title": "Pods", "track": "cka", "domain": "workloads", "order": 1 },
      "body": "## Why\n\n..." },
    { "collection": "labs",
      "frontmatter": { "title": "Deployments", "track": "cka", "domain": "workloads", "order": 2 },
      "body": "## Why\n\n..." },
    { "collection": "labs",
      "frontmatter": { "title": "Services", "track": "cka", "domain": "services-networking", "order": 1 },
      "body": "## Why\n\n..." }
  ],
  "message": "publish: 3 CKA labs"
}
JSON
```

### Workflow 6: Upload a screenshot, embed in a lab

```bash
URL=$(curl -sS -X POST https://andrewadhikari.com/api/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/Users/me/screenshots/pod-states.png" | jq -r .url)
echo "$URL"
# https://cdn.andrewadhikari.com/img/abc123.png
```

Then embed `![Pod state diagram]($URL)` in the lab body and POST it.

### Workflow 7: Delete a stale lab

```bash
curl -sS -X DELETE \
  https://andrewadhikari.com/api/content/src/content/labs/2026-05-07-pod-lifecycle.md \
  -H "Authorization: Bearer $TOKEN"
```

### Workflow 8: Work-in-progress drafts

When the user says "draft this but don't publish yet" or "stash a half-done
lab", set `draft: true` in frontmatter. Drafts:

- Validate the same way (the file commits to git)
- Are skipped from the public site at build time
- Don't appear in `/labs/`, `/notes/`, search, or course-player listings
- Still show up via `GET /api/content?collection=labs` — Claude can find
  them later

When the draft is ready, PATCH it: `{"frontmatter": {"draft": false, "updated": "2026-05-08"}}`.

### Workflow 9: Touch-up — bump `updated` on every PATCH

When patching a lab/note's body or non-trivial frontmatter (anything beyond
typo-level edits), include `"updated": "<today>"` in the frontmatter merge.
The lesson reader displays `updated <date>` in the meta line. If you don't
bump it, the displayed date rots and the user can't tell when they last
revised the content.

```bash
TODAY=$(date -u +%F)
curl -sS -X PATCH \
  https://andrewadhikari.com/api/content/src/content/labs/2026-05-07-pod-lifecycle.md \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"frontmatter\":{\"updated\":\"$TODAY\"},\"body\":\"## Why\\n\\nUpdated body...\"}"
```

For pure typo fixes (single character / spelling), do NOT bump `updated` —
the date's meant to flag substantive revisions.

### Workflow 10: Log a mistake from a mock or flashcard

Mistakes are first-class content. Log them religiously after every mock and
spaced-repetition session that surfaces a wrong answer.

```bash
curl -sS -X POST https://andrewadhikari.com/api/content \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "collection": "mistakes",
  "frontmatter": {
    "title": "revisionHistoryLimit, not rollbackLimit",
    "track": "cka",
    "domain": "workloads",
    "date": "2026-05-08",
    "source_type": "mock",
    "source_ref": "cka-mock-workloads#q1",
    "correct_answer": "revisionHistoryLimit",
    "my_answer": "rollbackLimit"
  },
  "body": "I keep guessing rollbackLimit. The field is revisionHistoryLimit, default 10.\n"
}
JSON
```

The mistakes journal at `/mistakes/` filters by track + domain, surfaces the
correct answer with a track-accent underline, and is the per-track lesson
page's "1 mistake from this domain · review" link target.

## 7. Conventions Claude should always follow

- **Always include `track` AND `domain` for labs/notes.** Without them the
  lab won't appear in the course player. The lab will publish and validate
  cleanly, but the user will not find it.
- **For YAML collections (`cards`, `mocks`), always include `track`.**
  `domain` is optional but strongly recommended.
- **Don't fabricate track/domain values.** Call `GET /api/tracks` first if
  you don't know them. If a domain doesn't exist on the track, ask the user
  before adding new labs in it (the lab will publish, but it's a dangling
  reference).
- **Domain weights MUST sum to 100** when creating a new track. If you
  can't find official exam-guide weights, distribute evenly and round the
  last domain to absorb the remainder.
- **Set explicit `order` on every lab.** Without it, labs sort by slug
  (alphabetical) and lessons land in the wrong order in the course player.
  Number sequentially within (track, domain): 1, 2, 3, ...
- **Bump `updated` on substantive PATCHes** (body changes, frontmatter
  beyond typos). Skip the bump for single-character edits.
- **Use inline cards (`:::card`) liberally** in lab bodies for any
  memorisable fact (commands, defaults, specific numbers). They drill in
  the SR queue automatically — far better than maintaining parallel
  `cards/<deck>.yaml` files for tightly-coupled content.
- **Use wikilinks (`[[other-slug]]`)** when a lab references another. They
  resolve to internal links and feed the auto-backlinks footer at build
  time. Always verify the slug exists first via `GET /api/content` —
  unknown slugs render with a hairline strike (visible debugging) but
  pollute backlinks.
- **`type: cheatsheet`** for scan-not-read content (CLI flags, quick
  reference). Different layout, different writing style. Don't mix
  cheatsheet content into standard labs.
- **`draft: true`** for in-progress content the user doesn't want public
  yet. Don't ask "should I publish?" — if they said "draft this", stash
  it as a draft and tell them where to find it.
- **Wait for the response and report the `site_url`.** Don't fire and
  forget. The user wants the link.
- **Keep bodies under ~50KB.** GitHub's Contents API has size limits and
  longer markdown becomes a slog. Split large topics across labs with
  `prereq:` chaining (lab B's `prereq: ["lab-a-slug"]` tells the course
  player to gate B until A is marked done).
- **422 means schema fail.** The response body has the Zod error message —
  read it, fix the offending frontmatter field, retry. Don't just retry the
  same request.
- **409 means file exists.** Do not silently retry with `overwrite: true` —
  prompt the user first.
- **Prefer the bulk shape for >2 files** in the same logical commit (e.g.
  "5 labs for the same domain"). Don't bulk across logical boundaries
  (track + labs + cards in one commit) — separate commits give the user
  incremental visibility.
- **The user is `t-rhex`.** If the API returns 403, the token belongs to a
  different account — point them at SETUP.md to regenerate.

## 7a. Browser-side features (NOT operable from this skill)

The site has features that only work via the user's browser session — they
require a passkey login at `/me/login/` and Claude cannot drive them:

- **Highlights, inline notes, bookmarks** — the user selects text on a
  lesson page, picks a color, optionally writes a note. Persists to KV.
  Public visitors see the highlights but only the owner can edit.
- **Reader prefs** (font size, density, width) — per-user localStorage.
- **`/me/` dashboard** — annotation summary, bookmark management.

When the user mentions these ("highlight this passage", "save where I left
off", "make the font bigger"), tell them to use the browser UI directly.
Don't try to call any `/api/annotations/*`, `/api/bookmark`, or `/api/me/*`
endpoint — those are session-cookie gated and your bearer token won't
authenticate them.

## 8. Failure modes (cheatsheet)

| Status | Meaning                                  | What to do                                                       |
| ------ | ---------------------------------------- | ---------------------------------------------------------------- |
| 400    | Malformed body / bad shape               | Re-read the schema in section 4, fix shape, retry                |
| 401    | Missing or invalid auth                  | Tell user to set up token per `SETUP.md`. Don't retry blind.     |
| 403    | Token belongs to another GitHub user     | Wrong PAT. Same fix — regenerate per `SETUP.md`.                 |
| 404    | File not found (PATCH/DELETE/GET)        | Verify the path; list the collection first to see real paths    |
| 409    | File exists, `overwrite: false`          | Confirm with user before retrying with `overwrite: true`         |
| 422    | Frontmatter fails Zod schema             | Read the error message in body, fix the offending field, retry  |
| 500    | Function crashed                         | Retry once; if persistent, tell the user                         |
| 502    | GitHub API upstream error                | Retry once with backoff; if persistent, tell the user            |

## 9. Environment notes

- The site builds via GitHub Actions on every push to `main` and the
  `deploy` job pushes the artifact to Cloudflare Pages. After a successful
  POST/PATCH/DELETE, the pipeline kicks off automatically: ~40s build +
  ~50s deploy + CDN propagation. Always say "queued — live in ~90s",
  never "live now". (The Cloudflare Pages git integration is intentionally
  NOT used — see `.github/workflows/ci.yml` deploy job.)
- Sveltia CMS at `https://andrewadhikari.com/admin/` writes to the same
  repo. Both can be used interchangeably; last write wins on the same file.
  If the user has `/admin/` open while you publish, they'll need to refresh
  to see your changes.
- There is no staging environment. All writes go to production.
- The auth Worker at `auth.andrewadhikari.com` is separate — that's for
  Sveltia's OAuth dance, not for this API. This API uses raw GitHub PATs.
