Claude Skill

audit-shells

Audit pracht shells: missing `Loading()` on SPA routes, `<html>`/`<head>`/ `<body>` rendered inside a shell, shells that never render `children`, unused shells, and `ErrorBoundary` exports shadowed by every route under them. Use for "audit shells", "check shell composition", "fin

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Download jovidecroock-pracht-skills_audit-shells-8bcdd52.zip · 3 KB

Install

skills CLI npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/audit-shells
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart
Git git clone https://github.com/JoviDeCroock/pracht.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jovidecroock/pracht collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Pracht Audit Shells

Shells in pracht are named layout components composed around routes. The framework owns the document — shells must not render <html>, <head>, or <body>. They sit between the framework's HTML scaffold and the route component.

Step 1: Enumerate

MCP: when the pracht MCP server is registered (pracht dev-mcp), prefer its inspect_routes/inspect_api/inspect_build/doctor/verify tools over shelling out.

pracht inspect routes --json

Prerequisite: pracht inspect needs a vite config with the pracht plugin wired up; run it from the app root.

For every shell used by the app, read its source file. Also list which routes use which shell (the JSON output includes resolved shell and shellFile per route). The JSON's top-level mode field tells you which router the app uses — see Step 3 for how that changes shell discovery.

Step 2: Per-shell checks

For each shell file:

2a. Document-level tag misuse

Grep for <html, <head>, <body>, <meta, <title>, <link rel. Any of these inside a shell is a bug — the framework injects them based on head() exports. Recommend moving meta/title to the shell's head() export and removing the JSX tags.

2b. Shell export shape

  • Must be a function component named Shell.
  • Must accept { children }: ShellProps.
  • Must render {children} somewhere — flag shells that never render children (hard to spot, blank page everywhere).

2c. Loading() for SPA routes

If any route assigned to this shell has render: "spa", the shell SHOULD export a Loading() function that renders a placeholder during the client-only data fetch. Without it, users see blank content during navigation.

2d. head() export

  • Optional, but recommended if the shell sets shared meta tags.
  • Verify return shape matches { title?, lang?, meta?, link?, script? }.
  • Flag shells whose head() returns undefined unconditionally — delete the export.

2e. ErrorBoundary export

ErrorBoundary is a valid export on both shells and routes (see ShellModule and RouteModule in packages/framework/src/types.ts). The runtime resolves routeMod.ErrorBoundary ?? shellModule.ErrorBoundary — a shell-level boundary is the fallback for every route under that shell when the route doesn't declare its own.

  • A shell exporting ErrorBoundary is fine — often the right place for a shared error fallback.
  • If both the shell and a route declare one, the route wins. Flag as info at most, and only if the shell boundary is thereby unreachable for every route under it (dead code).

2f. headers() export

Shells may export headers() to contribute response headers (merged with route headers()). Optional, but if present:

  • Verify the return shape is a plain HeadersInit.
  • Flag shells whose headers() returns undefined unconditionally — delete the export.

2g. loader export

Shells may export loader(args) for layout-level data, read with useShellData() in the shell and its routes. If present:

  • Run the audit-loaders checks on it: its return value is serialized into every page under the shell (shellData in the hydration state and the route-state JSON).
  • useShellData() returns undefined while the shell renders without its data (SPA loading state, an error boundary after the shell loader failed). Flag a shell that dereferences it unguarded.
  • Flag a shell loader used as an auth gate (redirecting anonymous users) with no gating middleware on its routes: navigations inside the shell ask the server to skip it. Gate in middleware.
  • Flag several routes under one shell whose loaders each fetch the same layout-level value (the current user): that belongs in the shell loader.

Step 3: Coverage and waste

