diff --git a/docs/.agents/skills/design-an-interface/SKILL.md b/docs/.agents/skills/design-an-interface/SKILL.md new file mode 100644 index 00000000..d056bd18 --- /dev/null +++ b/docs/.agents/skills/design-an-interface/SKILL.md @@ -0,0 +1,94 @@ +--- +name: design-an-interface +description: Generate multiple radically different interface designs for a module using parallel sub-agents. Use when user wants to design an API, explore interface options, compare module shapes, or mentions "design it twice". +--- + +# Design an Interface + +Based on "Design It Twice" from "A Philosophy of Software Design": your first idea is unlikely to be the best. Generate multiple radically different designs, then compare. + +## Workflow + +### 1. Gather Requirements + +Before designing, understand: + +- [ ] What problem does this module solve? +- [ ] Who are the callers? (other modules, external users, tests) +- [ ] What are the key operations? +- [ ] Any constraints? (performance, compatibility, existing patterns) +- [ ] What should be hidden inside vs exposed? + +Ask: "What does this module need to do? Who will use it?" + +### 2. Generate Designs (Parallel Sub-Agents) + +Spawn 3+ sub-agents simultaneously using Task tool. Each must produce a **radically different** approach. + +``` +Prompt template for each sub-agent: + +Design an interface for: [module description] + +Requirements: [gathered requirements] + +Constraints for this design: [assign a different constraint to each agent] +- Agent 1: "Minimize method count - aim for 1-3 methods max" +- Agent 2: "Maximize flexibility - support many use cases" +- Agent 3: "Optimize for the most common case" +- Agent 4: "Take inspiration from [specific paradigm/library]" + +Output format: +1. Interface signature (types/methods) +2. Usage example (how caller uses it) +3. What this design hides internally +4. Trade-offs of this approach +``` + +### 3. Present Designs + +Show each design with: + +1. **Interface signature** - types, methods, params +2. **Usage examples** - how callers actually use it in practice +3. **What it hides** - complexity kept internal + +Present designs sequentially so user can absorb each approach before comparison. + +### 4. Compare Designs + +After showing all designs, compare them on: + +- **Interface simplicity**: fewer methods, simpler params +- **General-purpose vs specialized**: flexibility vs focus +- **Implementation efficiency**: does shape allow efficient internals? +- **Depth**: small interface hiding significant complexity (good) vs large interface with thin implementation (bad) +- **Ease of correct use** vs **ease of misuse** + +Discuss trade-offs in prose, not tables. Highlight where designs diverge most. + +### 5. Synthesize + +Often the best design combines insights from multiple options. Ask: + +- "Which design best fits your primary use case?" +- "Any elements from other designs worth incorporating?" + +## Evaluation Criteria + +From "A Philosophy of Software Design": + +**Interface simplicity**: Fewer methods, simpler params = easier to learn and use correctly. + +**General-purpose**: Can handle future use cases without changes. But beware over-generalization. + +**Implementation efficiency**: Does interface shape allow efficient implementation? Or force awkward internals? + +**Depth**: Small interface hiding significant complexity = deep module (good). Large interface with thin implementation = shallow module (avoid). + +## Anti-Patterns + +- Don't let sub-agents produce similar designs - enforce radical difference +- Don't skip comparison - the value is in contrast +- Don't implement - this is purely about interface shape +- Don't evaluate based on implementation effort diff --git a/docs/.agents/skills/diagnose/SKILL.md b/docs/.agents/skills/diagnose/SKILL.md new file mode 100644 index 00000000..ed55bda2 --- /dev/null +++ b/docs/.agents/skills/diagnose/SKILL.md @@ -0,0 +1,117 @@ +--- +name: diagnose +description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression. +--- + +# Diagnose + +A discipline for hard bugs. Skip phases only when explicitly justified. + +When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching. + +## Phase 1 — Build a feedback loop + +**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you. + +Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.** + +### Ways to construct one — try them in roughly this order + +1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e. +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network. +5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation. +6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call. +7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. +8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. +9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. +10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you. + +Build the right feedback loop, and the bug is 90% fixed. + +### Iterate on the loop itself + +Treat the loop as a product. Once you have _a_ loop, ask: + +- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) +- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) +- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.) + +A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower. + +### Non-deterministic bugs + +The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable. + +### When you genuinely cannot build a loop + +Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop. + +Do not proceed to Phase 2 until you have a loop you believe in. + +## Phase 2 — Reproduce + +Run the loop. Watch the bug appear. + +Confirm: + +- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix. +- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against). +- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it. + +Do not proceed until you reproduce the bug. + +## Phase 3 — Hypothesise + +Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. + +Each hypothesis must be **falsifiable**: state the prediction it makes. + +> Format: "If is the cause, then will make the bug disappear / will make it worse." + +If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it. + +**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK. + +## Phase 4 — Instrument + +Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.** + +Tool preference: + +1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs. +2. **Targeted logs** at the boundaries that distinguish hypotheses. +3. Never "log everything and grep". + +**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die. + +**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second. + +## Phase 5 — Fix + regression test + +Write the regression test **before the fix** — but only if there is a **correct seam** for it. + +A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence. + +**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase. + +If a correct seam exists: + +1. Turn the minimised repro into a failing test at that seam. +2. Watch it fail. +3. Apply the fix. +4. Watch it pass. +5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario. + +## Phase 6 — Cleanup + post-mortem + +Required before declaring done: + +- [ ] Original repro no longer reproduces (re-run the Phase 1 loop) +- [ ] Regression test passes (or absence of seam is documented) +- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix) +- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location) +- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns + +**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started. diff --git a/docs/.agents/skills/diagnose/scripts/hitl-loop.template.sh b/docs/.agents/skills/diagnose/scripts/hitl-loop.template.sh new file mode 100644 index 00000000..40afc465 --- /dev/null +++ b/docs/.agents/skills/diagnose/scripts/hitl-loop.template.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Human-in-the-loop reproduction loop. +# Copy this file, edit the steps below, and run it. +# The agent runs the script; the user follows prompts in their terminal. +# +# Usage: +# bash hitl-loop.template.sh +# +# Two helpers: +# step "" → show instruction, wait for Enter +# capture VAR "" → show question, read response into VAR +# +# At the end, captured values are printed as KEY=VALUE for the agent to parse. + +set -euo pipefail + +step() { + printf '\n>>> %s\n' "$1" + read -r -p " [Enter when done] " _ +} + +capture() { + local var="$1" question="$2" answer + printf '\n>>> %s\n' "$question" + read -r -p " > " answer + printf -v "$var" '%s' "$answer" +} + +# --- edit below --------------------------------------------------------- + +step "Open the app at http://localhost:3000 and sign in." + +capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)" + +capture ERROR_MSG "Paste the error message (or 'none'):" + +# --- edit above --------------------------------------------------------- + +printf '\n--- Captured ---\n' +printf 'ERRORED=%s\n' "$ERRORED" +printf 'ERROR_MSG=%s\n' "$ERROR_MSG" diff --git a/docs/.agents/skills/grill-me/SKILL.md b/docs/.agents/skills/grill-me/SKILL.md new file mode 100644 index 00000000..bd04394c --- /dev/null +++ b/docs/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,10 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time. + +If a question can be answered by exploring the codebase, explore the codebase instead. diff --git a/docs/.agents/skills/grill-with-docs/ADR-FORMAT.md b/docs/.agents/skills/grill-with-docs/ADR-FORMAT.md new file mode 100644 index 00000000..da7e78ec --- /dev/null +++ b/docs/.agents/skills/grill-with-docs/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/docs/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md b/docs/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md new file mode 100644 index 00000000..eaf2a185 --- /dev/null +++ b/docs/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md @@ -0,0 +1,60 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A one or two sentence description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`. +- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/docs/.agents/skills/grill-with-docs/SKILL.md b/docs/.agents/skills/grill-with-docs/SKILL.md new file mode 100644 index 00000000..5ea0aa91 --- /dev/null +++ b/docs/.agents/skills/grill-with-docs/SKILL.md @@ -0,0 +1,88 @@ +--- +name: grill-with-docs +description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. +--- + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. + +If a question can be answered by exploring the codebase, explore the codebase instead. + + + + + +## Domain awareness + +During codebase exploration, also look for existing documentation: + +### File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). + + diff --git a/docs/.agents/skills/handoff/SKILL.md b/docs/.agents/skills/handoff/SKILL.md new file mode 100644 index 00000000..0aa5b993 --- /dev/null +++ b/docs/.agents/skills/handoff/SKILL.md @@ -0,0 +1,15 @@ +--- +name: handoff +description: Compact the current conversation into a handoff document for another agent to pick up. +argument-hint: "What will the next session be used for?" +--- + +Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the temporary directory of the user's OS - not the current workspace. + +Include a "suggested skills" section in the document, which suggests skills that the agent should invoke. + +Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead. + +Redact any sensitive information, such as API keys, passwords, or personally identifiable information. + +If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly. diff --git a/docs/.agents/skills/improve-codebase-architecture/DEEPENING.md b/docs/.agents/skills/improve-codebase-architecture/DEEPENING.md new file mode 100644 index 00000000..ecaf5d7d --- /dev/null +++ b/docs/.agents/skills/improve-codebase-architecture/DEEPENING.md @@ -0,0 +1,37 @@ +# Deepening + +How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**. + +## Dependency categories + +When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam. + +### 1. In-process + +Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed. + +### 2. Local-substitutable + +Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface. + +### 3. Remote but owned (Ports & Adapters) + +Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter. + +Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."* + +### 4. True external (Mock) + +Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter. + +## Seam discipline + +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection. +- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them. + +## Testing strategy: replace, don't layer + +- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them. +- Write new tests at the deepened module's interface. The **interface is the test surface**. +- Tests assert on observable outcomes through the interface, not internal state. +- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface. diff --git a/docs/.agents/skills/improve-codebase-architecture/HTML-REPORT.md b/docs/.agents/skills/improve-codebase-architecture/HTML-REPORT.md new file mode 100644 index 00000000..8adc368f --- /dev/null +++ b/docs/.agents/skills/improve-codebase-architecture/HTML-REPORT.md @@ -0,0 +1,123 @@ +# HTML Report Format + +The architectural review is rendered as a single self-contained HTML file in the OS temp directory. Tailwind and Mermaid both come from CDNs. Mermaid handles graph-shaped diagrams reliably; hand-built divs and inline SVG handle the more editorial visuals (mass diagrams, cross-sections). Mix the two — don't lean on Mermaid for everything, it'll start to look generic. + +## Scaffold + +```html + + + + + Architecture review — {{repo name}} + + + + + +
+
...
+
...
+
...
+
+ + +``` + +## Header + +Repo name, date, and a compact legend: solid box = module, dashed line = seam, red arrow = leakage, thick dark box = deep module. No introduction paragraph — straight into the candidates. + +## Candidate card + +The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms ([LANGUAGE.md](LANGUAGE.md)) without ceremony. + +Each candidate is one `
`: + +- **Title** — short, names the deepening (e.g. "Collapse the Order intake pipeline"). +- **Badge row** — recommendation strength (`Strong` = emerald, `Worth exploring` = amber, `Speculative` = slate), plus a tag for the dependency category (`in-process`, `local-substitutable`, `ports & adapters`, `mock`). +- **Files** — monospaced list, `font-mono text-sm`. +- **Before / After diagram** — the centrepiece. Two columns, side by side. See patterns below. +- **Problem** — one sentence. What hurts. +- **Solution** — one sentence. What changes. +- **Wins** — bullets, ≤6 words each. e.g. "Tests hit one interface", "Pricing logic stops leaking", "Delete 4 shallow wrappers". +- **ADR callout** (if applicable) — one line in an amber-tinted box. + +No paragraphs of explanation. If the diagram needs a paragraph to be understood, redraw the diagram. + +## Diagram patterns + +Pick the pattern that fits the candidate. Mix them. Don't make every diagram look the same — variety is part of the point. + +### Mermaid graph (the workhorse for dependencies / call flow) + +Use a Mermaid `flowchart` or `graph` when the point is "X calls Y calls Z, and look at the mess." Wrap it in a Tailwind-styled card so it doesn't feel parachuted in. Style with classDef to colour leakage edges red and the deep module dark. Sequence diagrams work well for "before: 6 round-trips; after: 1." + +```html +
+
+    flowchart LR
+      A[OrderHandler] --> B[OrderValidator]
+      B --> C[OrderRepo]
+      C -.leak.-> D[PricingClient]
+      classDef leak stroke:#dc2626,stroke-width:2px;
+      class C,D leak
+  
+
+``` + +### Hand-built boxes-and-arrows (when Mermaid's layout fights you) + +Modules as `
`s with borders and labels. Arrows as inline SVG `` or `` elements positioned absolutely over a relative container. Reach for this when you want the "after" diagram to feel like one thick-bordered deep module with greyed-out internals — Mermaid won't render that with the right weight. + +### Cross-section (good for layered shallowness) + +Stack horizontal bands (`h-12 border-l-4`) to show layers a call passes through. Before: 6 thin layers each doing nothing. After: 1 thick band labelled with the consolidated responsibility. + +### Mass diagram (good for "interface as wide as implementation") + +Two rectangles per module — one for interface surface area, one for implementation. Before: interface rectangle is nearly as tall as the implementation rectangle (shallow). After: interface rectangle is short, implementation rectangle is tall (deep). + +### Call-graph collapse + +Before: a tree of function calls rendered as nested boxes. After: the same tree collapsed into one box, with the now-internal calls shown faded inside it. + +## Style guidance + +- Lean editorial, not corporate-dashboard. Generous whitespace. Serif optional for headings (`font-serif` works well with stone/slate). +- Colour sparingly: one accent (emerald or indigo) plus red for leakage and amber for warnings. +- Keep diagrams ~320px tall so before/after sits comfortably side by side without scrolling. +- Use `text-xs uppercase tracking-wider` for module labels inside diagrams — they should read as schematic, not as UI. +- The only scripts are the Tailwind CDN and the Mermaid ESM import. The report is otherwise static — no app code, no interactivity beyond Mermaid's own rendering. + +## Top recommendation section + +One larger card. Candidate name, one sentence on why, anchor link to its card. That's it. + +## Tone + +Plain English, concise — but the architectural nouns and verbs come straight from [LANGUAGE.md](LANGUAGE.md). Concision is not an excuse to drift. + +**Use exactly:** module, interface, implementation, depth, deep, shallow, seam, adapter, leverage, locality. + +**Never substitute:** component, service, unit (for module) · API, signature (for interface) · boundary (for seam) · layer, wrapper (for module, when you mean module). + +**Phrasings that fit the style:** + +- "Order intake module is shallow — interface nearly matches the implementation." +- "Pricing leaks across the seam." +- "Deepen: one interface, one place to test." +- "Two adapters justify the seam: HTTP in prod, in-memory in tests." + +**Wins bullets** name the gain in glossary terms: *"locality: bugs concentrate in one module"*, *"leverage: one interface, N call sites"*, *"interface shrinks; implementation absorbs the wrappers"*. Don't write *"easier to maintain"* or *"cleaner code"* — those terms aren't in the glossary and don't earn their place. + +No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in [LANGUAGE.md](LANGUAGE.md), reach for one that is before inventing a new one. diff --git a/docs/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md b/docs/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md new file mode 100644 index 00000000..3197723a --- /dev/null +++ b/docs/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md @@ -0,0 +1,44 @@ +# Interface Design + +When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best. + +Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**. + +## Process + +### 1. Frame the problem space + +Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: + +- The constraints any new interface would need to satisfy +- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md)) +- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete + +Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel. + +### 2. Spawn sub-agents + +Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module. + +Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint: + +- Agent 1: "Minimize the interface — aim for 1–3 entry points max. Maximise leverage per entry point." +- Agent 2: "Maximise flexibility — support many use cases and extension." +- Agent 3: "Optimise for the most common caller — make the default case trivial." +- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies." + +Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language. + +Each sub-agent outputs: + +1. Interface (types, methods, params — plus invariants, ordering, error modes) +2. Usage example showing how callers use it +3. What the implementation hides behind the seam +4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md)) +5. Trade-offs — where leverage is high, where it's thin + +### 3. Present and compare + +Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**. + +After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu. diff --git a/docs/.agents/skills/improve-codebase-architecture/LANGUAGE.md b/docs/.agents/skills/improve-codebase-architecture/LANGUAGE.md new file mode 100644 index 00000000..530c2763 --- /dev/null +++ b/docs/.agents/skills/improve-codebase-architecture/LANGUAGE.md @@ -0,0 +1,53 @@ +# Language + +Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point. + +## Terms + +**Module** +Anything with an interface and an implementation. Deliberately scale-agnostic — applies equally to a function, class, package, or tier-spanning slice. +_Avoid_: unit, component, service. + +**Interface** +Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. +_Avoid_: API, signature (too narrow — those refer only to the type-level surface). + +**Implementation** +What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise. + +**Depth** +Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation. + +**Seam** _(from Michael Feathers)_ +A place where you can alter behaviour without editing in that place. The *location* at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it. +_Avoid_: boundary (overloaded with DDD's bounded context). + +**Adapter** +A concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside). + +**Leverage** +What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests. + +**Locality** +What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere. + +## Principles + +- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface. +- **The deletion test.** Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep. +- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape. +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. + +## Relationships + +- A **Module** has exactly one **Interface** (the surface it presents to callers and tests). +- **Depth** is a property of a **Module**, measured against its **Interface**. +- A **Seam** is where a **Module**'s **Interface** lives. +- An **Adapter** sits at a **Seam** and satisfies the **Interface**. +- **Depth** produces **Leverage** for callers and **Locality** for maintainers. + +## Rejected framings + +- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead. +- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know. +- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**. diff --git a/docs/.agents/skills/improve-codebase-architecture/SKILL.md b/docs/.agents/skills/improve-codebase-architecture/SKILL.md new file mode 100644 index 00000000..c12b263b --- /dev/null +++ b/docs/.agents/skills/improve-codebase-architecture/SKILL.md @@ -0,0 +1,81 @@ +--- +name: improve-codebase-architecture +description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable. +--- + +# Improve Codebase Architecture + +Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability. + +## Glossary + +Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md). + +- **Module** — anything with an interface and an implementation (function, class, package, slice). +- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature. +- **Implementation** — the code inside. +- **Depth** — leverage at the interface: a lot of behaviour behind a small interface. **Deep** = high leverage. **Shallow** = interface nearly as complex as the implementation. +- **Seam** — where an interface lives; a place behaviour can be altered without editing in place. (Use this, not "boundary.") +- **Adapter** — a concrete thing satisfying an interface at a seam. +- **Leverage** — what callers get from depth. +- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place. + +Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list): + +- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. +- **The interface is the test surface.** +- **One adapter = hypothetical seam. Two adapters = real seam.** + +This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate. + +## Process + +### 1. Explore + +Read the project's domain glossary and any ADRs in the area you're touching first. + +Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction: + +- Where does understanding one concept require bouncing between many small modules? +- Where are modules **shallow** — interface nearly as complex as the implementation? +- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)? +- Where do tightly-coupled modules leak across their seams? +- Which parts of the codebase are untested, or hard to test through their current interface? + +Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want. + +### 2. Present candidates as an HTML report + +Write a self-contained HTML file to the OS temp directory so nothing lands in the repo. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `/architecture-review-.html` so each run gets a fresh file. Open it for the user — `xdg-open ` on Linux, `open ` on macOS, `start ` on Windows — and tell them the absolute path. + +The report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals — use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual. + +For each candidate, the same template as before, but rendered as a card: + +- **Files** — which files/modules are involved +- **Problem** — why the current architecture is causing friction +- **Solution** — plain English description of what would change +- **Benefits** — explained in terms of locality and leverage, and how tests would improve +- **Before / After diagram** — side-by-side, custom-drawn, illustrating the shallowness and the deepening +- **Recommendation strength** — one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge + +End the report with a **Top recommendation** section: which candidate you'd tackle first and why. + +**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service." + +**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the card (e.g. a warning callout: _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids. + +See [HTML-REPORT.md](HTML-REPORT.md) for the full HTML scaffold, diagram patterns, and styling guidance. + +Do NOT propose interfaces yet. After the file is written, ask the user: "Which of these would you like to explore?" + +### 3. Grilling loop + +Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive. + +Side effects happen inline as decisions crystallize: + +- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/grill-with-docs` (see [CONTEXT-FORMAT.md](../grill-with-docs/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist. +- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there. +- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../grill-with-docs/ADR-FORMAT.md). +- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md). diff --git a/docs/.agents/skills/qa/SKILL.md b/docs/.agents/skills/qa/SKILL.md new file mode 100644 index 00000000..305e43fb --- /dev/null +++ b/docs/.agents/skills/qa/SKILL.md @@ -0,0 +1,130 @@ +--- +name: qa +description: Interactive QA session where user reports bugs or issues conversationally, and the agent files GitHub issues. Explores the codebase in the background for context and domain language. Use when user wants to report bugs, do QA, file issues conversationally, or mentions "QA session". +--- + +# QA Session + +Run an interactive QA session. The user describes problems they're encountering. You clarify, explore the codebase for context, and file GitHub issues that are durable, user-focused, and use the project's domain language. + +## For each issue the user raises + +### 1. Listen and lightly clarify + +Let the user describe the problem in their own words. Ask **at most 2-3 short clarifying questions** focused on: + +- What they expected vs what actually happened +- Steps to reproduce (if not obvious) +- Whether it's consistent or intermittent + +Do NOT over-interview. If the description is clear enough to file, move on. + +### 2. Explore the codebase in the background + +While talking to the user, kick off an Agent (subagent_type=Explore) in the background to understand the relevant area. The goal is NOT to find a fix — it's to: + +- Learn the domain language used in that area (check UBIQUITOUS_LANGUAGE.md) +- Understand what the feature is supposed to do +- Identify the user-facing behavior boundary + +This context helps you write a better issue — but the issue itself should NOT reference specific files, line numbers, or internal implementation details. + +### 3. Assess scope: single issue or breakdown? + +Before filing, decide whether this is a **single issue** or needs to be **broken down** into multiple issues. + +Break down when: + +- The fix spans multiple independent areas (e.g. "the form validation is wrong AND the success message is missing AND the redirect is broken") +- There are clearly separable concerns that different people could work on in parallel +- The user describes something that has multiple distinct failure modes or symptoms + +Keep as a single issue when: + +- It's one behavior that's wrong in one place +- The symptoms are all caused by the same root behavior + +### 4. File the GitHub issue(s) + +Create issues with `gh issue create`. Do NOT ask the user to review first — just file and share URLs. + +Issues must be **durable** — they should still make sense after major refactors. Write from the user's perspective. + +#### For a single issue + +Use this template: + +``` +## What happened + +[Describe the actual behavior the user experienced, in plain language] + +## What I expected + +[Describe the expected behavior] + +## Steps to reproduce + +1. [Concrete, numbered steps a developer can follow] +2. [Use domain terms from the codebase, not internal module names] +3. [Include relevant inputs, flags, or configuration] + +## Additional context + +[Any extra observations from the user or from codebase exploration that help frame the issue — e.g. "this only happens when using the Docker layer, not the filesystem layer" — use domain language but don't cite files] +``` + +#### For a breakdown (multiple issues) + +Create issues in dependency order (blockers first) so you can reference real issue numbers. + +Use this template for each sub-issue: + +``` +## Parent issue + +# (if you created a tracking issue) or "Reported during QA session" + +## What's wrong + +[Describe this specific behavior problem — just this slice, not the whole report] + +## What I expected + +[Expected behavior for this specific slice] + +## Steps to reproduce + +1. [Steps specific to THIS issue] + +## Blocked by + +- # (if this issue can't be fixed until another is resolved) + +Or "None — can start immediately" if no blockers. + +## Additional context + +[Any extra observations relevant to this slice] +``` + +When creating a breakdown: + +- **Prefer many thin issues over few thick ones** — each should be independently fixable and verifiable +- **Mark blocking relationships honestly** — if issue B genuinely can't be tested until issue A is fixed, say so. If they're independent, mark both as "None — can start immediately" +- **Create issues in dependency order** so you can reference real issue numbers in "Blocked by" +- **Maximize parallelism** — the goal is that multiple people (or agents) can grab different issues simultaneously + +#### Rules for all issue bodies + +- **No file paths or line numbers** — these go stale +- **Use the project's domain language** (check UBIQUITOUS_LANGUAGE.md if it exists) +- **Describe behaviors, not code** — "the sync service fails to apply the patch" not "applyPatch() throws on line 42" +- **Reproduction steps are mandatory** — if you can't determine them, ask the user +- **Keep it concise** — a developer should be able to read the issue in 30 seconds + +After filing, print all issue URLs (with blocking relationships summarized) and ask: "Next issue, or are we done?" + +### 5. Continue the session + +Keep going until the user says they're done. Each issue is independent — don't batch them. diff --git a/docs/.agents/skills/request-refactor-plan/SKILL.md b/docs/.agents/skills/request-refactor-plan/SKILL.md new file mode 100644 index 00000000..7e8b2e41 --- /dev/null +++ b/docs/.agents/skills/request-refactor-plan/SKILL.md @@ -0,0 +1,68 @@ +--- +name: request-refactor-plan +description: Create a detailed refactor plan with tiny commits via user interview, then file it as a GitHub issue. Use when user wants to plan a refactor, create a refactoring RFC, or break a refactor into safe incremental steps. +--- + +This skill will be invoked when the user wants to create a refactor request. You should go through the steps below. You may skip steps if you don't consider them necessary. + +1. Ask the user for a long, detailed description of the problem they want to solve and any potential ideas for solutions. + +2. Explore the repo to verify their assertions and understand the current state of the codebase. + +3. Ask whether they have considered other options, and present other options to them. + +4. Interview the user about the implementation. Be extremely detailed and thorough. + +5. Hammer out the exact scope of the implementation. Work out what you plan to change and what you plan not to change. + +6. Look in the codebase to check for test coverage of this area of the codebase. If there is insufficient test coverage, ask the user what their plans for testing are. + +7. Break the implementation into a plan of tiny commits. Remember Martin Fowler's advice to "make each refactoring step as small as possible, so that you can always see the program working." + +8. Create a GitHub issue with the refactor plan. Use the following template for the issue description: + + + +## Problem Statement + +The problem that the developer is facing, from the developer's perspective. + +## Solution + +The solution to the problem, from the developer's perspective. + +## Commits + +A LONG, detailed implementation plan. Write the plan in plain English, breaking down the implementation into the tiniest commits possible. Each commit should leave the codebase in a working state. + +## Decision Document + +A list of implementation decisions that were made. This can include: + +- The modules that will be built/modified +- The interfaces of those modules that will be modified +- Technical clarifications from the developer +- Architectural decisions +- Schema changes +- API contracts +- Specific interactions + +Do NOT include specific file paths or code snippets. They may end up being outdated very quickly. + +## Testing Decisions + +A list of testing decisions that were made. Include: + +- A description of what makes a good test (only test external behavior, not implementation details) +- Which modules will be tested +- Prior art for the tests (i.e. similar types of tests in the codebase) + +## Out of Scope + +A description of the things that are out of scope for this refactor. + +## Further Notes (optional) + +Any further notes about the refactor. + + diff --git a/docs/.agents/skills/review/SKILL.md b/docs/.agents/skills/review/SKILL.md new file mode 100644 index 00000000..7507a362 --- /dev/null +++ b/docs/.agents/skills/review/SKILL.md @@ -0,0 +1,78 @@ +--- +name: review +description: Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/PRD asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X". +--- + +# Review + +Two-axis review of the diff between `HEAD` and a fixed point the user supplies: + +- **Standards** — does the code conform to this repo's documented coding standards? +- **Spec** — does the code faithfully implement the originating issue / PRD / spec? + +Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings. + +The issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if `docs/agents/issue-tracker.md` is missing. + +## Process + +### 1. Pin the fixed point + +Whatever the user said is the fixed point — a commit SHA, branch name, tag, `main`, `HEAD~5`, etc. Don't be opinionated; pass it through. If they didn't specify one, ask: "Review against what — a branch, a commit, or `main`?" Don't proceed until you have it. + +Capture the diff command once: `git diff ...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log ..HEAD --oneline`. + +### 2. Identify the spec source + +Look for the originating spec, in this order: + +1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.) — fetch via the workflow in `docs/agents/issue-tracker.md`. +2. A path the user passed as an argument. +3. A PRD/spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature. +4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available". + +### 3. Identify the standards sources + +Anything in the repo that documents how code should be written. Common locations: + +- `CLAUDE.md`, `AGENTS.md` +- `CONTRIBUTING.md` +- `CONTEXT.md`, `CONTEXT-MAP.md`, per-context `CONTEXT.md` files +- `docs/adr/` (architectural decisions are standards) +- `.editorconfig`, `eslint.config.*`, `biome.json`, `prettier.config.*`, `tsconfig.json` (machine-enforced standards — note them but don't re-check what tooling already checks) +- Any `STYLE.md`, `STANDARDS.md`, `STYLEGUIDE.md`, or similar at the repo root or under `docs/` + +Collect the list of files. The **Standards** sub-agent will read them. + +### 4. Spawn both sub-agents in parallel + +Send a single message with two `Agent` tool calls. Use the `general-purpose` subagent for both. + +**Standards sub-agent prompt** — include: + +- The full diff command and commit list. +- The list of standards-source files you found in step 3. +- The brief: "Read the standards docs. Then read the diff. Report — per file/hunk where relevant — every place the diff violates a documented standard. Cite the standard (file + the rule). Distinguish hard violations from judgement calls. Skip anything tooling enforces. Under 400 words." + +**Spec sub-agent prompt** — include: + +- The diff command and commit list. +- The path or fetched contents of the spec. +- The brief: "Read the spec. Then read the diff. Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words." + +If the spec is missing, skip the Spec sub-agent and note this in the final report. + +### 5. Aggregate + +Present the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings — the two axes are deliberately separate so the user can see them independently. + +End with a one-line summary: total findings per axis, and the worst single issue (if any) flagged. + +## Why two axes + +A change can pass one axis and fail the other: + +- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.** +- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.** + +Reporting them separately stops one axis from masking the other. diff --git a/docs/.agents/skills/teach/GLOSSARY-FORMAT.md b/docs/.agents/skills/teach/GLOSSARY-FORMAT.md new file mode 100644 index 00000000..9cae84c4 --- /dev/null +++ b/docs/.agents/skills/teach/GLOSSARY-FORMAT.md @@ -0,0 +1,35 @@ +# GLOSSARY.md Format + +`GLOSSARY.md` is the canonical language for this teaching workspace. All explainers, exercises, and learning records should adhere to its terminology. Building it is itself part of learning: compressing a concept into a tight definition is evidence the user understands it. + +## Structure + +```md +# {Topic} Glossary + +{One or two sentence description of the topic this glossary covers.} + +## Terms + +**Hypertrophy**: +Muscle growth driven by mechanical tension and metabolic stress over repeated training sessions. +_Avoid_: Bulking, getting big + +**Progressive overload**: +Systematically increasing the demand on a muscle over time — via load, volume, or intensity. +_Avoid_: Pushing harder, levelling up + +**RPE (Rate of Perceived Exertion)**: +A 1–10 self-rating of how hard a set felt, where 10 is failure and 8 means two reps left in the tank. +_Avoid_: Effort score, intensity rating +``` + +## Rules + +- **Add a term only when the user understands it.** The glossary is a record of compressed knowledge, not a dictionary the user reads to learn. If the user has just been introduced to a concept, wait until they can use it correctly before promoting it here. +- **Be opinionated.** When several words exist for the same concept, pick the best one and list the rest as aliases to avoid. This is how language compresses. +- **Keep definitions tight.** One or two sentences. Define what the term IS, not what it does or how to do it. +- **Use the glossary's own terms inside definitions.** Once a term is in the glossary, prefer it everywhere — including inside other definitions. This is what makes complex terms easier to grasp later. +- **Group under subheadings** when natural clusters emerge (e.g. `## Anatomy`, `## Programming`). A flat list is fine when terms cohere. +- **Flag ambiguities explicitly.** If a term is used loosely in the wider field, note the resolution: "In this workspace, 'set' always means a working set — warm-ups are tracked separately." +- **Revise as understanding deepens.** A definition the user wrote in week one may be wrong by week six. Update in place; do not leave stale entries. diff --git a/docs/.agents/skills/teach/LEARNING-RECORD-FORMAT.md b/docs/.agents/skills/teach/LEARNING-RECORD-FORMAT.md new file mode 100644 index 00000000..2faa7c98 --- /dev/null +++ b/docs/.agents/skills/teach/LEARNING-RECORD-FORMAT.md @@ -0,0 +1,46 @@ +# Learning Record Format + +Learning records live in `./learning-records/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. Create the directory lazily — only when the first record is written. + +They are the teaching equivalent of ADRs: they capture non-obvious lessons, key insights, and stated prior knowledge that will steer future sessions. They are used to calculate the zone of proximal development. + +## Template + +```md +# {Short title of what was learned or established} + +{1-3 sentences: what was learned (or what prior knowledge was established), and why it matters for future sessions.} +``` + +That is the whole format. A learning record can be a single paragraph. The value is recording _that_ this is now known and _why_ it changes what to teach next — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most records won't need them. + +- **Status** frontmatter (`active | superseded by LR-NNNN`) — useful when an earlier understanding turns out to be wrong and is replaced. +- **Evidence** — how the user demonstrated the understanding (a question answered, an exercise completed, prior experience cited). Useful when the claim might be revisited. +- **Implications** — what this unlocks or rules out for future sessions. Worth recording when non-obvious. + +## Numbering + +Scan `./learning-records/` for the highest existing number and increment by one. + +## When to write a learning record + +Write one when any of these is true: + +1. **The user demonstrated genuine understanding of something non-trivial** — not just exposure, but evidence they can use the concept correctly. This sets a new floor for what to teach next. +2. **The user disclosed prior knowledge** — "I already know X." Record it so future sessions don't re-teach it. Also record the _depth_ claimed. +3. **A misconception was corrected** — the user previously believed something wrong and now sees why. These are high-value: they predict future stumbling blocks for related topics. +4. **The mission shifted in response to learning** — the user discovered they cared about something different than they thought. Cross-link to [[MISSION.md]] and update it. + +### What does _not_ qualify + +- Material that was merely covered. Coverage is not learning. Wait for evidence. +- Anything already captured tersely in [[GLOSSARY.md]] as a term definition. Don't duplicate. +- Session-by-session activity logs. Learning records are not a journal — they are decision-grade insights. + +## Supersession + +When a later record contradicts an earlier one (the user's understanding deepened or corrected), mark the old record `Status: superseded by LR-NNNN` rather than deleting it. The history of how understanding evolved is itself useful signal. diff --git a/docs/.agents/skills/teach/MISSION-FORMAT.md b/docs/.agents/skills/teach/MISSION-FORMAT.md new file mode 100644 index 00000000..5dac184a --- /dev/null +++ b/docs/.agents/skills/teach/MISSION-FORMAT.md @@ -0,0 +1,31 @@ +# MISSION.md Format + +`MISSION.md` lives at the workspace root. It captures the _reason_ the user is learning this topic. Every teaching decision — what to teach next, which resources to surface, which exercises to design — should trace back to this document. + +## Template + +```md +# Mission: {Topic} + +## Why +{1-3 sentences. The concrete real-world goal the user is chasing. What changes in their life or work when they have this skill? Avoid abstract framings like "to understand X" — push for the underlying outcome.} + +## Success looks like +- {A specific, observable thing the user will be able to do} +- {Another specific thing} +- {…} + +## Constraints +- {Time, budget, prior commitments, learning preferences, anything that bounds the approach} + +## Out of scope +- {Adjacent topics the user explicitly does not want to chase right now — protects the zone of proximal development} +``` + +## Rules + +- **One mission per workspace.** If the user wants to learn two unrelated things, that is two workspaces. +- **Concrete over abstract.** "Run a half marathon by October" beats "get fitter." "Ship a Rust CLI to my team" beats "learn Rust." +- **Push back on vagueness.** If the user cannot articulate why, interview them before writing anything. A bad mission is worse than no mission. +- **Revise when reality shifts.** Missions change. When the user's goal moves, update this file — don't leave a stale mission steering future sessions. +- **Keep it short.** If `MISSION.md` runs past a screen, it has stopped being a compass and started being a plan. diff --git a/docs/.agents/skills/teach/RESOURCES-FORMAT.md b/docs/.agents/skills/teach/RESOURCES-FORMAT.md new file mode 100644 index 00000000..c94aac6a --- /dev/null +++ b/docs/.agents/skills/teach/RESOURCES-FORMAT.md @@ -0,0 +1,32 @@ +# RESOURCES.md Format + +`RESOURCES.md` is the curated set of trusted sources for this topic. Knowledge for explainers should be drawn from here, not from parametric guesses. Wisdom comes from the communities listed here. + +## Structure + +```md +# {Topic} Resources + +## Knowledge + +- [Book: _The Science and Practice of Strength Training_ — Zatsiorsky & Kraemer](https://example.com) + Foundational text on programming and adaptation. Use for: anything to do with periodisation, recovery, intensity zones. +- [Article: "How Much Should I Train?" — Greg Nuckols (Stronger By Science)](https://example.com) + Evidence-based review of volume landmarks. Use for: weekly set targets per muscle group. + +## Wisdom (Communities) + +- [r/weightroom](https://reddit.com/r/weightroom) + High-signal subreddit, moderated against bro-science. Use for: programme critique, plateau troubleshooting. +- Local: Tuesday strength class at {gym name} + Use for: real-time coaching feedback on lifts. +``` + +## Rules + +- **High-trust only.** Prefer primary sources, recognised experts, peer-reviewed work, and communities with strong moderation. If a resource is marketing dressed as education, leave it out. +- **Annotate every entry.** A bare link is useless in three months. Add one line: what it covers and when to reach for it. +- **Group by Knowledge / Wisdom.** Mirrors the philosophy in [SKILL.md](./SKILL.md). It is fine for a resource to appear in only one group. +- **Surface gaps explicitly.** If no good resource exists for an area the mission needs, write a `## Gaps` section listing what is missing. This drives future search. +- **Prune ruthlessly.** A resource that turned out to be wrong, shallow, or off-mission should be removed, not buried. Better five sharp sources than thirty mediocre ones. +- **Record community preferences.** If the user has opted out of joining communities, note it here so future sessions don't keep proposing them. diff --git a/docs/.agents/skills/teach/SKILL.md b/docs/.agents/skills/teach/SKILL.md new file mode 100644 index 00000000..d683bbd3 --- /dev/null +++ b/docs/.agents/skills/teach/SKILL.md @@ -0,0 +1,91 @@ +--- +name: teach +description: Teach the user a new skill or concept, within this workspace. +disable-model-invocation: true +argument-hint: "What would you like to learn about?" +--- + +The user has asked you to teach them something. This is a stateful request - they intend to learn the topic over multiple sessions. + +## Teaching Workspace + +Treat the current directory as a teaching workspace. The state of their learning is captured in this directory in several files: + +- `MISSION.md`: A document capturing the _reason_ the user is interested in the topic. This should be used to ground all teaching. Use the format in [MISSION-FORMAT.md](./MISSION-FORMAT.md). +- `GLOSSARY.md`: A glossary of terminology related to the topic. All workspace files should adhere to this terminology. Use the format in [GLOSSARY-FORMAT.md](./GLOSSARY-FORMAT.md). +- `RESOURCES.md`: A list of resources which can be explored to ground your teaching in contextual knowledge, or to acquire knowledge and wisdom. Use the format in [RESOURCES-FORMAT.md](./RESOURCES-FORMAT.md). +- `./learning-records/*.md`: A directory of learning records, which capture what the user has learned. These are loosely equivalent to architectural decision records in software development - they capture non-obvious lessons and key insights that may need to be revised later, or drive future sessions. These should be used to calculate the zone of proximal development. They are titled `0001-.md`, where the number increments each time. Use the format in [LEARNING-RECORD-FORMAT.md](./LEARNING-RECORD-FORMAT.md). + +## Philosophy + +To learn at a deep level, the user needs three things: + +- **Knowledge**, captured from high-quality, high-trust resources +- **Skills**, acquired through highly-relevant exercises devised by you, based on the knowledge +- **Wisdom**, which comes from interacting with other learners and practitioners + +Before the `RESOURCES.md` is well-populated, your focus should be to find high-quality resources which will help the user acquire knowledge. Never trust your parametric knowledge. + +Some topics may require more skills than knowledge. Learning more about theoretical physics might be more knowledge-based. For yoga, more skills-based. + +## The Mission + +Every teaching session should be tied into the mission - the reason that the user is interested in learning about the topic. + +If the user is unclear about the mission, or the `MISSION.md` is not populated, your first job should be to question the user on why they want to learn this. + +Failing to understand the mission will mean knowledge acquisition is not grounded in real-world goals. Exercises will feel too abstract. You will have no way of judging what the user should do next. + +## Zone Of Proximal Development + +The user should always feel as if they are being challenged 'just enough'. The scope of the topic being taught should feel extremely tight, should be directly tied into their mission. + +The user may specify an exact thing they want to learn. If they don't, figure out their zone of proximal development by: + +- Reading their `learning-records` +- Figuring out the right thing to teach them based on their mission +- Teach the most relevant thing that fits in their zone of proximal development + +A user may tell you that they already know about that topic. If so, record it in their `learning-records`. + +## Glossary + +A key part of acquiring knowledge is compressing knowledge into language. Once a term is known and understood, it can be used and combined in new ways to make more complex terms easier to understand. + +Building the glossary should be done once you feel confident that the user understands the term. Glossaries should use a strict format, and use as concise a definition as possible. + +## Acquiring Knowledge + +Knowledge and skills usually need to be taught as a 1-2 punch. You teach the knowledge first, then get the user to practice the skills via exercises. + +Knowledge should first be gathered from trusted resources, then taught to the user via HTML explainers. These explainers should be beautiful, adhere to the glossary, and be saved to the local file system where they can be reviewed later. + +Explainers should be littered with citations - links to external resources to back up any claim made. + +Explainers should be as interactive as possible, with "try this" callouts to let the user try the knowledge. + +You should make opening the HTML explainer as easy as possible for the user, ideally with a CLI command they can run. + +Once the user has read the knowledge, allow them to ask questions about it. Answer their questions directly, and amend the explainer if needed (or produce another one). + +At this point, you can amend the glossary if it appears clear they understand a term. + +## Acquiring Skills + +Skills should be taught through interactive exercises. There are several tools at your disposal: + +- Interactive HTML explainers, using quizzes and light in-browser exercises +- HTML explainers which guide the user through a list of real-world steps to take (for instance, yoga poses) +- In-agent quizzes, where you ask the user scenario-based questions about what they've learned + +Each exercise should be based on a **feedback loop**, where the user receives feedback on their performance. This feedback loop should be as tight as possible, giving feedback immediately. + +## Acquiring Wisdom + +Wisdom comes from true real-world interaction - testing your skills outside the learning environment. + +When the user asks a question that appears to require wisdom, your default posture should be to attempt to answer - but to ultimately delegate to a **community**. + +A community is a place (online or offline) where the user can test their skills in the real world. This might be a forum, a subreddit, a real-world class (budget permitting) or a local interest group. + +You should attempt to find high-reputation communities the user can join. If the user expresses a preference that they don't want to join a community, respect it. diff --git a/docs/.claude/skills/design-an-interface b/docs/.claude/skills/design-an-interface new file mode 120000 index 00000000..9e0bdace --- /dev/null +++ b/docs/.claude/skills/design-an-interface @@ -0,0 +1 @@ +../../.agents/skills/design-an-interface \ No newline at end of file diff --git a/docs/.claude/skills/diagnose b/docs/.claude/skills/diagnose new file mode 120000 index 00000000..7d4b7c9e --- /dev/null +++ b/docs/.claude/skills/diagnose @@ -0,0 +1 @@ +../../.agents/skills/diagnose \ No newline at end of file diff --git a/docs/.claude/skills/grill-me b/docs/.claude/skills/grill-me new file mode 120000 index 00000000..eea91a86 --- /dev/null +++ b/docs/.claude/skills/grill-me @@ -0,0 +1 @@ +../../.agents/skills/grill-me \ No newline at end of file diff --git a/docs/.claude/skills/grill-with-docs b/docs/.claude/skills/grill-with-docs new file mode 120000 index 00000000..f6cbb9cd --- /dev/null +++ b/docs/.claude/skills/grill-with-docs @@ -0,0 +1 @@ +../../.agents/skills/grill-with-docs \ No newline at end of file diff --git a/docs/.claude/skills/handoff b/docs/.claude/skills/handoff new file mode 120000 index 00000000..a34a6b23 --- /dev/null +++ b/docs/.claude/skills/handoff @@ -0,0 +1 @@ +../../.agents/skills/handoff \ No newline at end of file diff --git a/docs/.claude/skills/improve-codebase-architecture b/docs/.claude/skills/improve-codebase-architecture new file mode 120000 index 00000000..be3dac9e --- /dev/null +++ b/docs/.claude/skills/improve-codebase-architecture @@ -0,0 +1 @@ +../../.agents/skills/improve-codebase-architecture \ No newline at end of file diff --git a/docs/.claude/skills/qa b/docs/.claude/skills/qa new file mode 120000 index 00000000..8826ba1f --- /dev/null +++ b/docs/.claude/skills/qa @@ -0,0 +1 @@ +../../.agents/skills/qa \ No newline at end of file diff --git a/docs/.claude/skills/request-refactor-plan b/docs/.claude/skills/request-refactor-plan new file mode 120000 index 00000000..32190ba0 --- /dev/null +++ b/docs/.claude/skills/request-refactor-plan @@ -0,0 +1 @@ +../../.agents/skills/request-refactor-plan \ No newline at end of file diff --git a/docs/.claude/skills/review b/docs/.claude/skills/review new file mode 120000 index 00000000..d19862ae --- /dev/null +++ b/docs/.claude/skills/review @@ -0,0 +1 @@ +../../.agents/skills/review \ No newline at end of file diff --git a/docs/.claude/skills/teach b/docs/.claude/skills/teach new file mode 120000 index 00000000..83977359 --- /dev/null +++ b/docs/.claude/skills/teach @@ -0,0 +1 @@ +../../.agents/skills/teach \ No newline at end of file diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 82495eeb..6ff5487e 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -1,5 +1,17 @@ # Docs Agent Guidelines +The marketing + documentation site for RocketSim, built with Astro/Starlight. For the domain terminology used across this site's content model (Post, Title, Page title, Featured post), see [CONTEXT.md](./CONTEXT.md). + +## Commands + +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run typecheck` - Type check the codebase +- `npm run format:check` - Check code formatting +- `npm run lint` - Lint the source code +- `npm run knip` - Check for unused dependencies/exports + ## After Making Changes — Required Quality Gate CI will **reject** the PR if any of the checks below fail. Always run every check from the `docs/` directory **before** committing and pushing: @@ -17,6 +29,32 @@ If `format:check` fails, run `npx prettier --write .` and commit the result. All five commands must exit with code 0 before you push. +## Code Style Guidelines + +- Use TypeScript with strict type checking +- Follow Astro recommended ESLint rules +- Format code with Prettier (auto-configured for Astro files) +- Use ES modules for imports/exports +- Component files use `.astro` extension +- Place new components in `/src/components` +- Place new pages in `/src/pages` +- Place static assets in `/public` directory +- Content collections are in `/src/collections` +- Switch statements must use closure brackets for each case: + ```typescript + switch (value) { + case "option1": { + return "result1"; + } + case "option2": { + return "result2"; + } + default: { + return "default"; + } + } + ``` + ## Content Authoring Guide ### Features diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 6ff31a2c..eac8e3d2 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -1,62 +1,5 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +All agent guidance for this project lives in [AGENTS.md](./AGENTS.md) — commands, the required quality gate, available MCP servers, code style, and the content authoring guide. -## Commands - -- `npm run dev` - Start development server -- `npm run build` - Build for production -- `npm run preview` - Preview production build -- `npm run typecheck` - Type check the codebase -- `npm run format:check` - Check code formatting -- `npm run lint` - Lint the source code -- `npm run knip` - Check for unused dependencies/exports - -## Available MCP Servers - -This project has access to MCP (Model Context Protocol) servers that provide additional capabilities: - -- **Astro Docs MCP** (`mcp__astro-docs__search_astro_docs`): Search official Astro documentation - - Use this proactively when working with Astro features, Starlight configuration, or answering questions about Astro best practices - - Helpful for understanding Astro/Starlight APIs, configuration options, and recommended patterns - - Example: When implementing new Starlight features or troubleshooting Astro-specific issues - -To use an MCP server tool: - -1. First load it with ToolSearch: `query: "select:mcp__astro-docs__search_astro_docs"` -2. Then call the tool directly - -## After Making Changes - -Always run these checks after modifying files in this project: - -1. `npm run lint` - Verify ESLint passes -2. `npm run format:check` - Verify Prettier formatting -3. `npm run typecheck` - Verify TypeScript types -4. `npm run build` - Verify production build succeeds - -## Code Style Guidelines - -- Use TypeScript with strict type checking -- Follow Astro recommended ESLint rules -- Format code with Prettier (auto-configured for Astro files) -- Use ES modules for imports/exports -- Component files use `.astro` extension -- Place new components in `/src/components` -- Place new pages in `/src/pages` -- Place static assets in `/public` directory -- Content collections are in `/src/collections` -- Switch statements must use closure brackets for each case: - ```typescript - switch (value) { - case "option1": { - return "result1"; - } - case "option2": { - return "result2"; - } - default: { - return "default"; - } - } - ``` +For domain terminology (the site's content model), see [CONTEXT.md](./CONTEXT.md). diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md new file mode 100644 index 00000000..97968a47 --- /dev/null +++ b/docs/CONTEXT.md @@ -0,0 +1,23 @@ +# RocketSim Docs Site + +The marketing + documentation site for RocketSim, built with Astro/Starlight. This glossary captures the language specific to this site's content model. + +## Language + +### Blog + +**Post**: +An authored article in the blog, stored as a single MDX entry in the `blog` content collection. One post = one co-located folder under `src/content/blog//`. +_Avoid_: Article (reserve "article" for the structured-data / schema.org type), entry. + +**Title**: +The short, SEO-facing name of a post. Used for the HTML `` tag and the listing/overview card. +_Avoid_: Headline (that maps to schema.org and may differ — see Page title). + +**Page title**: +The on-page `<h1>` of a post, and the structured-data `headline`. May be deliberately longer than the Title for SEO reasons (e.g. Title "How to Test VoiceOver on the Xcode Simulator" vs Page title "…Without a Physical Device"). Falls back to Title when not set. +_Avoid_: Subtitle, H1. + +**Featured post**: +The single most-recent post, rendered as the large hero card at the top of the blog overview. The remaining posts render in the grid below. +_Avoid_: Highlighted, pinned (there is no manual pinning — "featured" is purely the latest by date). diff --git a/docs/astro.config.ts b/docs/astro.config.ts index 61ba3d86..205d27f8 100644 --- a/docs/astro.config.ts +++ b/docs/astro.config.ts @@ -8,10 +8,70 @@ import starlight from "@astrojs/starlight"; import starlightLlmsTxt from "starlight-llms-txt"; import sitemap from "@astrojs/sitemap"; +import rehypeSlug from "rehype-slug"; +import rehypeAutolinkHeadings from "rehype-autolink-headings"; +import type { Plugin, Transformer } from "unified"; +import type { Element, Root } from "hast"; import { llmsTxtPostProcess } from "./src/integrations/llms-txt-post-process"; import config from "./src/config/config.json"; +// Heading anchors are generated at build time for blog posts only. Starlight +// already manages slugs/anchors for the docs collection, so we scope these +// plugins to files under `src/content/blog` to avoid double-processing docs. +const scopeToBlog = + <S extends unknown[]>( + plugin: Plugin<S, Root>, + ...settings: S + ): Plugin<[], Root> => + function () { + const transformer = plugin.call(this, ...settings) as Transformer< + Root, + Root + >; + // Keep this wrapper arity-2 so unified treats it as a synchronous + // transformer. The wrapped plugins (slug/autolink) mutate the tree + // synchronously and ignore the `next` callback. + return (tree, file) => { + if (!file?.path?.includes("/content/blog/")) return; + transformer(tree, file, () => undefined); + }; + }; + +// Link icon prepended to each heading anchor (sized via `.heading-anchor svg`). +const headingAnchorIcon: Element = { + type: "element", + tagName: "svg", + properties: { + xmlns: "http://www.w3.org/2000/svg", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + strokeWidth: 2, + strokeLinecap: "round", + strokeLinejoin: "round", + ariaHidden: "true", + }, + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71", + }, + children: [], + }, + { + type: "element", + tagName: "path", + properties: { + d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71", + }, + children: [], + }, + ], +}; + // https://astro.build/config export default defineConfig({ site: config.site.base_url, @@ -51,6 +111,19 @@ export default defineConfig({ entrypoint: "astro/assets/services/sharp", }, }, + markdown: { + rehypePlugins: [ + scopeToBlog(rehypeSlug), + scopeToBlog(rehypeAutolinkHeadings, { + behavior: "prepend", + properties: { + className: ["heading-anchor"], + ariaLabel: "Link to this section", + }, + content: headingAnchorIcon, + }), + ], + }, // TODO: fix later vite: { plugins: [tailwindcss() as any] }, integrations: [ diff --git a/docs/docs/adr/0001-blog-content-local-mdx.md b/docs/docs/adr/0001-blog-content-local-mdx.md new file mode 100644 index 00000000..ec7d8306 --- /dev/null +++ b/docs/docs/adr/0001-blog-content-local-mdx.md @@ -0,0 +1,17 @@ +# Blog content moved from WordPress to local MDX + +The blog previously fetched posts at build time from a WordPress instance (`cms.rocketsim.app`) via the WP REST API — both the overview (`/blog`) and detail pages (`/blog/[slug]`), plus feature "Learn more" links that resolved a WordPress post ID to a slug. We migrated all posts into local MDX in the `blog` content collection (`src/content/blog/<slug>/index.mdx`, one self-contained folder per post) and removed the WordPress dependency entirely. + +**Why:** the WordPress instance is being decommissioned, and a build-time fetch to an external CMS is a fragility (the build breaks if WP is down or slow) and an ownership liability (content lives outside version control). Local MDX makes posts version-controlled, reviewable, and buildable with no external dependency. + +## Consequences + +- We lose the WordPress authoring UI; posts are now authored as MDX in the repo. +- Post folders are treated as **immutable snapshots** — shared/doc images are duplicated into each post folder rather than referenced, so updating a docs screenshot never silently alters a published post. +- Feature → blog links changed from a numeric `blogId` (resolved via a WP fetch) to a direct `blogSlug`. Slug renames now silently break those links, but they are easy to grep. +- `jsdom` and `github-slugger` (used only to post-process WordPress HTML) were removed; heading anchors are now generated by rehype plugins at build time. + +## Considered options + +- **Keep WordPress** — rejected: the instance is being decommissioned. +- **Migrate to another headless CMS** — rejected: with only a handful of posts and a single author, a CMS adds operational overhead for no benefit over version-controlled MDX. diff --git a/docs/package-lock.json b/docs/package-lock.json index f6b94a8a..e9b3b746 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -15,11 +15,12 @@ "aos": "^2.3.4", "astro": "^5.16.6", "date-fns": "^4.1.0", - "github-slugger": "^2.0.0", "marked": "^16.4.1", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-slug": "^6.0.0", "starlight-llms-txt": "^0.7.0" }, "devDependencies": { @@ -27,83 +28,21 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.18", - "@types/jsdom": "^27.0.0", + "@types/hast": "^3.0.4", "@types/node": "^24.7.0", "@typescript-eslint/parser": "^8.50.0", "@wordpress/env": "^10.36.0", "astro-auto-import": "^0.4.4", "eslint": "^9.39.2", "eslint-plugin-astro": "^1.5.0", - "jsdom": "^27.3.0", "knip": "^5.75.1", "prettier": "^3.7.4", "prettier-plugin-astro": "^0.14.1", "tailwindcss": "^4.0.15", - "typescript": "^5.9.2" - } - }, - "node_modules/@acemir/cssom": { - "version": "0.9.29", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.29.tgz", - "integrity": "sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@asamuzakjp/css-color": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", - "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.4" + "typescript": "^5.9.2", + "unified": "^11.0.5" } }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.6", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", - "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.4" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@astrojs/check": { "version": "0.9.6", "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.6.tgz", @@ -734,141 +673,6 @@ "node": ">=18" } }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.21.tgz", - "integrity": "sha512-plP8N8zKfEZ26figX4Nvajx8DuzfuRpLTqglQ5d0chfnt35Qt3X+m6ASZ+rG0D0kxe/upDVNwSIVJP5n4FuNfw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@ctrl/tinycolor": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", @@ -3880,6 +3684,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", "dependencies": { "@types/unist": "*" } @@ -3896,18 +3701,6 @@ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "license": "MIT" }, - "node_modules/@types/jsdom": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", - "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4006,12 +3799,6 @@ "@types/node": "*" } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -4328,15 +4115,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4850,16 +4628,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5542,40 +5310,12 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, - "node_modules/cssstyle": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz", - "integrity": "sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^4.1.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "peer": true }, - "node_modules/data-urls": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", - "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", - "dev": true, - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -5601,12 +5341,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true - }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -6887,6 +6621,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-is-body-ok-link": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", @@ -7190,18 +6937,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -7231,19 +6966,6 @@ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -7257,19 +6979,6 @@ "node": ">=10.19.0" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/i18next": { "version": "23.16.8", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", @@ -7506,13 +7215,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -7581,59 +7283,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "27.3.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.3.0.tgz", - "integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@acemir/cssom": "^0.9.28", - "@asamuzakjp/dom-selector": "^6.7.6", - "cssstyle": "^5.3.4", - "data-urls": "^6.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.1.0", - "ws": "^8.18.3", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -10356,6 +10005,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-expressive-code": { "version": "0.41.6", "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.41.6.tgz", @@ -10454,6 +10121,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-stringify": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", @@ -10836,18 +10520,6 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -11271,12 +10943,6 @@ "url": "https://opencollective.com/svgo" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -11358,24 +11024,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tldts": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz", - "integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==", - "dev": true, - "dependencies": { - "tldts-core": "^7.0.16" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz", - "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==", - "dev": true - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11387,30 +11035,6 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "dev": true, - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -11579,6 +11203,7 @@ "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -12282,18 +11907,6 @@ "dev": true, "license": "MIT" }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/walk-up-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", @@ -12321,61 +11934,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", - "dev": true, - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", - "dev": true, - "dependencies": { - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12500,42 +12058,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/docs/package.json b/docs/package.json index f57c19c7..13466728 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,11 +22,12 @@ "aos": "^2.3.4", "astro": "^5.16.6", "date-fns": "^4.1.0", - "github-slugger": "^2.0.0", "marked": "^16.4.1", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-slug": "^6.0.0", "starlight-llms-txt": "^0.7.0" }, "devDependencies": { @@ -34,18 +35,18 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.18", - "@types/jsdom": "^27.0.0", + "@types/hast": "^3.0.4", "@types/node": "^24.7.0", "@typescript-eslint/parser": "^8.50.0", "@wordpress/env": "^10.36.0", "astro-auto-import": "^0.4.4", "eslint": "^9.39.2", "eslint-plugin-astro": "^1.5.0", - "jsdom": "^27.3.0", "knip": "^5.75.1", "prettier": "^3.7.4", "prettier-plugin-astro": "^0.14.1", "tailwindcss": "^4.0.15", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "unified": "^11.0.5" } } diff --git a/docs/public/blog/metadata-recording.mp4 b/docs/public/blog/metadata-recording.mp4 new file mode 100644 index 00000000..63d07375 Binary files /dev/null and b/docs/public/blog/metadata-recording.mp4 differ diff --git a/docs/src/collections/feature/01-network-monitoring.md b/docs/src/collections/feature/01-network-monitoring.md index af356ea1..3ef6a0f3 100644 --- a/docs/src/collections/feature/01-network-monitoring.md +++ b/docs/src/collections/feature/01-network-monitoring.md @@ -3,7 +3,7 @@ showOnHomepage: true name: "Network Monitor" youtubeLink: "https://www.youtube.com/watch?v=ihVwU9usxgQ" tagLine: "Inspect, summarize, and debug network traffic in realtime" -blogId: 20 +blogSlug: "monitor-urlsession-network-requests-without-the-pain-of-custom-certificates" featurePage: "networking" asset: type: "video" diff --git a/docs/src/collections/feature/31-network-ai-prompts.md b/docs/src/collections/feature/31-network-ai-prompts.md index 81bacc7c..29bf2cd2 100644 --- a/docs/src/collections/feature/31-network-ai-prompts.md +++ b/docs/src/collections/feature/31-network-ai-prompts.md @@ -1,7 +1,7 @@ --- showOnHomepage: false name: "Network AI Prompts" -blogId: 32 +blogSlug: "introducing-ai-prompts-and-recording-metadata" blogFragment: "exporting-network-requests-ai-prompts" featurePage: "networking" asset: diff --git a/docs/src/config/config.json b/docs/src/config/config.json index 28b2f2a8..2f0f0b49 100644 --- a/docs/src/config/config.json +++ b/docs/src/config/config.json @@ -13,9 +13,6 @@ "settings": { "sticky_header": true }, - "blog": { - "base_url": "https://cms.rocketsim.app" - }, "params": { "copyright": "Copyright © {year} RocketSim. All Rights Reserved." }, diff --git a/docs/src/config/menu.json b/docs/src/config/menu.json index a236cbab..834b55ab 100644 --- a/docs/src/config/menu.json +++ b/docs/src/config/menu.json @@ -12,6 +12,10 @@ "name": "Pricing", "url": "/pricing" }, + { + "name": "Blog", + "url": "/blog" + }, { "name": "Docs", "url": "https://www.rocketsim.app/docs" @@ -44,6 +48,10 @@ "name": "Pricing", "url": "/pricing" }, + { + "name": "Blog", + "url": "/blog" + }, { "name": "Students", "url": "/student" diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts index a5173c26..e5da129f 100644 --- a/docs/src/content.config.ts +++ b/docs/src/content.config.ts @@ -32,7 +32,7 @@ const feature = defineCollection({ showOnHomepage: z.boolean().default(false), tagLine: z.string().optional(), docPath: z.string().optional(), - blogId: z.number().optional(), + blogSlug: z.string().optional(), blogFragment: z.string().optional(), youtubeLink: z.string().url().optional(), featurePage: z @@ -75,6 +75,33 @@ const docs = defineCollection({ schema: docsSchema(), }); +const blog = defineCollection({ + // One self-contained folder per post: src/content/blog/<slug>/index.mdx. + // The slug is the folder name, so we strip the trailing `/index` the glob + // loader would otherwise include in the generated id. + loader: glob({ + pattern: "**/index.{md,mdx}", + base: "./src/content/blog", + generateId: ({ entry }) => entry.replace(/\/index\.mdx?$/, ""), + }), + schema: ({ image }) => + z.object({ + // SEO-facing title: HTML <title> and the listing/overview card. + title: z.string(), + // On-page <h1> and structured-data headline. May be longer than `title` + // for SEO; falls back to `title` when omitted. + pageTitle: z.string().optional(), + description: z.string(), + publishedTime: z.coerce.date(), + // Falls back to `publishedTime` when omitted. + modifiedTime: z.coerce.date().optional(), + // Optional hero image, rendered at the top of the post and used as the + // Open Graph image. When omitted, the site's default banner is used. + image: image().optional(), + imageAlt: z.string().optional(), + }), +}); + const featurePage = defineCollection({ loader: glob({ pattern: "**/[^_]*.md", @@ -99,4 +126,5 @@ export const collections = { statisticsSectionCollection, trustedBrandsSectionCollection, docs, + blog, }; diff --git a/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/index.mdx b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/index.mdx new file mode 100644 index 00000000..039a3927 --- /dev/null +++ b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/index.mdx @@ -0,0 +1,116 @@ +--- +title: "RocketSim 15: VoiceOver testing in the Simulator and better Xcode Simulator recordings" +description: RocketSim 15 adds VoiceOver Navigator, Elements Overlay, Tinted Liquid Glass testing, a new post editor for Xcode Simulator recordings, networking improvements, and the first RocketSim CLI beta. +publishedTime: 2026-04-09T09:05:24.000Z +image: ./voiceover-overlay-hero.png +imageAlt: RocketSim VoiceOver Navigator showing numbered accessibility elements directly on top of the iOS Simulator. +--- + +import { Image } from "astro:assets"; +import voiceOverNavigation from "./voiceover-navigation.png"; +import liquidGlassCompare from "./liquid-glass-compare.jpg"; +import postEditorVideo from "./post-editor.png"; +import keychainResetButton from "./reset-keychain-button.png"; +import networkingTools from "./networking-tools.jpeg"; +import rocketsimCLI from "./rocketsim-cli.png"; + +**VoiceOver testing in the Simulator** used to be one of those jobs I would postpone until the very end. You grab a device, enable VoiceOver, start swiping, lose context, and then go back to the Simulator to fix the issue you just found. RocketSim 15 changes that loop completely. + +On top of that, this release makes **Xcode Simulator recordings** much more useful after capture. Instead of retaking a screenshot or video because the framing is slightly off, you can now post-edit the result directly inside RocketSim. Combined with a dedicated networking tools tab, a keychain reset action, localized App Store pages, and the first RocketSim CLI beta, this is easily the biggest RocketSim release I have shipped so far. + +## VoiceOver testing in the Simulator + +The headline feature in RocketSim 15 is [VoiceOver Navigator](/docs/features/accessibility/voiceover-navigator). It lets you move through accessibility elements using your keyboard, directly in the Simulator. Arrow keys move through the current VoiceOver order, Enter activates the selected element, and the overlay updates as your app changes state. + +What I like most is that this is not just another inspector. You can still click elements and inspect their accessibility metadata, but the real win is speed. You immediately see whether the reading order makes sense, whether a heading lands where you expect it to, and whether activating a control moves focus in a sensible way. + +<figure> + <Image + src={voiceOverNavigation} + alt="VoiceOver Navigator overlay visualizing accessibility element order in RocketSim." + class="w-full rounded-2xl" + /> +</figure> + +Accessibility work in iOS often becomes much easier once you can see the structure instead of only hearing it. That is exactly what the new Elements Overlay gives you. If you care about shipping more accessible apps, this feature is worth checking out on its own. + +RocketSim 15 also adds direct testing for [Tinted Liquid Glass](/docs/features/accessibility/toggles-and-dynamic-text#tinted-liquid-glass-testing). Liquid Glass looks great in demos, but the accessibility story matters just as much. Being able to compare both states in the Simulator is a much faster way to validate contrast and readability while you iterate. + +<figure> + <Image + src={liquidGlassCompare} + alt="Comparison of an app before and after enabling the Tinted Liquid Glass accessibility toggle in RocketSim." + class="w-full rounded-2xl" + /> +</figure> + +## Editing Xcode Simulator recordings after capture + +The second big improvement in RocketSim 15 is the new [post editor](/docs/features/capturing/post-editor) for screenshots and videos. If you record Xcode Simulator videos for release notes, social media, demos, or App Store assets, you probably know how often a capture is technically correct but still not quite ready to share. + +The new editor fixes that. You can trim the recording, adjust the styling, tweak the frame, change the output ratio, update metadata, and preview everything live before exporting. That means one good base recording can now become several polished variants without you needing to capture the whole flow again. + +<figure> + <Image + src={postEditorVideo} + alt="RocketSim Capture Editor open for a recording, showing live preview, trim controls, and export settings." + class="w-full rounded-2xl" + /> +</figure> + +I think this is especially useful for teams that rely on Xcode Simulator recordings as part of their development workflow. You can keep your capture loop fast, but still end up with an output that looks intentional. If you want to see it in action first, I also published a [short demo video](https://www.youtube.com/watch?v=3LlGj6oVi7A). + +<Youtube + url="https://www.youtube.com/watch?v=3LlGj6oVi7A" + title="RocketSim Post Editor demo" +/> + +## Smaller updates that speed up testing + +RocketSim 15 also includes a few smaller changes that matter a lot in day-to-day work. The new [Reset Keychain](/docs/features/app-actions/quick-actions) action is one of those features you start using once and then wonder why it did not exist earlier. Authentication testing often goes wrong because of stale credentials, and resetting the Simulator keychain is much faster than manually clearing app state over and over again. + +<figure> + <Image + src={keychainResetButton} + alt="RocketSim Recent Builds side window showing the Reset Keychain quick action." + class="w-full rounded-2xl" + /> +</figure> + +Networking tools also moved into a dedicated tab. That sounds small, but it makes both discovery and repeated usage better. Features like network speed control and the network monitor now feel like part of one workflow instead of scattered utilities. + +<figure> + <Image + src={networkingTools} + alt="RocketSim networking tools overview with request details and network inspection controls." + class="w-full rounded-2xl" + /> +</figure> + +If you regularly test under poor connectivity, inspect requests, or debug networking issues in the Simulator, this new structure should save you a few clicks every day. Sometimes that is exactly the kind of improvement that makes a tool feel much more polished. + +## The first RocketSim CLI beta + +I also bundled the first version of the RocketSim CLI in this release. It only supports a few commands today, but the direction is clear: I want to make it easier for your agents and automation workflows to interact with the Simulator in a secure, sandboxed way. + +<figure> + <Image + src={rocketsimCLI} + alt="RocketSim CLI help output showing simulator, elements, and interact subcommands." + class="w-full rounded-2xl" + /> +</figure> + +I am particularly excited about this because I used an agentic coding setup heavily while building RocketSim 15 itself. The foundation of RocketSim has always been solid, but the current tooling finally lets me move at a speed that was simply not realistic before. If your team is already deep into agentic coding, feel free to reach out if you want to join the beta. + +## Localized pages and student access + +Two more updates are worth calling out. First, RocketSim now has localized App Store pages in Chinese, French, German, and Spanish. After visiting Let's Vision in Shanghai, it became clear to me that RocketSim needed to do a better job outside English-only messaging. + +Second, students can now get RocketSim Pro for free for one year through the [student program](/student). Professional tooling made a big difference early in my own career, so this felt like the right moment to give something back. + +## Conclusion + +RocketSim 15 is a release focused on faster feedback loops. VoiceOver testing in the Simulator becomes practical enough to use throughout development, and Xcode Simulator recordings become much easier to polish after capture. The rest of the release continues that same pattern: fewer interruptions, less repetitive setup, and quicker access to the tools you actually use. + +If you already use RocketSim, I recommend installing the latest update from the [Mac App Store](https://apps.apple.com/app/apple-store/id1504940162?pt=117264678&ct=april-newsletter&mt=8) and trying the accessibility and capture workflows first. If you have feature ideas or feedback, feel free to [open an issue on GitHub](https://github.com/AvdLee/RocketSimApp/issues). Thanks! diff --git a/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/liquid-glass-compare.jpg b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/liquid-glass-compare.jpg new file mode 100644 index 00000000..15da6e3b Binary files /dev/null and b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/liquid-glass-compare.jpg differ diff --git a/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/networking-tools.jpeg b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/networking-tools.jpeg new file mode 100644 index 00000000..b12daf63 Binary files /dev/null and b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/networking-tools.jpeg differ diff --git a/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/post-editor.png b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/post-editor.png new file mode 100644 index 00000000..fc1010b6 Binary files /dev/null and b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/post-editor.png differ diff --git a/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/reset-keychain-button.png b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/reset-keychain-button.png new file mode 100644 index 00000000..d79f14fe Binary files /dev/null and b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/reset-keychain-button.png differ diff --git a/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/rocketsim-cli.png b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/rocketsim-cli.png new file mode 100644 index 00000000..ddb5dae0 Binary files /dev/null and b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/rocketsim-cli.png differ diff --git a/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/voiceover-navigation.png b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/voiceover-navigation.png new file mode 100644 index 00000000..2805ac74 Binary files /dev/null and b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/voiceover-navigation.png differ diff --git a/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/voiceover-overlay-hero.png b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/voiceover-overlay-hero.png new file mode 100644 index 00000000..404e580f Binary files /dev/null and b/docs/src/content/blog/15-voiceover-navigator-pro-xcode-simulator-recordings/voiceover-overlay-hero.png differ diff --git a/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/index.mdx b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/index.mdx new file mode 100644 index 00000000..3225cea3 --- /dev/null +++ b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/index.mdx @@ -0,0 +1,110 @@ +--- +title: How to Test VoiceOver on the Xcode Simulator +pageTitle: How to Test VoiceOver on the Xcode Simulator (Without a Physical Device) +description: Test VoiceOver on the Xcode Simulator without a physical device. Use a numbered overlay, keyboard shortcuts, and the rotor to verify iOS accessibility fast. +publishedTime: 2026-05-28T09:00:00.000Z +image: ./voiceover-overlay-hero.png +imageAlt: VoiceOver testing on the Xcode Simulator with RocketSim, showing numbered accessibility elements overlaid on the iPhone Simulator and the Voice Over Elements list. +--- + +import { Image } from "astro:assets"; +import voiceOverOverlay from "./voiceover-overlay.png"; +import voiceOverNavigation from "./voiceover-navigation.png"; +import liquidGlassCompare from "./liquid-glass-compare.jpg"; + +**Testing VoiceOver on the Xcode Simulator** is one of those tasks every iOS developer agrees is important, and most developers postpone until the last minute. The Xcode Simulator does not ship with a real VoiceOver runtime, so the usual answer is "grab a physical iPhone, enable VoiceOver in Settings, and start swiping." That works, but it pulls you out of your normal development flow and slows down every small fix. + +This article walks through the practical options you have today. You will see what Xcode itself offers, why the physical-device workflow is still painful, and how RocketSim turns VoiceOver testing into a keyboard-driven step you can run inside the Simulator while you develop. The goal is simple: make accessibility checks fast enough that you actually run them. + +## Why VoiceOver testing belongs in your daily iOS workflow + +VoiceOver is the most visible accessibility feature on iOS, and it is the one most likely to break silently when you change a layout. Adding a wrapping `VStack` or renaming an icon button can flip the reading order in a way you never see visually, but a VoiceOver user will hit immediately. The earlier you catch it, the cheaper it is to fix. + +In my experience, the main reason accessibility regressions ship is not a lack of care — it is friction. If verifying a single screen takes ten minutes of device juggling, you will not do it on every pull request. If it takes ten seconds in the Simulator, you will. That is the loop this article tries to give you. + +## What you can (and cannot) do with VoiceOver in the Xcode Simulator + +The honest answer is that **the Xcode Simulator does not run real VoiceOver**. There is no Settings toggle that turns it on the way you would on an iPhone, and the triple-click side-button shortcut does not exist either. The Simulator is a great development tool, but it is not designed to be a screen reader sandbox. + +What you do have is [Xcode's Accessibility Inspector](https://developer.apple.com/documentation/accessibility/accessibility-inspector). It can target a running Simulator, list accessibility elements, highlight them on hover, and run audits for missing labels or low contrast. It is genuinely useful for debugging a specific element, but it is not a substitute for stepping through the app the way VoiceOver actually does. You see metadata, not behavior. + +That gap — inspecting elements versus moving through them — is exactly the reason most developers fall back to a physical device once they want to verify a full screen or flow. + +## The physical-device workflow (and why it slows you down) + +On a physical iPhone, you enable VoiceOver from **Settings → Accessibility → VoiceOver** and bind the Accessibility Shortcut to a triple-click of the side button. From there you swipe right and left to move through elements, double-tap to activate, and rotate two fingers to switch rotor categories. It works, and it is the most accurate way to experience your app the way a VoiceOver user would. + +The problem is the development loop around it. You build and deploy to the device, switch focus from Xcode to your phone, enable VoiceOver, find the bug, disable VoiceOver so you can use the keyboard again, walk back to Xcode, change one line, and repeat. Every iteration breaks your context. + +You will still want to validate critical flows on a real device before shipping. But for the dozens of small fixes you make while building — reordering elements, adding labels, grouping a card, tagging a heading — the device-based loop is too slow to use as your default. + +## Testing VoiceOver on the Xcode Simulator with RocketSim + +[RocketSim's VoiceOver Navigator](/docs/features/accessibility/voiceover-navigator) closes that gap. It runs alongside the Xcode Simulator and shows a numbered overlay on every accessibility element in the exact order VoiceOver would announce them. You see the reading order without listening to the reading order — which is usually what you actually need while you are coding. + +<figure> + <Image + src={voiceOverOverlay} + alt="RocketSim VoiceOver Elements Overlay on the iOS Simulator showing numbered accessibility elements, the rotor dropdown, and the Voice Over Elements list with roles like StaticText and Button." + class="w-full rounded-2xl" + /> + <figcaption> + The Elements Overlay shows every accessibility element on top of the Xcode + Simulator, in VoiceOver order. + </figcaption> +</figure> + +From the side window you turn on **Elements Overlay** and pick a rotor category from the dropdown — for example **All Elements**, **Headings**, or **Form Controls**. Each element gets a number and a row in the list with its role, so you can immediately see whether a heading lands where you expect it or whether a static text element accidentally became a button. The element count at the bottom is a useful sanity check on its own. + +Because the overlay is rendered live, it updates as your app updates. Push a new view, hit **Refresh**, and the numbering re-runs. There is no device build, no toggling, no triple-click — you stay on the Mac, in your editor. + +## Navigating elements with the keyboard, like a real VoiceOver swipe + +Inspecting elements is useful, but VoiceOver is fundamentally about **movement**. To replicate that loop on the Simulator, click **Start Navigating** in the Voice Over panel. The overlay stays on screen and the focus highlight jumps between elements as you press the arrow keys. + +<figure> + <Image + src={voiceOverNavigation} + alt="RocketSim VoiceOver Navigator in navigation mode on the Xcode Simulator, with the keyboard shortcuts panel showing arrow keys, Enter, and Esc, and the currently focused Watchlist button highlighted." + class="w-full rounded-2xl" + /> + <figcaption> + Navigation mode highlights the focused element and lists the keyboard + shortcuts for moving through the rotor. + </figcaption> +</figure> + +The shortcuts map directly onto VoiceOver gestures: + +- **↑ / ↓** — Move to the previous or next element in VoiceOver order, exactly like swiping left or right on device. +- **← / →** — Switch rotor category, the way a two-finger rotate would on a real iPhone. +- **⏎ Enter** — Activate the focused element, similar to a double-tap; the app navigates and the overlay updates with the new screen. +- **Esc** — Exit navigation mode and return to the overlay-only view. + +This is the part that makes accessibility testing feel like part of the build cycle rather than a separate phase. You step through a screen, spot a missing label or a wrong rotor group, fix it in Xcode, hit **Refresh**, and verify in seconds. No cable, no headphones, no losing your place. + +## Combine VoiceOver with the rest of your iOS accessibility checks + +VoiceOver is the headline feature, but accessibility on iOS is a stack: Dynamic Type, Bold Text, Increase Contrast, Reduce Motion, Reduce Transparency, and now Tinted Liquid Glass. A screen that reads well in VoiceOver can still fail badly at the largest Dynamic Type size or under Increase Contrast. + +From the same side window, RocketSim's [Environment Overrides](/docs/features/accessibility/environment-overrides) give you direct toggles for those settings, plus a slider for every [Dynamic Type](/docs/features/accessibility/toggles-and-dynamic-text) size. So once you finish a VoiceOver pass, you can flip on Bold Text, crank up Dynamic Type, and check the same screen again — without going back into Simulator Settings. + +<figure> + <Image + src={liquidGlassCompare} + alt="Side-by-side comparison of an iOS app on the Xcode Simulator before and after enabling Tinted Liquid Glass through RocketSim's accessibility toggles." + class="w-full rounded-2xl" + /> + <figcaption> + Tinted Liquid Glass is one of several accessibility toggles you can flip + from the same panel as VoiceOver Navigator. + </figcaption> +</figure> + +If you treat VoiceOver as one stop in a short accessibility checklist — VoiceOver order, headings, Dynamic Type, contrast — you will catch most of the regressions that normally slip through until App Review or a user report. The full set of [RocketSim accessibility features](/features/accessibility) is built around exactly this kind of fast iteration loop on the Xcode Simulator. + +## Conclusion + +For a long time, "test VoiceOver on the Xcode Simulator" really meant "give up and use a physical device." That is no longer true. With a numbered Elements Overlay, a keyboard-driven navigator, and a rotor that matches the device experience, you can verify the reading order and activation flow of an iOS app without leaving the Simulator. A physical device is still the right place for a final pass, but it should not be your default anymore. + +If you want to try this on your own app, install [RocketSim from the Mac App Store](https://apps.apple.com/app/apple-store/id1504940162?pt=117264678&ct=voiceover-article&mt=8) and open the **Voice Over** tab in the side window. You can also dig into the full [VoiceOver Navigator documentation](/docs/features/accessibility/voiceover-navigator) for the keyboard shortcuts and rotor details. Feel free to reach out on [X/Twitter](https://x.com/twannl) or [open an issue on GitHub](https://github.com/AvdLee/RocketSimApp/issues) if you run into anything or have feature ideas. Thanks! diff --git a/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/liquid-glass-compare.jpg b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/liquid-glass-compare.jpg new file mode 100644 index 00000000..15da6e3b Binary files /dev/null and b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/liquid-glass-compare.jpg differ diff --git a/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-navigation.png b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-navigation.png new file mode 100644 index 00000000..2805ac74 Binary files /dev/null and b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-navigation.png differ diff --git a/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-overlay-hero.png b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-overlay-hero.png new file mode 100644 index 00000000..404e580f Binary files /dev/null and b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-overlay-hero.png differ diff --git a/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-overlay.png b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-overlay.png new file mode 100644 index 00000000..bef1e358 Binary files /dev/null and b/docs/src/content/blog/how-to-test-voiceover-on-the-xcode-simulator/voiceover-overlay.png differ diff --git a/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/build-insights.png b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/build-insights.png new file mode 100644 index 00000000..34d5a6e1 Binary files /dev/null and b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/build-insights.png differ diff --git a/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/index.mdx b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/index.mdx new file mode 100644 index 00000000..604b2782 --- /dev/null +++ b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/index.mdx @@ -0,0 +1,168 @@ +--- +title: Introducing AI prompts and recording metadata +description: RocketSim 14.5 introduces AI prompts generated from the Network Monitor and recording metadata overlays for your captures, plus faster, more reliable build monitoring. +publishedTime: 2026-01-19T11:58:13Z +--- + +import { Image } from "astro:assets"; +import networkPrompts from "./network-prompts.png"; +import showMetadataToggle from "./show-metadata-toggle.png"; +import metadataSettings from "./metadata-settings.png"; +import buildInsights from "./build-insights.png"; + +The 14.5.0 release is now officially [available in the App Store](https://apps.apple.com/app/apple-store/id1504940162?pt=117264678&ct=blog&mt=8) and introduces two significant new features. With AI prompts generated from the Network Monitor you'll have a first way to benefit from data collected inside RocketSim. Recording metadata allows you to promote your apps with a higher chance of success. + +## Exporting Network Requests AI Prompts + +We're preparing for the future of RocketSim, in which we bring your Agent tooling closer to the Simulator. RocketSim collects data such as build insights and network requests, which can be incredibly valuable when AI has access to them. We imagine using AI to improve networking, build performance, app design, and more. + +The first step is a feature available today: Network Request Prompts. + +<figure> + <Image + src={networkPrompts} + alt="Export prompts using your network requests from within RocketSim's Network Monitor." + class="w-full rounded-2xl" + /> + <figcaption> + Export prompts using your network requests from within RocketSim's Network + Monitor. + </figcaption> +</figure> + +We're starting with 3 prompts you can pick from: + +1. **Redundant calls & caching** — Analyze redundant calls and caching opportunities. +2. **Performance & overfetching** — Analyze performance, overfetching, timeouts, and payload bloat. Highlight the largest bytes out, slowest durations, and fingerprints with varying bytes out. +3. **Failures & error spikes** — Analyze failures and error spikes, focusing on non-2xx; hypothesize causes (auth, validation, rate limits, server errors) and list next debug steps. + +This is just the beginning, and we'd love to hear from you on how we can improve these prompts. Maybe you even know additional prompts we can add. + +### Privacy-first with heavy redaction + +The first thing you might wonder is how we share the network requests within the prompt. Your request contains sensitive data, such as authentication tokens and user data. We don't want to leak them into any AI tool. + +Therefore, we focused heavily on redaction, only sharing what matters for the task we're executing. Here's an example prompt: + +```text +Analyze redundant calls and caching opportunities. Group by fingerprint; provide table columns: count, cache hint, dedupe window, memoization key. Then recommend batching/conditional requests: + +Summary: 2026-01-16T13:27:25.378Z – 2026-01-16T13:27:40.109Z | requests: 21 | fingerprints: 14 | errors: 1 | slow(>500ms): 0 +2026-01-16T13:27:27.363Z GET www.googleapis.com /youtube/v3/videos?{id,part} 200 72ms in:- out:80 KB ct:application/json; charset=UTF-8 #87ada2 + stats: count:4 min:72ms p50:124ms max:172ms +2026-01-16T13:27:26.741Z GET www.googleapis.com /youtube/v3/channels?{id,part} 304 225ms in:- out:- ct:application/json; charset=UTF-8 #5a0cba + stats: count:4 min:53ms p50:83ms max:225ms +2026-01-16T13:27:27.008Z GET www.googleapis.com /youtube/v3/commentThreads?{allThreadsRelatedToChannelId,maxResults,order,part,textFormat} 304 318ms in:- out:- ct:application/json; charset=UTF-8 #66c52b + stats: count:2 min:318ms p50:435ms max:435ms +2026-01-16T13:27:27.363Z GET www.googleapis.com /youtube/v3/playlistItems?{maxResults,pageToken,part,playlistId} 304 224ms in:- out:- ct:application/json; charset=UTF-8 #a85d56 + stats: count:1 min:224ms p50:224ms max:224ms +2026-01-16T13:27:40.109Z GET yt3.ggpht.com /ytc/AIdro_mr4ubik6szRQk0IT4R1Wpxv5L0fy6UtUb9krE7YUwkLqg=s48-c-k-c0x00ffffff-no-rj 200 46ms in:- out:940 bytes ct:image/jpeg #0d021e + stats: count:1 min:46ms p50:46ms max:46ms +2026-01-16T13:27:25.378Z POST oauth2.googleapis.com /token 200 291ms in:275 bytes out:444 bytes ct:application/json; charset=utf-8 #047182 + stats: count:1 min:291ms p50:291ms max:291ms +2026-01-16T13:27:26.796Z GET www.googleapis.com /youtube/v3/channels?{mine,part} 304 143ms in:- out:- ct:application/json; charset=UTF-8 #a46ca8 + stats: count:1 min:143ms p50:143ms max:143ms +2026-01-16T13:27:40.104Z GET yt3.ggpht.com /e2ogn5CTP8TmDlhVn6M7scf97mstLYBAYEC8uf8t6AXUNTmOQ98P8cQuVLcLLxUIb6d7OJAa=s48-c-k-c0x00ffffff-no-rj 200 419ms in:- out:2 KB ct:image/jpeg #d42cb5 + stats: count:1 min:419ms p50:419ms max:419ms +2026-01-16T13:27:27.779Z GET youtubeanalytics.googleapis.com /v2/reports?{dimensions,endDate,filters,ids,maxResults,metrics,sort,startDate} 200 400ms in:- out:4 KB ct:application/json; charset=UTF-8 #df4d4c + stats: count:1 min:400ms p50:400ms max:400ms +2026-01-16T13:27:40.107Z GET yt3.ggpht.com /ytc/AIdro_ljEM7oAi8W-J_NFkpQGj5ejMOoMsYhzLp3slvSxVQ=s48-c-k-c0x00ffffff-no-rj 200 95ms in:- out:826 bytes ct:image/jpeg #9bdcf6 + stats: count:1 min:95ms p50:95ms max:95ms +2026-01-16T13:27:40.106Z GET yt3.ggpht.com /ytc/AIdro_kGCfFJCiyz8FnynrAN_I4Fji6c4UK7KorjRzmVrhrYoA=s48-c-k-c0x00ffffff-no-rj 200 244ms in:- out:2 KB ct:image/jpeg #008429 + stats: count:1 min:244ms p50:244ms max:244ms +2026-01-16T13:27:40.108Z GET yt3.ggpht.com /ytc/AIdro_nD-SrZRfwve4qS5Dh8PD8Ep2LL3JuOvaLHdNVhzcGSDk8=s48-c-k-c0x00ffffff-no-rj 200 56ms in:- out:2 KB ct:image/jpeg #6423a2 + stats: count:1 min:56ms p50:56ms max:56ms +2026-01-16T13:27:27.016Z GET www.googleapis.com /youtube/v3/playlistItems?{maxResults,part,playlistId} 304 288ms in:- out:- ct:application/json; charset=UTF-8 #5ce196 + stats: count:1 min:288ms p50:288ms max:288ms +2026-01-16T13:27:40.102Z GET yt3.ggpht.com /ytc/AIdro_mWmGoeDeFCP9ZIxycZxvst6RG4-UFSNC8TycQ41y2FVa0j=s48-c-k-c0x00ffffff-no-rj 200 256ms in:- out:2 KB ct:image/jpeg #920d56 + stats: count:1 min:256ms p50:256ms max:256ms +``` + +Nothing special in this prompt, or at least, no sensitive data. We optimized the requests for AI to analyze. For example, it contains a fingerprint (e.g. #920d56), which makes it easy to find duplicates. + +### Reducing token usage + +If we shared raw request data, your tokens and context would be easily filled in. We reduced output as much as possible, while retaining enough information for the prompt to be useful. + +We'd love for you to try this out and [let us know on GitHub](https://github.com/AvdLee/RocketSimApp) in case you find any improvements. + +## Showing metadata on top of screenshots and recordings + +Another wish I've had for a while is to showcase the app's title and website whenever I share a recording of my app. There's now a new toggle in the side window that allows you to show metadata on top of captures: + +<figure> + <Image + src={showMetadataToggle} + alt='A new "Show metadata" option is now available in the Side Window.' + class="w-full rounded-2xl" + /> + <figcaption> + A new "Show metadata" option is now available in the Side Window. + </figcaption> +</figure> + +Once enabled, RocketSim will try to fill in the metadata based on your most recent build. Additionally, you can provide the metadata to use in the settings for your app's bundle identifier: + +<figure> + <Image + src={metadataSettings} + alt="New metadata options can be configured inside App Groups." + class="w-full rounded-2xl" + /> + <figcaption> + New metadata options can be configured inside App Groups. + </figcaption> +</figure> + +Eventually, once configured, your captures can look like this: + +<figure> + <video controls playsinline class="w-full rounded-2xl"> + <source src="/blog/metadata-recording.mp4" type="video/mp4" /> + </video> + <figcaption> + An example RocketSim recording with the metadata visible in the bottom-left + corner. + </figcaption> +</figure> + +## Build monitoring performance improvements + +Less visible, but just as important, are the performance improvements we made. RocketSim monitors your builds to provide build insights: + +<figure> + <Image + src={buildInsights} + alt="RocketSim Build Insights showing recent builds with their durations and whether they were incremental or clean." + class="w-full rounded-2xl" + /> +</figure> + +Analyzing your builds used to be quite CPU-heavy, even though we only needed to know whether your build was incremental or clean. In some cases, if you opened RocketSim after a while, RocketSim could run with 100% CPU to analyze all the builds it didn't read before. + +It also turned out that Xcode 26.2 changed the build log format, making it harder for us to determine the build type. Therefore, we revisited this logic completely, resulting in a more reliable analysis pipeline that will never reach 100% CPU. It's lightning fast! + +## Get started today + +[Get the update from the App Store](https://apps.apple.com/app/apple-store/id1504940162?pt=117264678&ct=blog&mt=8) and get started with these new changes today. + +### Full Changelog + +**New:** + +- Side window shows a clear icon now for the Network Monitoring quick view. +- You can now configure metadata to appear above captures. See the new metadata toggle in the side window. +- The Network Monitor console allows you to export network requests for AI evaluation, including predefined prompts. +- Network requests in the Network Monitor console can now be navigated using arrow keys (up/down) when a cell is selected. + +**Improved:** + +- Monitoring system settings like dark mode is now more efficient. + +**Fixed:** + +- Simulator Camera dropdown no longer appears empty +- Preview popover now scales to the available screen with padding so captures always fit. +- Builds created by Xcode 26.2 are now properly monitored. +- Optimized the performance of monitoring new builds and prevented duplicate parsing of build logs. +- Settings sidebar can no longer collapse. diff --git a/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/metadata-settings.png b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/metadata-settings.png new file mode 100644 index 00000000..6a4f50a2 Binary files /dev/null and b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/metadata-settings.png differ diff --git a/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/network-prompts.png b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/network-prompts.png new file mode 100644 index 00000000..f5d19830 Binary files /dev/null and b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/network-prompts.png differ diff --git a/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/show-metadata-toggle.png b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/show-metadata-toggle.png new file mode 100644 index 00000000..221b153c Binary files /dev/null and b/docs/src/content/blog/introducing-ai-prompts-and-recording-metadata/show-metadata-toggle.png differ diff --git a/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/failed-network-request.png b/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/failed-network-request.png new file mode 100644 index 00000000..5f5b3f7e Binary files /dev/null and b/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/failed-network-request.png differ diff --git a/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/index.mdx b/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/index.mdx new file mode 100644 index 00000000..4f7f8093 --- /dev/null +++ b/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/index.mdx @@ -0,0 +1,106 @@ +--- +title: Monitor URLSession Network Requests without the pain of custom certificates +description: Monitor URLSession network requests without installing custom certificates. RocketSim's Network Monitor connects over Bonjour so you can inspect every request and response while you develop. +publishedTime: 2025-10-08T17:43:58Z +modifiedTime: 2025-10-15T09:39:01Z +--- + +import { Image } from "astro:assets"; +import networkMonitorResponse from "./network-monitor-response.png"; +import failedNetworkRequest from "./failed-network-request.png"; +import sideWindowOverview from "./side-window-overview.png"; + +Configuring a proxy using many of the popular apps like Proxyman and Charles Proxy often requires you to install custom certificates. It's when you're running into an unexpected network failure when you've realized you didn't have the proxy running. + +We knew this workflow could be improved and aimed to design a new solution: + +- No need for custom certificates +- Easy setup that always work +- A consistent network requests monitor to catch those issues when you don't expect them + +The result is what we call "Network Monitoring" and it's demonstrated in the following video: + +<Youtube + url="https://www.youtube.com/watch?v=ihVwU9usxgQ" + title="Network Traffic Monitor - Proxy the iOS Simulator without needing certificates" +/> + +## Setting up RocketSim's Network Monitor + +RocketSim works by connecting via Bonjour with your development app. The in-app setup guides you by providing a piece of code that will only run during debug builds. + +Here's an example **(note: you need to use the code from the app, not this example)**: + +```swift +private func loadRocketSimConnect() { + #if DEBUG + guard (Bundle(path: "/Applications/RocketSim.app/Contents/Frameworks/RocketSimConnectLinker.nocache.framework")?.load() == true) else { + print("Failed to load linker framework") + return + } + print("RocketSim Connect successfully linked") + #endif +} +``` + +As you can see, the method will only run for debug builds due to the `#if DEBUG`. Secondly, this code works for every engineer in your team in case you're using [Team Licenses](https://www.rocketsim.app/team-insights). At least, as long as they've installed RocketSim inside the `/Applications` directory, which we recommend! + +Once you've copied the code, it's time to run your app. Every time when you launch your app, RocketSim will connect directly. From that point on, all your network requests will be monitored in the background. **There's no impact on your app's performance.** + +The latter is important to point out. We simply swizzle your network requests and send it over Bonjour in a background thread. + +## Your debugging partner when the unexpected happens + +I'm sure you recognize this: + +- You're running your app during regular feature development +- An edge case occurs and suddenly your app doesn't work as expected +- The app's failure hints to an unexpected response failure, but you have no way to find out which request, or in which order they fired + +These cases are no longer an issue. Now you can go back in time, find the exact order of requests that fired, and explore their responses. + +<figure> + <Image + src={networkMonitorResponse} + alt="RocketSim's Network Monitor in action, showing the response for one of the requests. A failed network request is also visible." + class="w-full rounded-2xl" + /> + <figcaption> + RocketSim's Network Monitor in action, showing the response for one of the + requests. A failed network request is also visible. + </figcaption> +</figure> + +Here's an example of such a scenario. The screenshot displays the JSON response of the successful request, but we can also see that the other request failed. Selecting that request shows that it failed due to a network error: + +<figure> + <Image + src={failedNetworkRequest} + alt="A failed URLSession network request as displayed inside RocketSim's Network Monitor." + class="w-full rounded-2xl" + /> + <figcaption> + A failed URLSession network request as displayed inside RocketSim's Network + Monitor. + </figcaption> +</figure> + +You don't always have to dive into detail, though. We also show a quick overview in the side window: + +<figure> + <Image + src={sideWindowOverview} + alt="The side window shows a quick overview of requests fired from your app using URLSession." + class="w-full rounded-2xl" + /> + <figcaption> + The side window shows a quick overview of requests fired from your app using + URLSession. + </figcaption> +</figure> + +## Download RocketSim today and get started + +You can [get RocketSim from the Mac App Store](https://apps.apple.com/app/apple-store/id1504940162?pt=117264678&ct=network-monitoring-rs-article&mt=8), which allows you to try out the network monitor for free. + +Enjoy! diff --git a/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/network-monitor-response.png b/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/network-monitor-response.png new file mode 100644 index 00000000..f361f567 Binary files /dev/null and b/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/network-monitor-response.png differ diff --git a/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/side-window-overview.png b/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/side-window-overview.png new file mode 100644 index 00000000..46d2c938 Binary files /dev/null and b/docs/src/content/blog/monitor-urlsession-network-requests-without-the-pain-of-custom-certificates/side-window-overview.png differ diff --git a/docs/src/layouts/Base.astro b/docs/src/layouts/Base.astro index d3d3d855..2214f34e 100755 --- a/docs/src/layouts/Base.astro +++ b/docs/src/layouts/Base.astro @@ -21,7 +21,7 @@ const { seo, structuredData } = Astro.props; --- <!doctype html> -<html lang="en"> +<html lang="en" data-theme="dark"> <head> <!-- favicon --> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> diff --git a/docs/src/layouts/components/Feature.astro b/docs/src/layouts/components/Feature.astro index e9d46fa7..27135efd 100644 --- a/docs/src/layouts/components/Feature.astro +++ b/docs/src/layouts/components/Feature.astro @@ -3,51 +3,20 @@ import { Image } from "astro:assets"; import { marked } from "marked"; import type { CollectionEntry } from "astro:content"; -import config from "@/config/config.json"; - interface Props { data: CollectionEntry<"feature">["data"]; } const { - data: { asset, tagLine, name, blogId, blogFragment, youtubeLink }, + data: { asset, tagLine, name, blogSlug, blogFragment, youtubeLink }, } = Astro.props; const parsedCaption = asset.caption ? marked.parseInline(asset.caption) : undefined; -async function fetchJson<T>(url: string): Promise<T | undefined> { - try { - const response = await fetch(url); - const contentType = response.headers.get("content-type") ?? ""; - - if (!response.ok || !contentType.includes("application/json")) { - console.warn( - `Skipping WordPress response for "${url}": ${response.status} ${response.statusText} (${contentType || "unknown content type"})`, - ); - return undefined; - } - - return (await response.json()) as T; - } catch (error) { - console.warn( - `Skipping WordPress response for "${url}": ${error instanceof Error ? error.message : String(error)}`, - ); - return undefined; - } -} - -let blogPath; - -if (blogId) { - const blogBaseUrl = config.blog.base_url; - const post = await fetchJson<{ slug: string }>( - `${blogBaseUrl}/wp-json/wp/v2/posts/${blogId}?_fields=slug`, - ); - blogPath = post - ? `/blog/${post.slug}${blogFragment ? `/#${blogFragment}` : ""}` - : undefined; -} +const blogPath = blogSlug + ? `/blog/${blogSlug}${blogFragment ? `/#${blogFragment}` : ""}` + : undefined; let imageSizes; diff --git a/docs/src/lib/utils/selectOgImage.ts b/docs/src/lib/utils/selectOgImage.ts deleted file mode 100644 index 35576530..00000000 --- a/docs/src/lib/utils/selectOgImage.ts +++ /dev/null @@ -1,46 +0,0 @@ -export function selectOgImage( - sizes: Record<string, { width: number; height: number; source_url: string }>, -): string | undefined { - // First, check for an exact or closest match to 1200x630 (landscape, ~1.91:1 aspect) - const TARGET_WIDTH = 1200; - const TARGET_HEIGHT = 630; - const MIN_WIDTH = 600; - const MIN_HEIGHT = 315; - const MIN_DIM = 200; - - let best; - let fallback; - - for (const key in sizes) { - const { width, height, source_url } = sizes[key]; - const aspectRatio = width / height; - - // Check for perfect or almost perfect 1.91:1 aspect and minimum size - if ( - width >= TARGET_WIDTH && - height >= TARGET_HEIGHT && - Math.abs(aspectRatio - 1.91) < 0.15 - ) { - return source_url; // Best possible match - } - - // Save the largest landscape above min 600x315 as fallback - if (width >= MIN_WIDTH && height >= MIN_HEIGHT && aspectRatio > 1.5) { - if (!best || width > best.width) { - best = { width, height, source_url }; - } - } - - // Grab possible image above 200x200 as secondary fallback - if (width >= MIN_DIM && height >= MIN_DIM) { - if (!fallback || width > fallback.width) { - fallback = { width, height, source_url }; - } - } - } - - if (best) return best.source_url; - if (fallback) return fallback.source_url; - // As last resort, try "full" size image (if present) - return sizes.full ? sizes.full.source_url : undefined; -} diff --git a/docs/src/pages/blog/15-voiceover-navigator-pro-xcode-simulator-recordings.astro b/docs/src/pages/blog/15-voiceover-navigator-pro-xcode-simulator-recordings.astro deleted file mode 100644 index be00655c..00000000 --- a/docs/src/pages/blog/15-voiceover-navigator-pro-xcode-simulator-recordings.astro +++ /dev/null @@ -1,324 +0,0 @@ ---- -import { Image } from "astro:assets"; - -import Base from "@/layouts/Base.astro"; -import config from "@/config/config.json"; -import Youtube from "@/layouts/shortcodes/Youtube"; - -import voiceOverHero from "../../assets/blog/rocketsim-15/voiceover-accessibility-overlay-white.png"; -import voiceOverNavigation from "../../assets/blog/rocketsim-15/voiceover-navigation.png"; -import rocketsimCLI from "../../assets/blog/rocketsim-15/rocketsim-cli.png"; -import liquidGlassCompare from "../../assets/features/liquid-glass-accessibility-testing-compare.jpg"; -import postEditorVideo from "../../content/docs/docs/features/capturing/post-editor/video-post-editor.png"; -import keychainResetButton from "../../content/docs/docs/features/app-actions/quick-actions/reset-keychain-button.png"; -import networkingTools from "../../content/docs/docs/features/networking/network-traffic-monitoring/networking_tools_details.jpeg"; - -const slug = "15-voiceover-navigator-pro-xcode-simulator-recordings"; -const title = - "RocketSim 15: VoiceOver testing in the Simulator and better Xcode Simulator recordings"; -const description = - "RocketSim 15 adds VoiceOver Navigator, Elements Overlay, Tinted Liquid Glass testing, a new post editor for Xcode Simulator recordings, networking improvements, and the first RocketSim CLI beta."; -const publishedTime = "2026-04-09T09:05:24.000Z"; -const modifiedTime = publishedTime; -const canonicalUrl = `${config.site.base_url}/blog/${slug}`; -const image = `${config.site.base_url}${voiceOverHero.src}`; - -const seo = { - title: `${title} - RocketSim`, - description, - image, - canonical: canonicalUrl, - twitterCreator: "@twannl", - twitterSite: "@rocketsim_app", - article: { - publishedTime, - modifiedTime, - }, -}; - -const structuredData = { - type: "article" as const, - article: { - headline: title, - description, - datePublished: publishedTime, - dateModified: modifiedTime, - url: canonicalUrl, - slug, - image: { - url: image, - }, - }, -}; ---- - -<Base seo={seo} structuredData={structuredData}> - <article> - <header> - <div class="ph-spacing"> - <div class="container text-center"> - <div class="flex flex-col items-center gap-16 lg:gap-28"> - <div class="space-y-3 md:space-y-5 mx-auto"> - <p - class="text-center text-base font-medium text-text pb-2" - data-aos="fade-up-sm" - data-aos-delay="0" - > - Apr 09, 2026 - </p> - <h1 - class="page-heading leading-none text-center !mb-6" - data-aos="fade-up-sm" - data-aos-delay="50" - > - {title} - </h1> - </div> - </div> - </div> - </div> - - <div class="ph-merged-section"> - <div class="container"> - <figure data-aos="fade-up-sm" data-aos-delay="150"> - <Image - src={voiceOverHero} - alt="RocketSim VoiceOver Navigator showing numbered accessibility elements directly on top of the iOS Simulator." - class="w-full max-h-[580px] object-cover rounded-2xl" - /> - </figure> - </div> - </div> - </header> - - <section class="section -mt-20"> - <div class="container"> - <div class="content xl:px-32"> - <p> - <strong>VoiceOver testing in the Simulator</strong> used to be one of - those jobs I would postpone until the very end. You grab a device, enable - VoiceOver, start swiping, lose context, and then go back to the Simulator - to fix the issue you just found. RocketSim 15 changes that loop completely. - </p> - - <p> - On top of that, this release makes <strong - >Xcode Simulator recordings</strong - > - much more useful after capture. Instead of retaking a screenshot or video - because the framing is slightly off, you can now post-edit the result - directly inside RocketSim. Combined with a dedicated networking tools - tab, a keychain reset action, localized App Store pages, and the first - RocketSim CLI beta, this is easily the biggest RocketSim release I have - shipped so far. - </p> - - <h2>VoiceOver testing in the Simulator</h2> - - <p> - The headline feature in RocketSim 15 is <a - href="/docs/features/accessibility/voiceover-navigator" - >VoiceOver Navigator</a - >. It lets you move through accessibility elements using your - keyboard, directly in the Simulator. Arrow keys move through the - current VoiceOver order, Enter activates the selected element, and - the overlay updates as your app changes state. - </p> - - <p> - What I like most is that this is not just another inspector. You can - still click elements and inspect their accessibility metadata, but - the real win is speed. You immediately see whether the reading order - makes sense, whether a heading lands where you expect it to, and - whether activating a control moves focus in a sensible way. - </p> - - <figure> - <Image - src={voiceOverNavigation} - alt="VoiceOver Navigator overlay visualizing accessibility element order in RocketSim." - class="w-full rounded-2xl" - /> - </figure> - - <p> - Accessibility work in iOS often becomes much easier once you can see - the structure instead of only hearing it. That is exactly what the - new Elements Overlay gives you. If you care about shipping more - accessible apps, this feature is worth checking out on its own. - </p> - - <p> - RocketSim 15 also adds direct testing for <a - href="/docs/features/accessibility/toggles-and-dynamic-text#tinted-liquid-glass-testing" - >Tinted Liquid Glass</a - >. Liquid Glass looks great in demos, but the accessibility story - matters just as much. Being able to compare both states in the - Simulator is a much faster way to validate contrast and readability - while you iterate. - </p> - - <figure> - <Image - src={liquidGlassCompare} - alt="Comparison of an app before and after enabling the Tinted Liquid Glass accessibility toggle in RocketSim." - class="w-full rounded-2xl" - /> - </figure> - - <h2>Editing Xcode Simulator recordings after capture</h2> - - <p> - The second big improvement in RocketSim 15 is the new <a - href="/docs/features/capturing/post-editor">post editor</a - > - for screenshots and videos. If you record Xcode Simulator videos for release - notes, social media, demos, or App Store assets, you probably know how - often a capture is technically correct but still not quite ready to share. - </p> - - <p> - The new editor fixes that. You can trim the recording, adjust the - styling, tweak the frame, change the output ratio, update metadata, - and preview everything live before exporting. That means one good - base recording can now become several polished variants without you - needing to capture the whole flow again. - </p> - - <figure> - <Image - src={postEditorVideo} - alt="RocketSim Capture Editor open for a recording, showing live preview, trim controls, and export settings." - class="w-full rounded-2xl" - /> - </figure> - - <p> - I think this is especially useful for teams that rely on Xcode - Simulator recordings as part of their development workflow. You can - keep your capture loop fast, but still end up with an output that - looks intentional. If you want to see it in action first, I also - published a <a href="https://www.youtube.com/watch?v=3LlGj6oVi7A" - >short demo video</a - >. - </p> - - <Youtube - url="https://www.youtube.com/watch?v=3LlGj6oVi7A" - title="RocketSim Post Editor demo" - /> - - <h2>Smaller updates that speed up testing</h2> - - <p> - RocketSim 15 also includes a few smaller changes that matter a lot - in day-to-day work. The new <a - href="/docs/features/app-actions/quick-actions">Reset Keychain</a - > - action is one of those features you start using once and then wonder why - it did not exist earlier. Authentication testing often goes wrong because - of stale credentials, and resetting the Simulator keychain is much faster - than manually clearing app state over and over again. - </p> - - <figure> - <Image - src={keychainResetButton} - alt="RocketSim Recent Builds side window showing the Reset Keychain quick action." - class="w-full rounded-2xl" - /> - </figure> - - <p> - Networking tools also moved into a dedicated tab. That sounds small, - but it makes both discovery and repeated usage better. Features like - network speed control and the network monitor now feel like part of - one workflow instead of scattered utilities. - </p> - - <figure> - <Image - src={networkingTools} - alt="RocketSim networking tools overview with request details and network inspection controls." - class="w-full rounded-2xl" - /> - </figure> - - <p> - If you regularly test under poor connectivity, inspect requests, or - debug networking issues in the Simulator, this new structure should - save you a few clicks every day. Sometimes that is exactly the kind - of improvement that makes a tool feel much more polished. - </p> - - <h2>The first RocketSim CLI beta</h2> - - <p> - I also bundled the first version of the RocketSim CLI in this - release. It only supports a few commands today, but the direction is - clear: I want to make it easier for your agents and automation - workflows to interact with the Simulator in a secure, sandboxed way. - </p> - - <figure> - <Image - src={rocketsimCLI} - alt="RocketSim CLI help output showing simulator, elements, and interact subcommands." - class="w-full rounded-2xl" - /> - </figure> - - <p> - I am particularly excited about this because I used an agentic - coding setup heavily while building RocketSim 15 itself. The - foundation of RocketSim has always been solid, but the current - tooling finally lets me move at a speed that was simply not - realistic before. If your team is already deep into agentic coding, - feel free to reach out if you want to join the beta. - </p> - - <h2>Localized pages and student access</h2> - - <p> - Two more updates are worth calling out. First, RocketSim now has - localized App Store pages in Chinese, French, German, and Spanish. - After visiting Let's Vision in Shanghai, it became clear to me - that RocketSim needed to do a better job outside English-only - messaging. - </p> - - <p> - Second, students can now get RocketSim Pro for free for one year - through the <a href="/student">student program</a>. Professional - tooling made a big difference early in my own career, so this felt - like the right moment to give something back. - </p> - - <h2><strong>Conclusion</strong></h2> - - <p> - RocketSim 15 is a release focused on faster feedback loops. - VoiceOver testing in the Simulator becomes practical enough to use - throughout development, and Xcode Simulator recordings become much - easier to polish after capture. The rest of the release continues - that same pattern: fewer interruptions, less repetitive setup, and - quicker access to the tools you actually use. - </p> - - <p> - If you already use RocketSim, I recommend installing the latest - update from the <a - href="https://apps.apple.com/app/apple-store/id1504940162?pt=117264678&ct=april-newsletter&mt=8" - >Mac App Store</a - > - and trying the accessibility and capture workflows first. If you have - feature ideas or feedback, feel free to <a - href="https://github.com/AvdLee/RocketSimApp/issues" - >open an issue on GitHub</a - >. Thanks! - </p> - </div> - </div> - </section> - </article> -</Base> diff --git a/docs/src/pages/blog/[slug].astro b/docs/src/pages/blog/[slug].astro index 2b7ef3a1..e1912242 100644 --- a/docs/src/pages/blog/[slug].astro +++ b/docs/src/pages/blog/[slug].astro @@ -1,204 +1,70 @@ --- -import { JSDOM } from "jsdom"; -import GithubSlugger from "github-slugger"; -import { createElement } from "react"; -import { renderToStaticMarkup } from "react-dom/server"; -import { FaLink } from "react-icons/fa6"; import { Image } from "astro:assets"; +import { getCollection, render } from "astro:content"; import Base from "@/layouts/Base.astro"; import CallToAction from "@/partials/CallToAction.astro"; import config from "@/config/config.json"; -import { selectOgImage } from "@/lib/utils/selectOgImage"; import { dateFormat } from "@/lib/utils/dateFormat"; -const { slug } = Astro.params; - -async function fetchJson<T>(url: string): Promise<T | undefined> { - try { - const response = await fetch(url); - const contentType = response.headers.get("content-type") ?? ""; - - if (!response.ok || !contentType.includes("application/json")) { - console.warn( - `Skipping WordPress response for "${url}": ${response.status} ${response.statusText} (${contentType || "unknown content type"})`, - ); - return undefined; - } - - return (await response.json()) as T; - } catch (error) { - console.warn( - `Skipping WordPress response for "${url}": ${error instanceof Error ? error.message : String(error)}`, - ); - return undefined; - } -} - export async function getStaticPaths() { - const blogBaseUrl = config.blog.base_url; - const postsUrl = `${blogBaseUrl}/wp-json/wp/v2/posts`; - let posts: Array<{ slug: string }> | undefined; - - try { - const response = await fetch(postsUrl); - const contentType = response.headers.get("content-type") ?? ""; - - if (!response.ok || !contentType.includes("application/json")) { - console.warn( - `Skipping WordPress response for "${postsUrl}": ${response.status} ${response.statusText} (${contentType || "unknown content type"})`, - ); - } else { - posts = (await response.json()) as Array<{ slug: string }>; - } - } catch (error) { - console.warn( - `Skipping WordPress response for "${postsUrl}": ${error instanceof Error ? error.message : String(error)}`, - ); - } - - if (!posts) { - return []; - } - + const posts = await getCollection("blog"); return posts.map((post) => ({ - params: { slug: post.slug }, - props: { post: post }, + params: { slug: post.id }, + props: { post }, })); } -let post; - -const blogBaseUrl = config.blog.base_url; - -try { - const posts = await fetchJson<Array<any>>( - `${blogBaseUrl}/wp-json/wp/v2/posts?slug=${slug}&_embed`, - ); - [post] = posts ?? []; -} catch (e) { - throw new Error( - `Failed to fetch blog post for slug "${slug}" from "${blogBaseUrl}/wp-json/wp/v2/posts?slug=${slug}&_embed": ${e instanceof Error ? e.message : String(e)}`, - ); -} - -if (!post) { - throw new Error( - `Failed to fetch blog post for slug "${slug}" from "${blogBaseUrl}/wp-json/wp/v2/posts?slug=${slug}&_embed"`, - ); -} - -const title = post.title.rendered; -const articlePublishedTime = post.date; -const articleModifiedTime = post.modified; - -// Extract featured image data -const featuredMedia = post._embedded?.["wp:featuredmedia"]?.[0]; -const image = featuredMedia - ? selectOgImage(featuredMedia.media_details.sizes) - : undefined; - -// Extract Yoast SEO data -const yoast = post.yoast_head_json || {}; -const description = - yoast.og_description || yoast.description || post.excerpt?.rendered || ""; - -// Extract Twitter metadata -const twitterCreator = yoast.twitter_misc?.["Written by"] - ? "@twannl" - : "@twannl"; -const twitterSite = yoast.twitter_site || "@rocketsim_app"; - -// Extract word count from schema data -let wordCount: number | undefined; -if (yoast.schema?.["@graph"]) { - const articleSchema = yoast.schema["@graph"].find( - (item: any) => item["@type"] === "Article", - ); - if (articleSchema?.wordCount) { - wordCount = articleSchema.wordCount; - } -} +const { post } = Astro.props; +const { Content } = await render(post); const { site: { base_url }, } = config; -// Build canonical URL (transform from cms.rocketsim.app to www.rocketsim.app) -const canonicalUrl = `${base_url}/blog/${post.slug}`; +const title = post.data.title; +const pageTitle = post.data.pageTitle ?? title; +const description = post.data.description; +const publishedTime = post.data.publishedTime; +const modifiedTime = post.data.modifiedTime ?? publishedTime; +const image = post.data.image; +const imageAlt = post.data.imageAlt ?? pageTitle; + +const canonicalUrl = `${base_url}/blog/${post.id}`; +const ogImage = image ? `${base_url}${image.src}` : undefined; -// Build SEO object for SEO component const seo = { title: `${title} - RocketSim`, description, - image, + image: ogImage, + imageAlt: image ? imageAlt : undefined, canonical: canonicalUrl, - twitterCreator, - twitterSite, + twitterCreator: "@twannl", + twitterSite: "@rocketsim_app", article: { - publishedTime: articlePublishedTime, - modifiedTime: articleModifiedTime, - wordCount, + publishedTime: publishedTime.toISOString(), + modifiedTime: modifiedTime.toISOString(), }, }; -// Build structured data object for StructuredData component const structuredData = { type: "article" as const, article: { - headline: title, + headline: pageTitle, description, - datePublished: articlePublishedTime, - dateModified: articleModifiedTime, - wordCount, - image: image + datePublished: publishedTime.toISOString(), + dateModified: modifiedTime.toISOString(), + url: canonicalUrl, + slug: post.id, + image: ogImage ? { - url: image, - width: featuredMedia?.media_details?.width, - height: featuredMedia?.media_details?.height, - caption: featuredMedia?.caption?.rendered, + url: ogImage, + width: image?.width, + height: image?.height, } : undefined, - url: canonicalUrl, - slug: post.slug, }, }; - -/** - * Manipulate the post content in any way you want here. - */ -const dom = new JSDOM(post.content.rendered); -const doc = dom.window.document; - -const videos = doc.querySelectorAll("video"); - -videos.forEach((video) => { - video.setAttribute("playsinline", "true"); - video.setAttribute("controls", "true"); -}); - -const slugger = new GithubSlugger(); -const headings = doc.querySelectorAll("h1, h2, h3, h4, h5, h6"); -const linkIconMarkup = renderToStaticMarkup( - createElement(FaLink, { "aria-hidden": true }), -); - -headings.forEach((heading) => { - const text = heading.textContent?.trim() ?? ""; - if (!text) return; - - const id = heading.id || slugger.slug(text); - heading.id = id; - - const anchor = doc.createElement("a"); - anchor.setAttribute("href", `#${id}`); - anchor.setAttribute("class", "heading-anchor"); - anchor.setAttribute("aria-label", `Link to section: ${text}`); - anchor.innerHTML = linkIconMarkup; - heading.insertBefore(anchor, heading.firstChild); -}); - -const manipulatedDom = dom.serialize(); --- <Base seo={seo} structuredData={structuredData}> @@ -208,29 +74,20 @@ const manipulatedDom = dom.serialize(); <div class="container"> <div class="flex flex-col gap-16 lg:gap-28"> <div class="space-y-3 md:space-y-5 mx-auto w-full max-w-[680px]"> - { - articleModifiedTime && ( - <p - class="text-base font-medium text-text pb-2" - data-aos="fade-up-sm" - data-aos-delay="0" - > - Last Updated - <span - set:html={dateFormat( - articleModifiedTime, - " MMM dd, yyyy", - )} - /> - </p> - ) - } + <p + class="text-base font-medium text-text pb-2" + data-aos="fade-up-sm" + data-aos-delay="0" + > + Last Updated {dateFormat(modifiedTime, "MMM dd, yyyy")} + </p> <h1 class="page-heading leading-none !mb-6" data-aos="fade-up-sm" data-aos-delay="50" - set:html={title} - /> + > + {pageTitle} + </h1> </div> </div> </div> @@ -243,7 +100,7 @@ const manipulatedDom = dom.serialize(); <figure data-aos="fade-up-sm" data-aos-delay="150"> <Image src={image} - alt={title} + alt={imageAlt} width={1300} height={768} class="w-full max-h-[580px] object-cover rounded-2xl" @@ -258,7 +115,7 @@ const manipulatedDom = dom.serialize(); <section class="section -mt-20"> <div class="container"> <div class="content mx-auto max-w-[680px]"> - <Fragment set:html={manipulatedDom} /> + <Content /> </div> </div> </section> @@ -311,18 +168,4 @@ const manipulatedDom = dom.serialize(); .content :global(.heading-anchor:focus-visible) { opacity: 1; } - - /* Make WordPress YouTube / video oEmbeds fill the content column */ - .content :global(.wp-embed-aspect-16-9 .wp-block-embed__wrapper) { - position: relative; - width: 100%; - aspect-ratio: 16 / 9; - } - - .content :global(.wp-embed-aspect-16-9 .wp-block-embed__wrapper iframe) { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - } </style> diff --git a/docs/src/pages/blog/how-to-test-voiceover-on-the-xcode-simulator.astro b/docs/src/pages/blog/how-to-test-voiceover-on-the-xcode-simulator.astro deleted file mode 100644 index 81d9e159..00000000 --- a/docs/src/pages/blog/how-to-test-voiceover-on-the-xcode-simulator.astro +++ /dev/null @@ -1,365 +0,0 @@ ---- -import { Image } from "astro:assets"; - -import Base from "@/layouts/Base.astro"; -import config from "@/config/config.json"; - -import voiceOverHero from "../../assets/blog/rocketsim-15/voiceover-accessibility-overlay-white.png"; -import voiceOverOverlay from "../../content/docs/docs/features/accessibility/voiceover-navigator/voiceover-overlay.png"; -import voiceOverNavigation from "../../content/docs/docs/features/accessibility/voiceover-navigator/voiceover-navigation.png"; -import liquidGlassCompare from "../../assets/features/liquid-glass-accessibility-testing-compare.jpg"; - -const slug = "how-to-test-voiceover-on-the-xcode-simulator"; -const title = "How to Test VoiceOver on the Xcode Simulator"; -const pageTitle = `${title} (Without a Physical Device)`; -const description = - "Test VoiceOver on the Xcode Simulator without a physical device. Use a numbered overlay, keyboard shortcuts, and the rotor to verify iOS accessibility fast."; -const publishedTime = "2026-05-28T09:00:00.000Z"; -const modifiedTime = publishedTime; -const canonicalUrl = `${config.site.base_url}/blog/${slug}`; -const image = `${config.site.base_url}${voiceOverHero.src}`; - -const seo = { - title: `${title} - RocketSim`, - description, - image, - canonical: canonicalUrl, - twitterCreator: "@twannl", - twitterSite: "@rocketsim_app", - article: { - publishedTime, - modifiedTime, - }, -}; - -const structuredData = { - type: "article" as const, - article: { - headline: pageTitle, - description, - datePublished: publishedTime, - dateModified: modifiedTime, - url: canonicalUrl, - slug, - image: { - url: image, - }, - }, -}; ---- - -<Base seo={seo} structuredData={structuredData}> - <article> - <header> - <div class="ph-spacing"> - <div class="container text-center"> - <div class="flex flex-col items-center gap-16 lg:gap-28"> - <div class="space-y-3 md:space-y-5 mx-auto"> - <p - class="text-center text-base font-medium text-text pb-2" - data-aos="fade-up-sm" - data-aos-delay="0" - > - May 28, 2026 - </p> - <h1 - class="page-heading leading-none text-center !mb-6" - data-aos="fade-up-sm" - data-aos-delay="50" - > - {pageTitle} - </h1> - </div> - </div> - </div> - </div> - - <div class="ph-merged-section"> - <div class="container"> - <figure data-aos="fade-up-sm" data-aos-delay="150"> - <Image - src={voiceOverHero} - alt="VoiceOver testing on the Xcode Simulator with RocketSim, showing numbered accessibility elements overlaid on the iPhone Simulator and the Voice Over Elements list." - class="w-full max-h-[580px] object-cover rounded-2xl" - /> - </figure> - </div> - </div> - </header> - - <section class="section -mt-20"> - <div class="container"> - <div class="content xl:px-32"> - <p> - <strong>Testing VoiceOver on the Xcode Simulator</strong> - is one of those tasks every iOS developer agrees is important, and most - developers postpone until the last minute. The Xcode Simulator does not - ship with a real VoiceOver runtime, so the usual answer is "grab a physical - iPhone, enable VoiceOver in Settings, and start swiping." That works, - but it pulls you out of your normal development flow and slows down every - small fix. - </p> - - <p> - This article walks through the practical options you have today. You - will see what Xcode itself offers, why the physical-device workflow - is still painful, and how RocketSim turns VoiceOver testing into a - keyboard-driven step you can run inside the Simulator while you - develop. The goal is simple: make accessibility checks fast enough - that you actually run them. - </p> - - <h2>Why VoiceOver testing belongs in your daily iOS workflow</h2> - - <p> - VoiceOver is the most visible accessibility feature on iOS, and it - is the one most likely to break silently when you change a layout. - Adding a wrapping <code>VStack</code> or renaming an icon button can flip - the reading order in a way you never see visually, but a VoiceOver user - will hit immediately. The earlier you catch it, the cheaper it is to fix. - </p> - - <p> - In my experience, the main reason accessibility regressions ship is - not a lack of care — it is friction. If verifying a single screen - takes ten minutes of device juggling, you will not do it on every - pull request. If it takes ten seconds in the Simulator, you will. - That is the loop this article tries to give you. - </p> - - <h2> - What you can (and cannot) do with VoiceOver in the Xcode Simulator - </h2> - - <p> - The honest answer is that <strong - >the Xcode Simulator does not run real VoiceOver</strong - >. There is no Settings toggle that turns it on the way you would on - an iPhone, and the triple-click side-button shortcut does not exist - either. The Simulator is a great development tool, but it is not - designed to be a screen reader sandbox. - </p> - - <p> - What you do have is <a - href="https://developer.apple.com/documentation/accessibility/accessibility-inspector" - >Xcode's Accessibility Inspector</a - >. It can target a running Simulator, list accessibility elements, - highlight them on hover, and run audits for missing labels or low - contrast. It is genuinely useful for debugging a specific element, - but it is not a substitute for stepping through the app the way - VoiceOver actually does. You see metadata, not behavior. - </p> - - <p> - That gap — inspecting elements versus moving through them — is - exactly the reason most developers fall back to a physical device - once they want to verify a full screen or flow. - </p> - - <h2>The physical-device workflow (and why it slows you down)</h2> - - <p> - On a physical iPhone, you enable VoiceOver from <strong - >Settings → Accessibility → VoiceOver</strong - > and bind the Accessibility Shortcut to a triple-click of the side button. - From there you swipe right and left to move through elements, double-tap - to activate, and rotate two fingers to switch rotor categories. It works, - and it is the most accurate way to experience your app the way a VoiceOver - user would. - </p> - - <p> - The problem is the development loop around it. You build and deploy - to the device, switch focus from Xcode to your phone, enable - VoiceOver, find the bug, disable VoiceOver so you can use the - keyboard again, walk back to Xcode, change one line, and repeat. - Every iteration breaks your context. - </p> - - <p> - You will still want to validate critical flows on a real device - before shipping. But for the dozens of small fixes you make while - building — reordering elements, adding labels, grouping a card, - tagging a heading — the device-based loop is too slow to use as your - default. - </p> - - <h2>Testing VoiceOver on the Xcode Simulator with RocketSim</h2> - - <p> - <a href="/docs/features/accessibility/voiceover-navigator" - >RocketSim's VoiceOver Navigator</a - > closes that gap. It runs alongside the Xcode Simulator and shows a numbered - overlay on every accessibility element in the exact order VoiceOver would - announce them. You see the reading order without listening to the reading - order — which is usually what you actually need while you are coding. - </p> - - <figure> - <Image - src={voiceOverOverlay} - alt="RocketSim VoiceOver Elements Overlay on the iOS Simulator showing numbered accessibility elements, the rotor dropdown, and the Voice Over Elements list with roles like StaticText and Button." - class="w-full rounded-2xl" - /> - <figcaption> - The Elements Overlay shows every accessibility element on top of - the Xcode Simulator, in VoiceOver order. - </figcaption> - </figure> - - <p> - From the side window you turn on <strong>Elements Overlay</strong> - and pick a rotor category from the dropdown — for example <strong - >All Elements</strong - >, <strong>Headings</strong>, or <strong>Form Controls</strong>. - Each element gets a number and a row in the list with its role, so - you can immediately see whether a heading lands where you expect it - or whether a static text element accidentally became a button. The - element count at the bottom is a useful sanity check on its own. - </p> - - <p> - Because the overlay is rendered live, it updates as your app - updates. Push a new view, hit <strong>Refresh</strong>, and the - numbering re-runs. There is no device build, no toggling, no - triple-click — you stay on the Mac, in your editor. - </p> - - <h2> - Navigating elements with the keyboard, like a real VoiceOver swipe - </h2> - - <p> - Inspecting elements is useful, but VoiceOver is fundamentally about <strong - >movement</strong - >. To replicate that loop on the Simulator, click <strong - >Start Navigating</strong - > in the Voice Over panel. The overlay stays on screen and the focus highlight - jumps between elements as you press the arrow keys. - </p> - - <figure> - <Image - src={voiceOverNavigation} - alt="RocketSim VoiceOver Navigator in navigation mode on the Xcode Simulator, with the keyboard shortcuts panel showing arrow keys, Enter, and Esc, and the currently focused Watchlist button highlighted." - class="w-full rounded-2xl" - /> - <figcaption> - Navigation mode highlights the focused element and lists the - keyboard shortcuts for moving through the rotor. - </figcaption> - </figure> - - <p>The shortcuts map directly onto VoiceOver gestures:</p> - - <ul> - <li> - <strong>↑ / ↓</strong> — Move to the previous or next element in VoiceOver - order, exactly like swiping left or right on device. - </li> - <li> - <strong>← / →</strong> — Switch rotor category, the way a two-finger - rotate would on a real iPhone. - </li> - <li> - <strong>⏎ Enter</strong> — Activate the focused element, similar to - a double-tap; the app navigates and the overlay updates with the new - screen. - </li> - <li> - <strong>Esc</strong> — Exit navigation mode and return to the overlay-only - view. - </li> - </ul> - - <p> - This is the part that makes accessibility testing feel like part of - the build cycle rather than a separate phase. You step through a - screen, spot a missing label or a wrong rotor group, fix it in - Xcode, hit <strong>Refresh</strong>, and verify in seconds. No - cable, no headphones, no losing your place. - </p> - - <h2> - Combine VoiceOver with the rest of your iOS accessibility checks - </h2> - - <p> - VoiceOver is the headline feature, but accessibility on iOS is a - stack: Dynamic Type, Bold Text, Increase Contrast, Reduce Motion, - Reduce Transparency, and now Tinted Liquid Glass. A screen that - reads well in VoiceOver can still fail badly at the largest Dynamic - Type size or under Increase Contrast. - </p> - - <p> - From the same side window, RocketSim's <a - href="/docs/features/accessibility/environment-overrides" - >Environment Overrides</a - > - give you direct toggles for those settings, plus a slider for every <a - href="/docs/features/accessibility/toggles-and-dynamic-text" - >Dynamic Type</a - > - size. So once you finish a VoiceOver pass, you can flip on Bold Text, - crank up Dynamic Type, and check the same screen again — without going - back into Simulator Settings. - </p> - - <figure> - <Image - src={liquidGlassCompare} - alt="Side-by-side comparison of an iOS app on the Xcode Simulator before and after enabling Tinted Liquid Glass through RocketSim's accessibility toggles." - class="w-full rounded-2xl" - /> - <figcaption> - Tinted Liquid Glass is one of several accessibility toggles you - can flip from the same panel as VoiceOver Navigator. - </figcaption> - </figure> - - <p> - If you treat VoiceOver as one stop in a short accessibility - checklist — VoiceOver order, headings, Dynamic Type, contrast — you - will catch most of the regressions that normally slip through until - App Review or a user report. The full set of <a - href="/features/accessibility">RocketSim accessibility features</a - > is built around exactly this kind of fast iteration loop on the Xcode - Simulator. - </p> - - <h2><strong>Conclusion</strong></h2> - - <p> - For a long time, "test VoiceOver on the Xcode Simulator" really - meant "give up and use a physical device." That is no longer true. - With a numbered Elements Overlay, a keyboard-driven navigator, and a - rotor that matches the device experience, you can verify the reading - order and activation flow of an iOS app without leaving the - Simulator. A physical device is still the right place for a final - pass, but it should not be your default anymore. - </p> - - <p> - If you want to try this on your own app, install <a - href="https://apps.apple.com/app/apple-store/id1504940162?pt=117264678&ct=voiceover-article&mt=8" - >RocketSim from the Mac App Store</a - > - and open the <strong>Voice Over</strong> tab in the side window. You can - also dig into the full <a - href="/docs/features/accessibility/voiceover-navigator" - >VoiceOver Navigator documentation</a - > - for the keyboard shortcuts and rotor details. Feel free to reach out on - <a href="https://x.com/twannl">X/Twitter</a> or - <a href="https://github.com/AvdLee/RocketSimApp/issues" - >open an issue on GitHub</a - > - if you run into anything or have feature ideas. Thanks! - </p> - </div> - </div> - </section> - </article> -</Base> diff --git a/docs/src/pages/blog/index.astro b/docs/src/pages/blog/index.astro new file mode 100644 index 00000000..ad741ae1 --- /dev/null +++ b/docs/src/pages/blog/index.astro @@ -0,0 +1,175 @@ +--- +import { getCollection } from "astro:content"; + +import Base from "@/layouts/Base.astro"; +import CallToAction from "@/partials/CallToAction.astro"; +import config from "@/config/config.json"; +import { dateFormat } from "@/lib/utils/dateFormat"; + +const { + site: { base_url }, +} = config; + +const posts = await getCollection("blog"); + +const items = posts + .map((post) => ({ + slug: post.id, + title: post.data.title, + description: post.data.description, + date: post.data.modifiedTime ?? post.data.publishedTime, + })) + .sort((a, b) => b.date.getTime() - a.date.getTime()); + +const [featured, ...rest] = items; + +const seo = { + title: "Blog - RocketSim", + description: + "Tips, guides, and deep dives for iOS developers who want to move faster.", + canonical: `${base_url}/blog/`, +}; + +const structuredData = { + type: "static" as const, +}; +--- + +<Base seo={seo} structuredData={structuredData}> + <section class="ph-spacing"> + <div class="container"> + <div class="flex flex-col items-center text-center max-w-[680px] mx-auto"> + <span + class="inline-flex items-center gap-1.5 rounded-full border border-primary/30 bg-primary/10 px-3.5 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary" + data-aos="fade-up-sm" + data-aos-delay="0" + > + Developer Resources + </span> + <h1 + class="page-heading mt-5 !mb-4 hasEmphasize text-balance" + data-aos="fade-up-sm" + data-aos-delay="50" + > + The RocketSim Blog + </h1> + <p + class="text-lg text-text text-balance leading-relaxed" + data-aos="fade-up-sm" + data-aos-delay="100" + > + Tips, guides, and deep dives for iOS developers who want to move + faster. + </p> + </div> + </div> + </section> + + <section class="section pt-12"> + <div class="container"> + <div class="mx-auto flex max-w-[1080px] flex-col gap-9"> + { + featured && ( + <a + href={`/blog/${featured.slug}/`} + class="featured-card group relative grid grid-cols-1 items-center gap-8 overflow-hidden rounded-2xl border border-white/8 bg-white/[0.03] p-8 transition-all duration-200 hover:border-white/14 hover:bg-white/[0.05] md:grid-cols-[1fr_auto] md:gap-12 md:p-11" + data-aos="fade-up-sm" + data-aos-delay="0" + > + <span class="featured-accent absolute inset-y-0 left-0 w-[3px] bg-primary opacity-50 transition-opacity duration-200 group-hover:opacity-100" /> + + <div class="flex flex-col gap-3.5"> + <div class="flex items-center gap-2.5"> + <span class="inline-flex items-center rounded-full border border-primary/25 bg-primary/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.06em] text-primary"> + Latest + </span> + <span class="text-[11px] font-semibold uppercase tracking-[0.06em] text-text-dark/30"> + Featured + </span> + </div> + + <h2 class="text-2xl font-bold leading-snug tracking-tight text-text-light text-balance md:text-[28px]"> + {featured.title} + </h2> + + {featured.description && ( + <p class="max-w-[580px] text-[15.5px] leading-[1.7] text-text-dark/50 text-balance"> + {featured.description} + </p> + )} + + <div class="mt-1 flex items-center gap-4 text-sm text-text-dark/30"> + <span> + Last updated {dateFormat(featured.date, "MMM dd, yyyy")} + </span> + </div> + </div> + + <div class="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full border border-white/8 bg-white/5 transition-all duration-200 group-hover:border-primary/40 group-hover:bg-primary/20"> + <svg + width="16" + height="16" + viewBox="0 0 16 16" + fill="none" + class="text-text-dark/40 transition-colors duration-200 group-hover:text-primary" + > + <path + d="M3 8h10M9 4l4 4-4 4" + stroke="currentColor" + stroke-width="1.5" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </div> + </a> + ) + } + + { + rest.length > 0 && ( + <div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3"> + {rest.map((post, index) => ( + <a + href={`/blog/${post.slug}/`} + class="post-card group relative flex flex-col gap-3 overflow-hidden rounded-[14px] border border-white/7 bg-white/[0.025] px-7 pb-6 pt-6 transition-all duration-200 hover:border-white/12 hover:bg-white/[0.05]" + data-aos="fade-up-sm" + data-aos-delay={50 + index * 25} + > + <span class="post-accent absolute inset-x-0 top-0 h-[2px] bg-primary opacity-0 transition-opacity duration-200 group-hover:opacity-70" /> + + <span class="text-xs text-text-dark/25"> + {dateFormat(post.date, "MMM dd, yyyy")} + </span> + + <h3 class="text-[17px] font-semibold leading-snug tracking-tight text-text-light/90 text-balance transition-colors duration-150 group-hover:text-text-light"> + {post.title} + </h3> + + {post.description && ( + <p class="line-clamp-2 text-[13.5px] leading-[1.65] text-text-dark/40 text-balance"> + {post.description} + </p> + )} + </a> + ))} + </div> + ) + } + </div> + </div> + </section> + + <CallToAction /> +</Base> + +<style> + .featured-card, + .post-card { + text-decoration: none; + } + + .post-card { + min-height: 180px; + } +</style> diff --git a/docs/src/styles/components.css b/docs/src/styles/components.css index 56a3cf0b..c7bf5b20 100755 --- a/docs/src/styles/components.css +++ b/docs/src/styles/components.css @@ -204,6 +204,45 @@ @apply prose-figcaption:text-center; } +/* MDX wraps multi-line <figcaption> text in a <p>, which otherwise picks up the + large prose paragraph size. Keep caption paragraphs at the figcaption size. */ +.content :where(figcaption) p { + margin: 0; + + font-size: inherit; + line-height: inherit; + color: inherit; +} + +/* Expressive Code blocks (blog). Mirrors the docs treatment in + starlight-custom.css, but uses brand tokens since the Starlight + --sl-color-* variables aren't available outside the docs layout. + Keeps the syntax theme but swaps its background for the brand surface. */ +.expressive-code { + --ec-codeBg: var(--color-light); + --ec-codeFg: var(--color-text-dark); + --ec-uiFg: var(--color-text); + --ec-uiBorderClr: var(--color-border); + --ec-frm-shdClr: rgba(0, 0, 0, 0.3); + + /* The visible code/terminal background is painted from these frame vars, + which Starlight points at --sl-color-* (undefined outside docs). Pin them + to the brand surface so framed blocks don't fall back to transparent. */ + --ec-frm-edBg: var(--color-light); + --ec-frm-trmBg: var(--color-light); + --ec-frm-edTabBarBg: var(--color-light); + --ec-frm-trmTtbBg: var(--color-light); + + /* Rounded corners (Starlight defaults to square) to match content imagery. */ + --ec-brdRad: 0.75rem; + + /* Copy button icon/border also point at an undefined --sl-color-* here, so + the icon renders transparent. Pin them to a visible brand color. */ + --ec-frm-inlBtnFg: var(--color-text-dark); + --ec-frm-inlBtnBg: var(--color-text-dark); + --ec-frm-inlBtnBrd: var(--color-text-dark); +} + /* Inline code only — leave <pre><code> blocks to their own theme */ .content :not(pre) > code { @apply rounded-md border border-border/60 bg-light px-1.5 py-0.5 text-[0.9em] font-normal text-text-dark;