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
File renamed without changes.
216 changes: 216 additions & 0 deletions .claude/commands/add-solution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
---
name: add-solution
description: >
Generate a structured TypeScript solution file for a challenge.
Accepts any input format: markdown, YAML, HTML, or plain text.
Downloads and converts images to WebP. Produces a file at
src/data/solutions/<adventure-id>/<level-id>.ts that matches
the Solution type in src/data/solutions/types.ts.
---

# Add Solution Command

Generate a structured TypeScript solution walkthrough for an OffOn challenge.

## What this command does

1. Gathers inputs, inferring what it can from the content before asking.
2. Parses the solution input (any format) into structured steps.
3. Downloads any referenced images and converts them to WebP using `cwebp`.
4. Writes `src/data/solutions/<adventure-id>/<level-id>.ts` with the structured content.
5. Runs the generator to update `src/data/solutions/index.ts` and all region patches.
6. Runs the build to verify the output compiles.

---

## Step 0: Gather inputs

If the user already provided solution content (pasted HTML, markdown, plain text, or a file path), treat that as the content — do not ask for it again.

For each remaining input, **first try to infer it from the content** before asking. If a value can be inferred with confidence, show it to the user for confirmation rather than asking a blank question.

| Input | Required | How to infer |
| --- | --- | --- |
| Adventure ID | Yes | Look for the challenge or adventure name in the content (title, headings, kicker). Convert to kebab-case. |
| Level ID | Yes | Look for difficulty words in the content (`beginner`, `intermediate`, `expert`). |
| Contributor name | No | Look for an author credit in the content. If not found, ask: "Who wrote this walkthrough? (leave blank to omit)" |

If adventure ID or level ID cannot be inferred confidently, ask for them directly.

Confirm all inferred values with the user before proceeding. A single confirmation message is better than multiple rounds of questions.

---

## Step 1: Locate the adventure

Find the adventure in `src/data/adventures/index.ts` or the generated files to confirm the IDs are valid:

```bash
grep -r "id: \"<adventure-id>\"" src/data/adventures/
```

Also confirm the level exists within that adventure. If either ID does not match, stop and report it.

---

## Step 2: Parse the input into structured steps

Read the input regardless of format (markdown, YAML, HTML, plain text). Extract:

- **`title`** — solution title, e.g. "Intermediate Solution: Governing the Provinces". Never include difficulty emoji. No em dashes.
- **`contributor`** — `{ name: string }` if a contributor was provided. Omit the field entirely if unknown.
- **`spoilerWarning`** — one sentence. Plain text, no markdown.
- **`intro`** — one or two sentences. Plain text, no markdown.
- **`context`** (optional) — a setup section that explains the tooling or architecture the reader needs to understand before the steps. Title should reflect the specific challenge, not a generic phrase. Use "Understanding the Setup" only if nothing more specific fits.
- **`steps`** — one per numbered objective. Each step:
- `id` — kebab-case, no numbers (e.g. `census-scope`, `isolated-namespaces`)
- `title` — title case, no leading emoji
- `intro` — one to two sentences, plain text
- `body` — array of `SolutionBlock` (see below). Code patches go here as `code` blocks.
- `takeaways` — plain-text strings, sentence case. Preserve all takeaways from the source. Omit only if a step has no non-obvious insight to share.
- `furtherReading` — array of `{ title, url }`. These are aggregated into the sidebar across all steps; do not duplicate a URL across multiple steps.
- **`completeSolution`** (optional) — final corrected config or runbook shown as a summary at the end.
- **`outro`** — narrative closing. End with a sentence that follows the adventure's story world, then invite the reader to see how others solved it. Do not use "Got there by a different route?" or similar generic phrases.

### SolutionBlock types

```typescript
| { type: "text"; html: string } // HTML paragraph or list
| { type: "code"; language: string; title?: string; code: string }
| { type: "image"; src: string; alt: string; caption?: string }
| { type: "callout"; variant: "tip" | "warning" | "info"; html: string }
```

