|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +**Volts** is the community content catalog for Tesla USB customizations — light shows, lock sounds, boombox tracks, wraps, and horn sounds. It is a **Jekyll** site (human-browsable HTML) that **also emits a JSON API** consumed by the companion **VOLTS** Kotlin Multiplatform app (`natobytes/VoltsApp`, the `app/` folder in this workspace). |
| 8 | + |
| 9 | +- Repo: `natobytes/Volts`. Deployed via **GitHub Pages** at `https://natobytes.com/Volts/` (custom domain) — also reachable at `https://natobytes.github.io/Volts/`. |
| 10 | +- The app fetches **`https://natobytes.com/Volts/api/catalog.json`**. The JSON is the contract between this repo and the app — see **Data Contract** below. Don't change its shape without coordinating with `app/.../catalog/CatalogApi.kt`. |
| 11 | +- Two toolchains live side by side: **Ruby/Jekyll** renders the website; **Node/TypeScript** generates the JSON API. They are independent and only joined in CI (`cp -r public/api _site/api`). |
| 12 | + |
| 13 | +## Build & Development Commands |
| 14 | + |
| 15 | +```bash |
| 16 | +# One-time install (both toolchains) |
| 17 | +bundle install # Ruby/Jekyll deps (Gemfile) |
| 18 | +npm install # Node deps for the build/validate scripts (package.json) |
| 19 | + |
| 20 | +# Run the website locally (http://localhost:4000/Volts/) |
| 21 | +bundle exec jekyll serve |
| 22 | + |
| 23 | +# Generate the JSON API into public/api/ (catalog.json, <category>.json, tags.json) |
| 24 | +npm run build # -> ts-node scripts/build.ts |
| 25 | + |
| 26 | +# Validate all content (run this before committing new content) |
| 27 | +npm run validate # -> ts-node scripts/validate.ts |
| 28 | +npm run validate -- --collection lightshows # validate just one category |
| 29 | +``` |
| 30 | + |
| 31 | +> `public/api/` is **gitignored** — the JSON is a build artifact, not committed. It only exists locally after `npm run build`, and in the deployed Pages artifact. |
| 32 | +
|
| 33 | +## Repository Structure |
| 34 | + |
| 35 | +``` |
| 36 | +content/ # Jekyll collections live here (collections_dir: content) |
| 37 | + _lightshows/ _locksounds/ _boombox/ _wraps/ _hornsounds/ |
| 38 | + <slug>.md # item metadata (front matter) — sits BESIDE the folder |
| 39 | + <slug>/ # item's binary assets (.fseq, .mp3, .wav, .png, ...) |
| 40 | + <files> |
| 41 | + tags.yaml # canonical allowed-tags list (validation source of truth) |
| 42 | +_layouts/ # category.html, item.html, tag.html, tags.html, default.html |
| 43 | +_includes/ # nav.html, tag-filter.html |
| 44 | +_plugins/copy_collection_files.rb # copies collection binaries into _site/content/... |
| 45 | +scripts/build.ts # generates public/api/*.json (the app's feed) |
| 46 | +scripts/validate.ts # content linter (required fields, tags, file existence, sizes) |
| 47 | +<category>.md # category index pages (lightshows.md, locksounds.md, ...) |
| 48 | +tags/*.md # one page per tag (mirrors content/tags.yaml — kept in sync by hand) |
| 49 | +_data/tags.yaml # duplicate of content/tags.yaml used by templates |
| 50 | +search.json # Jekyll-rendered client-side search index for the website |
| 51 | +``` |
| 52 | + |
| 53 | +**Identity rule:** an item's **slug is its folder name** (`content/_<category>/<slug>/`). The metadata file must be the sibling `content/_<category>/<slug>.md` — one level **up** from the folder, not inside it. `build.ts` and `validate.ts` both key off the folder name and skip a folder with no matching `<slug>.md`. |
| 54 | + |
| 55 | +## JSON Build Pipeline (`scripts/build.ts`) |
| 56 | + |
| 57 | +`npm run build` reads every `content/_<category>/<slug>/` folder, parses the sibling `<slug>.md` front matter (gray-matter), and writes to `public/api/`: |
| 58 | + |
| 59 | +- **`catalog.json`** — top-level `{ generatedAt, totalItems, lightshows[], locksounds[], boombox[], wraps[], hornsounds[] }`. **This is the file the app consumes.** |
| 60 | +- **`<category>.json`** — per-category `{ category, totalItems, items[] }`. |
| 61 | +- **`tags.json`** — `{ totalTags, tags: { <tag>: ["<category>/<slug>", ...] } }`. |
| 62 | + |
| 63 | +Each catalog **item** is: `slug`, `category`, `title` (defaults to slug), `author` (defaults to `"Unknown"`), `description` (defaults to `""`), `tags[]`, `thumbnail` (string or `null`), `files[]`, and `meta` (the full front matter). `download` URLs are `"<baseurl>/content/<category>/<slug>/<filename>"` where `baseurl` is read from `_config.yml` (`/Volts`). |
| 64 | + |
| 65 | +> ⚠️ **`files[]` is built from a disk glob of the slug folder, NOT from `meta.files`.** `build.ts` lists every file in the folder and emits `{name, downloadUrl}` for each. `meta.files[]` (with human `label`s) is carried under `meta` and separately gets `downloadUrl` injected. So the top-level `files[]` and `meta.files[]` are derived independently — a stray/extra/renamed file in the folder will appear in the feed even if it isn't declared in front matter. The app joins labels from `meta.files[]` back onto `files[]` by `name`. |
| 66 | +
|
| 67 | +## Data Contract (with the VOLTS app) |
| 68 | + |
| 69 | +The app (`app/shared/data/.../catalog/CatalogApi.kt`) fetches `catalog.json`, reads the top-level object, and iterates a **hardcoded** list of category keys (`lightshows`, `locksounds`, `boombox`, `wraps`, `hornsounds`). Per item it reads `slug`, `category`, `title`, `author`, `description`, `tags`, `files[] {name, downloadUrl}`, `thumbnail`, `meta`, and joins per-file `label`s from `meta.files[]`. |
| 70 | + |
| 71 | +Cross-repo invariants to preserve: |
| 72 | +- **Category key names** must stay exactly these five. Adding a new category here is **silently dropped** by the app until the app ships an update with the new key. |
| 73 | +- **`downloadUrl` is root-relative and includes the `/Volts` prefix** (e.g. `/Volts/content/lightshows/<slug>/<slug>.fseq`). The app resolves it against the host **only** (`https://natobytes.com`). Changing `_config.yml`'s `baseurl` silently breaks the app's URL resolution. |
| 74 | +- `meta.files[].label` is currently **required** by `validate.ts`; the app relies on it for display. |
| 75 | + |
| 76 | +## Content Authoring (adding an item) |
| 77 | + |
| 78 | +1. Pick the category. Create the folder `content/_<category>/<slug>/` (slug = lowercase alphanumeric + hyphens) and drop the binary asset(s) in it. |
| 79 | +2. Create the sibling `content/_<category>/<slug>.md` with front matter. Minimum required (enforced by `validate.ts`): |
| 80 | + - `title`, `author`, `description` |
| 81 | + - `files:` — a list of `{ name, label }`, each `name` must exist in the folder |
| 82 | + - `tags:` — every tag must already exist in `content/tags.yaml` (add it there first) |
| 83 | + - `audio: <filename>` — **required** for `lightshows`, `locksounds`, `boombox`, `hornsounds` |
| 84 | + - `thumbnail: <filename>` — **required** for `wraps` (must exist; png/jpg/jpeg/webp/svg) |
| 85 | +3. Allowed file extensions & size caps per category live in `scripts/validate.ts` (`ALLOWED_EXTENSIONS`, `MAX_FILE_SIZE_PER_CATEGORY`: locksounds & wraps 1 MB, else 50 MB). |
| 86 | +4. Run `npm run validate` and fix any errors. See `content/_<category>/README.md` and `.github/PULL_REQUEST_TEMPLATE/<type>.md` for per-type guidance. |
| 87 | + |
| 88 | +Example (`content/_lightshows/thunderstruck-rock-show.md`): |
| 89 | +```yaml |
| 90 | +--- |
| 91 | +title: Thunderstruck Rock Show |
| 92 | +author: Community (XLightShows) |
| 93 | +description: A high-energy rock-themed synchronized light show. |
| 94 | +audio: thunderstruck-rock-show.mp3 |
| 95 | +files: |
| 96 | + - name: thunderstruck-rock-show.fseq |
| 97 | + label: Light Sequence |
| 98 | + - name: thunderstruck-rock-show.mp3 |
| 99 | + label: Audio Track |
| 100 | +tags: [rock, synchronized] |
| 101 | +--- |
| 102 | +``` |
| 103 | + |
| 104 | +## CI / Deployment |
| 105 | + |
| 106 | +- **`.github/workflows/deploy.yml`** — on push to `main`: `npm install` → `npm run build` → `bundle exec jekyll build` → `cp -r public/api _site/api` → deploy `_site` to GitHub Pages. |
| 107 | + > ⚠️ Deploy does **not** run `npm run validate`. A direct push to `main` (or admin merge) can ship an unvalidated catalog. Validation only gates PRs. |
| 108 | +- **`.github/workflows/validate-pr.yml`** + the per-category `validate-<category>.yml` — run `npm run validate` on PRs to `main`. |
| 109 | +- **`.github/workflows/validate-single-component.yml`** — enforces that a PR touches only **one** collection. |
| 110 | +- **Renovate** (`renovate.json`) manages dependency updates. |
| 111 | +- Deployment domain (`natobytes.com`) is configured in the repo's Pages settings; there is **no committed `CNAME`** file. |
| 112 | + |
| 113 | +## Conventions |
| 114 | + |
| 115 | +- Commits/PR titles follow [Conventional Commits](https://www.conventionalcommits.org/) (`.github/semantic.yml` enforces it). Content PRs commonly use the `content:` type/scope. |
| 116 | +- Tags are governed: the allowed set is `content/tags.yaml`. It is **duplicated** in `_data/tags.yaml` and one `tags/<tag>.md` page per tag — keep all three in sync when adding a tag. |
| 117 | +- Validation, not types, is the schema authority. There is no committed JSON Schema; `scripts/validate.ts` + the per-category README/PR templates are the de-facto spec. |
| 118 | + |
| 119 | +## MCP servers |
| 120 | + |
| 121 | +`.mcp.json` configures two servers for this repo: |
| 122 | +- **codegraph** — code-intelligence knowledge graph (indexed; the TS build/validate scripts and the Ruby plugin are in the graph — markdown/Jekyll templates are not parsed). Re-run `codegraph sync` after large changes. |
| 123 | +- **context7** — up-to-date library/framework docs (use for Jekyll, kramdown, gray-matter, ts-node, GitHub Actions). |
0 commit comments