letmesplain

Hand-authoring guides (no Studio required)

Splain guides are JSON. The Studio makes refining them visual, but nothing about authoring requires it — this page is the whole manual for doing it by hand.

The most important rule: describe the real workflow, not the UI's affordances

The app's buttons show what's possible. A guide has to describe what's actually done — and those aren't always the same thing. This is the one mistake splain:check can't catch for you, because it's about your business, not your code.

The classic trap: a resource has a Create button, so it's natural to write "click New Employee to add an employee." The button works — but if employees really arrive through a hiring and onboarding pipeline, that guide is confidently teaching a workflow that doesn't exist. It plays perfectly and misleads every user.

splain:check proves a guide is well-formed and that its anchors resolve; it cannot know whether the steps match how your team actually works — only a person who knows the process can. So before you publish, ask of every step: is this how the task is really done here, or just a path the interface allows? That question is exactly what the publish sign-off makes you attest to — hold yourself to it.

The trap is sharpest for AI-drafted guides: a model reads the Create button with zero business context and writes the fiction with total confidence. That's why generated guides land as drafts you must review and sign off — the human gate is the only thing that catches a plausible-but-wrong workflow.

The minimal guide

A playable guide needs a slug, a title, one span (which page it covers), and steps. A playable step needs only what you see here:

{
  "slug": "my-first-guide",
  "title": "How to review a widget",
  "genre": "walkthrough",
  "spans": [
    { "resource": "The widgets page", "route": "/admin/widgets" }
  ],
  "steps": [
    {
      "order": 1,
      "kind": "instruction",
      "key": "find-the-table",
      "title": "This is your widget list",
      "instruction": "Every widget you can review appears here.",
      "span_ref": "The widgets page",
      "anchor": { "selector": ".fi-ta", "component_name": "the widgets table" }
    }
  ]
}

That's it. order is display position; key is the stable name other steps point at; span_ref names which span (page) the step plays on; anchor.selector is what gets spotlighted, and component_name is what humans call it. Everything else in the schema — decision options, popover sides, action steps, privacy masks, review flags — is optional enrichment; add it when you need it, never before.

Anchors: how to pick a selector without pain

Best: add a marker to your own Blade/Filament code and point at it —

->extraAttributes(['data-splain' => 'widget-approve'])   // on the component
"anchor": { "selector": "[data-splain=\"widget-approve\"]", "component_name": "the Approve button" }

Naming the marker: kebab-case, {thing} or {thing}-{role}widget-approve, report-row-actions, billing-section. Name the thing a human would name.

Second-best: a stable structural class (.fi-ta is the page's table). Avoid ids that look generated, and avoid anything positional if you can.

Check yourself

php artisan splain:check path/to/my-first-guide.json     # structure + coverage
php artisan splain:check --strict                        # the publish bar

The checker simulates playback on every page your spans cover and tells you in plain language what would break: unreachable steps, a segment that would end the guide early, a selector that can't work. Zero errors = it plays. Seed it into the splain_guides table (or use your app's seeder) and the helper dot appears on the covered pages.

When guides span pages

End a page's portion with a "kind": "navigation" step whose destination is the next page's path and whose anchor is the link users actually click — Splain hands the walkthrough off and resumes it over there. The checker will tell you if you forgot the bridge.

Grounding: splain:introspect

Writing a span route or an anchor by hand is where mistakes creep in (a wrong route is why a guide once played only part-way). splain:introspect reads your Filament app and prints, per resource, the exact span routes and the stable data-splain anchors already wired into it — deterministically, reading only class metadata, the route table, and source text (never a database row).

php artisan splain:introspect submissions      # one resource
php artisan splain:introspect --json            # machine-readable, all resources

Copy the routes into your guide's spans, and anchor steps to the listed selectors. If a resource has no markers yet, that's your cue to add ->extraAttributes(['data-splain' => '…']) (see the naming grammar above) or use design mode's picker.

Finding what to write next: splain:suggest

splain:introspect tells you about one resource; splain:suggest tells you which resources have no guide at all — the deterministic answer to "what should we cover next," read from the code with no LLM and no user surveys.

php artisan splain:suggest                 # every uncovered page, ranked
php artisan splain:suggest --panel=hr --limit=10
php artisan splain:suggest --json          # machine-readable, for a script

"Covered" means a guide actually plays on the page (the same segmentation splain:check and playback use), not merely that a span mentions the route. Gaps are ranked by a transparent heuristic — a confirmation/destructive action, then a form, then how much anchored surface exists — but the only hard fact is the gap itself; the ordering is a hint, and every signal is shown so you judge.


Rendered from splain@b6ea439. The documentation is rendered from the package repository — the same files that ship with Splain — so the site can't drift from the code.