**Text blocks:** Write minimal HTML. Use `<p>`, `<ul>`, `<li>`, `<strong>`, `<code>`, and `<a href="...">` only. No `<h1>`–`<h6>` inside text blocks. No em dashes.

**Callout guidance:**

- `tip` — shortcuts, optional improvements
- `warning` — footguns, things that silently go wrong
- `info` — neutral context that does not fit in the main prose

---

## Step 3: Process images

For every image referenced in the input:

1. **Identify the image URL or local path.**

2. **Download it** to `/tmp/<filename>` using `curl`:

```bash
curl -sL "<url>" -o /tmp/<filename>
```

3. **Convert to WebP** using `cwebp` at quality 85:

```bash
/opt/homebrew/bin/cwebp -q 85 /tmp/<filename> -o "public/solutions/<adventure-id>/<level-basename>.webp"
```

Naming convention: `<level-id>-<descriptive-slug>.webp`. Example: `beginner-no-apps.webp`.

4. **Write alt text:** describe what is shown (not what it means), one sentence under 100 characters, no em dashes, no "image of" or "screenshot of" filler.

5. **Set `src`** in the TypeScript file to `/solutions/<adventure-id>/<filename>.webp`.

If `cwebp` is not available, report the path to the user and skip conversion.

---

## Step 4: Write the TypeScript file

Create `src/data/solutions/<adventure-id>/<level-id>.ts`:

```typescript
import type { Solution } from "@/data/solutions/types";

export const solution: Solution = {
adventureId: "<adventure-id>",
levelId: "<level-id>",
title: "<Title>",
// contributor: { name: "<Name>" }, // include if known; omit entirely if not
spoilerWarning: "<one sentence>",
intro: "<one or two sentences>",
// context: { // include only when needed
// title: "<Specific Title>",
// body: [ /* SolutionBlock[] */ ],
// },
steps: [
{
id: "<kebab-id>",
title: "<Title Case>",
intro: "<plain text>",
body: [ /* SolutionBlock[] */ ],
takeaways: [ /* plain-text strings */ ],
furtherReading: [ /* { title, url }[] */ ],
},
// ... more steps
],
// completeSolution: { // include when there is a single corrected artefact
// title: "Complete <Thing>",
// description: "<one sentence>",
// language: "<yaml|bash|typescript|...>",
// code: `<the full corrected code>`,
// },
outro: {
heading: "<Story-world heading>",
html: "<p><Narrative close following the adventure story.></p><p><Invite to see how others solved it.></p>",
},
};
```

Rules:

- Use template literals for multi-line `code` values.
- No `@ts-ignore`, no `any`.
- Code block content goes in the `code` field as raw text, never in HTML text blocks.
- No em dashes in any string value.
- Optional fields (`contributor`, `context`, `completeSolution`) must be omitted entirely when not used, not set to `null` or `undefined`.

---

## Step 5: Run the generator and verify

```bash
node scripts/generate-solutions.mjs
npm run build
npm run lint
```

If lint or build fail, fix the issues before reporting done.

---

## Step 6: Verify the output

Check the built HTML contains the solution title:

```bash
grep -c "<solution-title-fragment>" dist/client/adventures/<adventure-id>/levels/<level-id>/solution/index.html
```

Use a unique word from the solution title as the fragment. If a contributor was provided, also confirm their name appears:

```bash
grep -c "<contributor-name>" dist/client/adventures/<adventure-id>/levels/<level-id>/solution/index.html
```

Report success with the path if both pass.

---

## Rules