Shell registration is mode-aware — check the mode field from the Step 1 JSON first:

  • Manifest apps (mode: "manifest"): shells are registered in defineApp({ shells }). Diff that registry against the per-route resolved shell/shellFile values from the inspect JSON — the JSON has no shell registry of its own, so "unused" means "registered in defineApp but referenced by no route or group".
  • Pages apps (mode: "pages"): there is no defineApp shell registry. Each _app file is auto-registered — src/pages/_app.tsx as "pages", src/pages/blog/_app.tsx as "pages:blog" — and owns the routes in its directory subtree (see https://pracht.resynapse.dev/docs/shells). Shells replace, they do not nest: the nearest _app above a route is the only one that renders it, so a directory shell must carry its own head()/headers() rather than inherit the parent's. "Unused shells" analysis does not apply; instead check pracht inspect routes --json and confirm each route's shell is the nearest _app you expect, and that no directory you meant to scope is silently falling back to "pages". An _app inside an underscore-reserved tree (src/pages/_components/_app.tsx) is a plain helper, and two _app files in one directory are a build, doctor, and verify error.

Then report:

  • Unused shells (manifest apps only): registered but referenced by no route or group. Recommend removal.
  • Single-use shells: shells used by exactly one route — sometimes a signal the layout should be inlined. Flag as info.
  • Routes without shells: routes resolved to no shell. Usually intentional for raw HTML responses, but worth listing.

Step 4: Report

Shell File Used by Issue Severity

Severities: error (document tags, missing children), warn (no Loading on SPA routes, empty headers()), info (unused, single-use, shell ErrorBoundary shadowed by route-level boundaries on every route).

Rules

  1. Source of truth is pracht inspect routes --json — it shows resolved shell-per-route after group inheritance.
  2. Read the shell source — do not infer from names.
  3. Loading() is a shell export (SPA-only fallback). ErrorBoundary is valid on both shells and routes: the shell's boundary is the fallback, the route's wins when both exist. Do not flag shell boundaries as bugs.
  4. Recommend deletions for unused shells; do not delete automatically.
  5. When in doubt about render mode interaction, cross-reference with tune-render-mode.

$ARGUMENTS

Files (pracht)
  • SKILL.md 6.7 KB
    ---
    name: audit-shells
    version: 1.4.0
    description: |
      Audit pracht shells: missing `Loading()` on SPA routes, `<html>`/`<head>`/
      `<body>` rendered inside a shell, shells that never render `children`, unused
      shells, and `ErrorBoundary` exports shadowed by every route under them.
      Use for "audit shells", "check shell composition", "find unused shells", "is my
      layout structured correctly".
    allowed-tools:
      - Bash
      - Read
      - Grep
      - Glob
    ---
    
    # Pracht Audit Shells
    
    Shells in pracht are named layout components composed around routes. The
    framework owns the document — shells must not render `<html>`, `<head>`, or
    `<body>`. They sit between the framework's HTML scaffold and the route
    component.
    
    ## Step 1: Enumerate
    
    MCP: when the pracht MCP server is registered (`pracht dev-mcp`), prefer its
    `inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
    shelling out.
    
    ```bash
    pracht inspect routes --json
    ```
    
    Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
    wired up; run it from the app root.
    
    For every shell used by the app, read its source file. Also list which routes
    use which shell (the JSON output includes resolved `shell` and `shellFile` per
    route). The JSON's top-level `mode` field tells you which router the app uses
    — see Step 3 for how that changes shell discovery.
    
    ## Step 2: Per-shell checks
    
    For each shell file:
    
    ### 2a. Document-level tag misuse
    
    Grep for `<html`, `<head>`, `<body>`, `<meta`, `<title>`, `<link rel`. Any of
    these inside a shell is a bug — the framework injects them based on `head()`
    exports. Recommend moving meta/title to the shell's `head()` export and
    removing the JSX tags.
    
    ### 2b. `Shell` export shape
    
    - Must be a function component named `Shell`.
    - Must accept `{ children }: ShellProps`.
    - Must render `{children}` somewhere — flag shells that never render
      `children` (hard to spot, blank page everywhere).
    
    ### 2c. `Loading()` for SPA routes
    
    If any route assigned to this shell has `render: "spa"`, the shell SHOULD
    export a `Loading()` function that renders a placeholder during the
    client-only data fetch. Without it, users see blank content during navigation.
    
    ### 2d. `head()` export
    
    - Optional, but recommended if the shell sets shared meta tags.
    - Verify return shape matches `{ title?, lang?, meta?, link?, script? }`.
    - Flag shells whose `head()` returns `undefined` unconditionally — delete the
      export.
    
    ### 2e. `ErrorBoundary` export
    
    `ErrorBoundary` is a valid export on **both** shells and routes (see
    `ShellModule` and `RouteModule` in `packages/framework/src/types.ts`). The
    runtime resolves `routeMod.ErrorBoundary ?? shellModule.ErrorBoundary` — a
    shell-level boundary is the fallback for every route under that shell when
    the route doesn't declare its own.
    
    - A shell exporting `ErrorBoundary` is fine — often the right place for a
      shared error fallback.
    - If **both** the shell and a route declare one, the route wins. Flag as
      `info` at most, and only if the shell boundary is thereby unreachable for
      every route under it (dead code).
    
    ### 2f. `headers()` export
    
    Shells may export `headers()` to contribute response headers (merged with
    route `headers()`). Optional, but if present:
    - Verify the return shape is a plain `HeadersInit`.
    - Flag shells whose `headers()` returns `undefined` unconditionally — delete
      the export.
    
    ### 2g. `loader` export
    
    Shells may export `loader(args)` for layout-level data, read with
    `useShellData()` in the shell and its routes. If present:
    - Run the audit-loaders checks on it: its return value is serialized into
      every page under the shell (`shellData` in the hydration state and the
      route-state JSON).
    - `useShellData()` returns `undefined` while the shell renders without its
      data (SPA loading state, an error boundary after the shell loader failed).
      Flag a shell that dereferences it unguarded.
    - Flag a shell loader used as an auth gate (redirecting anonymous users) with
      no gating middleware on its routes: navigations inside the shell ask the
      server to skip it. Gate in middleware.
    - Flag several routes under one shell whose loaders each fetch the same
      layout-level value (the current user): that belongs in the shell loader.
    
    ## Step 3: Coverage and waste
    
    Shell registration is mode-aware — check the `mode` field from the Step 1
    JSON first:
    
    - **Manifest apps** (`mode: "manifest"`): shells are
      registered in `defineApp({ shells })`. Diff that registry against the
      per-route resolved `shell`/`shellFile` values from the inspect JSON — the
      JSON has no shell registry of its own, so "unused" means "registered in
      `defineApp` but referenced by no route or group".
    - **Pages apps** (`mode: "pages"`): there is no `defineApp` shell registry.
      Each `_app` file is auto-registered — `src/pages/_app.tsx` as `"pages"`,
      `src/pages/blog/_app.tsx` as `"pages:blog"` — and owns the routes in its
      directory subtree (see <https://pracht.resynapse.dev/docs/shells>). Shells **replace**, they do not
      nest: the nearest `_app` above a route is the only one that renders it, so a
      directory shell must carry its own `head()`/`headers()` rather than inherit
      the parent's. "Unused shells" analysis does not apply; instead check
      `pracht inspect routes --json` and confirm each route's `shell` is the
      nearest `_app` you expect, and that no directory you meant to scope is
      silently falling back to `"pages"`. An `_app` inside an underscore-reserved
      tree (`src/pages/_components/_app.tsx`) is a plain helper, and two `_app`
      files in one directory are a build, `doctor`, and `verify` error.
    
    Then report:
    
    - **Unused shells** (manifest apps only): registered but referenced by no
      route or group. Recommend removal.
    - **Single-use shells**: shells used by exactly one route — sometimes a
      signal the layout should be inlined. Flag as `info`.
    - **Routes without shells**: routes resolved to no shell. Usually intentional
      for raw HTML responses, but worth listing.
    
    ## Step 4: Report
    
    | Shell | File | Used by | Issue | Severity |
    | ----- | ---- | ------- | ----- | -------- |
    
    Severities: `error` (document tags, missing children), `warn` (no `Loading`
    on SPA routes, empty `headers()`), `info` (unused, single-use, shell
    `ErrorBoundary` shadowed by route-level boundaries on every route).
    
    ## Rules
    
    1. Source of truth is `pracht inspect routes --json` — it shows resolved
       shell-per-route after group inheritance.
    2. Read the shell source — do not infer from names.
    3. `Loading()` is a shell export (SPA-only fallback). `ErrorBoundary` is
       valid on both shells and routes: the shell's boundary is the fallback,
       the route's wins when both exist. Do not flag shell boundaries as bugs.
    4. Recommend deletions for unused shells; do not delete automatically.
    5. When in doubt about render mode interaction, cross-reference with
       `tune-render-mode`.
    
    $ARGUMENTS
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.