- Never hardcode the community URL. Use the `COMMUNITY_URL` constant.
- Never add `any` types.
- No em dashes anywhere — in alt text, takeaways, prose, or heading strings.
- Code block content goes in the `code` field as raw text, never in `text` HTML blocks.
- Images must be WebP and stored in `public/solutions/<adventure-id>/`.
- Alt text must be specific and informative, not generic.
- Titles use Title Case. Takeaways and body prose use sentence case.
- **Contributor:** include `contributor: { name }` when the walkthrough author is known. Omit the field if unknown. Never fabricate a name.
- **Outro:** always include one. End with a narrative sentence that follows the adventure's story world (characters, setting, mission), then a sentence inviting the reader to see how others solved it. The "Browse the discussion" link in the component handles the CTA itself.
- **Takeaways:** preserve all takeaways from the source. Omit only if a step has no non-obvious insight. Do not cap or trim them.
- **Context section:** include only when the reader needs background on the tooling or architecture before they can understand the steps. If steps are self-contained, omit it.
File renamed without changes.
File renamed without changes.
77 changes: 69 additions & 8 deletions ADVENTURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Adventures live in a separate repo ([open-source-challenges](https://github.com/

## How the Content Pipeline Works

```
```text
off-on-dev/open-source-challenges offon.dev website repo
adventures/<id>/docs/
index.yaml ──── Sync Adventure workflow ────► src/data/adventures/<slug>/adventure.yaml
Expand All @@ -29,7 +29,7 @@ The generated TypeScript files are committed so the dev server works without run
Go to **Actions → Sync Adventure from Challenges Repo → Run workflow**.

| Input | Required | Description |
|---|---|---|
| --- | --- | --- |
| `adventure_url` | Yes | GitHub URL of the adventure folder — any branch works. Main: `https://github.com/off-on-dev/open-source-challenges/tree/main/adventures/05-lex-imperfecta`. PR branch: `https://github.com/off-on-dev/open-source-challenges/tree/feat/my-branch/adventures/05-lex-imperfecta`. |
| `levels` | No | Comma-separated level IDs to make live now (e.g. `beginner` or `beginner,intermediate`). Levels that exist in the challenges repo but are not listed here appear as "Coming Soon" placeholders. Leave blank to make all levels live. |

Expand Down Expand Up @@ -76,7 +76,7 @@ This field also survives future re-syncs once set.

Change `rewards.deadline:` from `TODO` to either an ISO 8601 datetime or the human-readable format used in the challenges repo:

```
```yaml
# ISO 8601 (preferred for direct edits)
rewards.deadline: "2026-07-01T23:59:00+01:00"

Expand All @@ -95,7 +95,7 @@ Each level's `topics:` list defaults to all adventure tags. Refine it to the sub
Once you have created the Discourse thread for a level, use the **Add Discussion URL to Level** workflow (Actions tab → Add Discussion URL to Level → Run workflow).

| Input | Description |
|---|---|
| --- | --- |
| `adventure_id` | Adventure slug, e.g. `lex-imperfecta` |
| `level_id` | `beginner`, `intermediate`, or `expert` |
| `discussion_url` | Full Discourse thread URL, e.g. `https://community.offon.dev/t/slug/1419` |
Expand Down Expand Up @@ -127,7 +127,7 @@ Run this after `community_category_id` is set. It adds the adventure to the lead

**In generate mode** (the default, including the sync workflow): if a value is wrong but an unambiguous match can be found by slug and difficulty, the YAML is patched in place and a warning is printed:

```
```text
Warning: <slug> levels[0]: devcontainer auto-corrected "<wrong>" → "<correct>" — update adventure.yaml in the challenges repo
```

Expand All @@ -137,6 +137,16 @@ If you see this warning, also fix the `devcontainer:` value upstream in the chal

If `gh` is unavailable or unauthenticated, the check is skipped with a warning and generation proceeds.

### Run the a11y audit

After the build passes, run the accessibility audit against any new or changed pages:

```sh
/a11y-audit
```

Target any new adventure or level detail pages. All severity-weighted findings must be resolved before merging.

### Final checks

```sh
Expand All @@ -163,7 +173,7 @@ If the challenges repo is updated while your PR is still open, or you want to pr
### What is preserved on re-sync

| Field | Preserved | Notes |
|---|---|---|
| --- | --- | --- |
| `contributor:` (adventure) | Always | Survives every re-sync once set |
| `community_category_id:` (adventure) | Always | Survives every re-sync once set; position is kept directly after `slug` |
| `month:` (adventure) | Always | Survives every re-sync once set |
Expand All @@ -188,14 +198,65 @@ When a new level is ready in the challenges repo after the first adventure PR ha

---

## Adding a Solution Walkthrough

Solution walkthroughs live in `src/data/solutions/<adventure-id>/<level-id>.ts` and are committed to the repo.

### Use the `/add-solution` skill

The fastest way to add a solution is with the Claude Code skill:

```sh
/add-solution
```

Paste or attach the walkthrough content in any format — markdown, YAML, HTML, or plain text. The skill infers the adventure ID, level ID, and contributor name from the content where possible, confirms them with you, and then:

1. Parses the input into structured steps (`SolutionBlock[]` arrays with text, code, image, and callout blocks).
2. Downloads any referenced images and converts them to WebP at quality 85 using `cwebp`. Images are saved to `public/solutions/<adventure-id>/`.
3. Writes `src/data/solutions/<adventure-id>/<level-id>.ts` with the full typed `Solution` object.
4. Runs `node scripts/generate-solutions.mjs` to rebuild the barrel index and patch region markers.
5. Runs `npm run build` and `npm run lint` to verify the output compiles cleanly.
6. Run `/a11y-audit` against the new solution page to catch any accessibility issues before merging.

### What the generator updates

`scripts/generate-solutions.mjs` scans every `.ts` file in `src/data/solutions/<adventure-id>/` (excluding `index.ts` and `types.ts`) and rebuilds four files automatically:

| File | What gets patched |
| --- | --- |
| `src/data/solutions/index.ts` | Full barrel re-generated: one import per solution file, exported as `SOLUTIONS: Solution[]`. Never edit by hand. |
| `react-router.config.ts` | `GENERATED:solutions` region: one prerender entry per solution route (`/adventures/<id>/levels/<level>/solution`). |
| `src/test/seo.test.ts` | `GENERATED:solutions` region: one route entry per solution. |
| `src/test/prerender.test.ts` | `GENERATED:solutions` region: one `{ file, check }` entry per solution asserting the built HTML contains `"Solution:"`. |
| `e2e/smoke.spec.ts` | `GENERATED:solutions` region: one `{ path, title }` smoke-test entry per solution. |

You do not need to touch any of these files manually when adding a solution.

### Deadline gating

Solutions are not visible on the site until the challenge deadline has passed. The solution page checks `level.deadline` (falling back to `adventure.rewards.deadline`) and renders a locked state with the deadline date until that moment arrives. Once the deadline passes, the page shows the full walkthrough automatically with no code change needed.

This means you can add a solution file to the repo at any point during the challenge period and it will not spoil anything for active participants.

### Output location

```text
src/data/solutions/<adventure-id>/<level-id>.ts ← authored TypeScript (commit this)
public/solutions/<adventure-id>/<level-id>-*.webp ← converted images (commit these)
src/data/solutions/index.ts ← auto-generated barrel (commit this)
```

---

## Workflows at a Glance

| Workflow | Trigger | Purpose |
|---|---|---|
| --- | --- | --- |
| `sync-adventure.yml` | Manual (`workflow_dispatch`) | Sync adventure content from the challenges repo and open or update a PR |
| `add-discussion-url.yml` | Manual (`workflow_dispatch`) | Set a Discourse thread URL for a level after it has been merged, and open a PR with updated YAML and initial posts |
| `validate-adventures.yml` | PR (when adventure files change) | Validate YAML schema, check generated files are up-to-date, verify route/sitemap/prerender consistency |
| `deploy.yml` | Push to `main` | Build and deploy to GitHub Pages at https://offon.dev |
| `deploy.yml` | Push to `main` | Build and deploy to GitHub Pages at [offon.dev](https://offon.dev) |
| `preview.yml` | Open PR | Deploy a PR preview at `/pr-preview/pr-<n>/` |
| `refresh-community-data.yml` | Hourly + manual | Refresh discussion posts, leaderboard data, and community leaders from Discourse |

Expand Down
21 changes: 11 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@ Guidance for AI coding agents working in this repository.

---

## Project Skills
## Project Commands

Project-level Claude Code skills live in `.claude/skills/`. Invoke them with `/skill-name` in Claude Code. These are committed to the repo and available to all contributors.
Project-level Claude Code commands live in `.claude/commands/`. Invoke them with `/command-name` in Claude Code. These are committed to the repo and available to all contributors.

| Skill | When to use |
| Command | When to use |
|---|---|
| `/a11y-audit` | On-demand accessibility audit using the Red Team / Blue Team persona pipeline. Run against a component or page to get a severity-weighted report. Invokes sub-skills below as needed. |
| &nbsp;&nbsp;`/keyboard` | Sub-skill: writing or reviewing any interactive element — buttons, modals, dropdowns, tabs, custom widgets. |
| &nbsp;&nbsp;`/navigation` | Sub-skill: working on nav components — primary nav, skip links, breadcrumbs, pagination, mobile menus. |
| &nbsp;&nbsp;`/progressive-enhancement` | Sub-skill: building any new feature or reviewing architecture. Ensures core content works without JS. |
| &nbsp;&nbsp;`/user-personalization` | Sub-skill: working on theme toggle, consent state, or any user preference persistence. |
| `/a11y-audit` | On-demand accessibility audit using the Red Team / Blue Team persona pipeline. Run against a component or page to get a severity-weighted report. Invokes sub-commands below as needed. |
| &nbsp;&nbsp;`/keyboard` | Sub-command: writing or reviewing any interactive element — buttons, modals, dropdowns, tabs, custom widgets. |
| &nbsp;&nbsp;`/navigation` | Sub-command: working on nav components — primary nav, skip links, breadcrumbs, pagination, mobile menus. |
| &nbsp;&nbsp;`/progressive-enhancement` | Sub-command: building any new feature or reviewing architecture. Ensures core content works without JS. |
| &nbsp;&nbsp;`/user-personalization` | Sub-command: working on theme toggle, consent state, or any user preference persistence. |
| `/add-solution` | Generate a structured TypeScript solution file from any input format (md, YAML, HTML, plain text). Downloads and converts images to WebP. |

The `spec-first-coding` skill is installed globally (`~/.claude/skills/`) and is not in this repo. It enforces W3C spec citations before generating any accessibility-related code.
The `spec-first-coding` command is installed globally (`~/.claude/skills/`) and is not in this repo. It enforces W3C spec citations before generating any accessibility-related code.

Use `/a11y-audit` for all accessibility audits in this repo. The four sub-skills can also be invoked directly when working in their specific domain.
Use `/a11y-audit` for all accessibility audits in this repo. The four sub-commands can also be invoked directly when working in their specific domain.

---

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Adventures are authored as YAML at `src/data/adventures/<id>/adventure.yaml` and
| `/adventures` | `Adventures.tsx` | Adventure landing hub (links to /challenges) |
| `/adventures/:id` | `AdventureDetail.tsx` | Adventure landing |
| `/adventures/:id/levels/:levelId` | `ChallengeDetail.tsx` | Individual challenge |
| `/adventures/:id/levels/:levelId/solution` | `SolutionDetail.tsx` | Solution walkthrough (post-deadline) |
| `/contribute` | `Contribute.tsx` | How to contribute (technical and non-technical ways) |
| `/sponsors` | `Sponsors.tsx` | Sponsorship info |
| `/about` | `About.tsx` | About the community |
Expand Down
Loading
Loading