caspian-utils 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ related:
10
10
  - /docs/mcp
11
11
  - /docs/file-uploads
12
12
  - /docs/routing
13
+ - /docs/static-export
13
14
  - /docs/project-structure
14
15
  - /docs/index
15
16
  ---
@@ -98,29 +99,51 @@ Only use this when the current project's `package.json` actually defines `npm ru
98
99
 
99
100
  Do not use `npm run build` as the default validation step for routine route, feature, or documentation edits.
100
101
 
102
+ ### Export the app to static HTML and preview it
103
+
104
+ ```bash
105
+ # Export to static/ (SSG, like Next.js `output: export`)
106
+ npm run static
107
+
108
+ # Serve the exported static/ folder over HTTP on an auto-selected free port
109
+ npm run static:serve
110
+ ```
111
+
112
+ Use `npm run static` when the user wants a folder of static HTML for a static host, and `npm run static:serve` to preview that folder locally.
113
+
114
+ This is an **app-owned build convention**, not a shipped Caspian feature and not gated by a `caspian.config.json` flag. Only use these when the current project's `package.json` actually defines them and `settings/build-static.py` (exporter) and `settings/serve-static.py` (preview server) exist.
115
+
116
+ Two things AI must keep correct:
117
+
118
+ - The `static` script must run `npm run build` (Tailwind **and** `projectName`) before `settings/build-static.py`, because the exporter walks the route index in `settings/files-list.json` that `projectName` regenerates. A build that only runs `tailwind:build` can export from a stale route index.
119
+ - `settings/serve-static.py` auto-selects a free port starting at a preferred default (8000) and binds loopback `127.0.0.1` by default; read the port it prints rather than assuming 8000 or reading `settings/bs-config.json` (that file is the dev BrowserSync source of truth, not the static preview).
120
+
121
+ See [static-export.md](./static-export.md) for the full export scope policy, `static_paths` dynamic-route pre-rendering, and the preview-server security and port behavior.
122
+
101
123
  ### Regenerate ORM after schema changes
102
124
 
103
125
  ```bash
104
126
  npx prisma migrate dev
105
127
 
106
- # If the change requires refreshed seed data:
107
- npx prisma generate
108
- # Destructive-data warning: ask the user for explicit approval before running this.
109
- npx prisma db seed
110
-
111
- npx ppy generate
112
- ```
113
-
114
- Use when `prisma/schema.prisma` changes and you need migrations, seed flow, and the generated Python ORM layer to stay aligned.
115
-
116
- `npx prisma db seed` is a delicate operation because the configured seed script may clean tables or replace existing rows before inserting seed data. Before running it, an AI agent must say the exact command it plans to run, warn that it can delete or overwrite database data, confirm the active datasource when practical, and wait for explicit user approval.
128
+ # If the change requires refreshed seed data:
129
+ npx prisma generate
130
+ # Destructive-data warning: ask the user for explicit approval before running this.
131
+ npx prisma db seed
132
+
133
+ npx ppy generate
134
+ ```
135
+
136
+ Use when `prisma/schema.prisma` changes and you need migrations, seed flow, and the generated Python ORM layer to stay aligned.
137
+
138
+ `npx prisma db seed` is a delicate operation because the configured seed script may clean tables or replace existing rows before inserting seed data. Before running it, an AI agent must say the exact command it plans to run, warn that it can delete or overwrite database data, confirm the active datasource when practical, and wait for explicit user approval.
117
139
 
118
140
  ## 2. Script Guardrails
119
141
 
120
142
  - Before using an optional feature, confirm its flag in `caspian.config.json`.
121
143
  - If the flag is false and the user wants that feature, ask first, then update `caspian.config.json` and run `npx casp update project` before assuming feature-managed files or scripts exist.
122
144
  - Do not run `package.json` scripts by default just because source files changed.
123
- - Treat `npm run dev` and `npm run build` as opt-in workflows.
145
+ - Treat `npm run dev`, `npm run build`, `npm run static`, and `npm run static:serve` as opt-in workflows.
146
+ - Use `npm run static` only when the user wants a static HTML export, and `npm run static:serve` only to preview it. Both are app-owned conventions; confirm the scripts and `settings/build-static.py` / `settings/serve-static.py` exist first.
124
147
  - Use `npm run dev` only when the user explicitly asks to start the local stack or the task truly needs that running workflow.
125
148
  - Use `npm run build` only for deployment prep or an explicit build request.
126
149
  - Treat `public/css/styles.css`, `settings/component-map.json`, `settings/files-list.json`, `__pycache__/`, and `.pyc` files as generated outputs when a script intentionally runs.
@@ -182,12 +205,12 @@ In skip-prompt mode, the default feature values are:
182
205
 
183
206
  - `backendOnly: false`
184
207
  - `tailwindcss: false`
185
- - `typescript: false`
186
- - `mcp: false`
187
- - `prisma: false`
188
- - `websocket: false`
189
-
190
- If a project needs WebSockets after creation, set `websocket: true` in `caspian.config.json`, then run the project update workflow so framework-managed files and any scaffolded socket surfaces stay aligned before assuming `@app.websocket(...)` routes or `src/lib/websocket/**` exist.
208
+ - `typescript: false`
209
+ - `mcp: false`
210
+ - `prisma: false`
211
+ - `websocket: false`
212
+
213
+ If a project needs WebSockets after creation, set `websocket: true` in `caspian.config.json`, then run the project update workflow so framework-managed files and any scaffolded socket surfaces stay aligned before assuming `@app.websocket(...)` routes or `src/lib/websocket/**` exist.
191
214
 
192
215
  ### 4.2 Backend-only combinations
193
216
 
@@ -598,18 +621,18 @@ The create and update commands above are not the whole maintenance story for a P
598
621
  ```bash
599
622
  npx prisma migrate dev
600
623
 
601
- # Only when seed flow or prisma/seed.ts depends on the new schema:
602
- npx prisma generate
603
- # Destructive-data warning: ask the user for explicit approval before running this.
604
- npx prisma db seed
605
-
606
- npx ppy generate
607
- ```
624
+ # Only when seed flow or prisma/seed.ts depends on the new schema:
625
+ npx prisma generate
626
+ # Destructive-data warning: ask the user for explicit approval before running this.
627
+ npx prisma db seed
628
+
629
+ npx ppy generate
630
+ ```
608
631
 
609
632
  Use this order:
610
633
 
611
634
  1. Run `npx prisma migrate dev` first so migrations and the development database stay aligned.
612
- 2. If `prisma/seed.ts` or seed data depends on the new schema, run `npx prisma generate`, then request explicit user approval before running `npx prisma db seed` because the seed script may delete or overwrite database data.
635
+ 2. If `prisma/seed.ts` or seed data depends on the new schema, run `npx prisma generate`, then request explicit user approval before running `npx prisma db seed` because the seed script may delete or overwrite database data.
613
636
  3. Run `npx ppy generate` last so the generated Python ORM layer matches the updated Prisma schema.
614
637
 
615
638
  Do not manually create or edit these generated files:
@@ -637,6 +660,8 @@ See [database.md](./database.md) for the full schema, migration, seed, and async
637
660
  | Update current project to beta safely in PowerShell | `npx casp update project --tag beta` |
638
661
  | Update current project to an exact version | `npx casp update project --version 1.2.3 -y` |
639
662
  | Regenerate Python ORM after schema changes | `npx prisma migrate dev` then optional seed commands, then `npx ppy generate` |
663
+ | Export the app to static HTML (SSG) | `npm run static` (when the project defines it) |
664
+ | Preview the exported static build over HTTP | `npm run static:serve` (auto-selects a free port) |
640
665
 
641
666
  ## 10. Configuration Notes
642
667
 
@@ -682,8 +707,9 @@ If an AI agent is deciding which command flow to use, apply these rules first.
682
707
  - Treat `--tailwindcss` and `--typescript` as frontend-oriented flags. They are not meaningful in normal backend-only scaffolds.
683
708
  - Treat starter kit defaults as a base layer that can be overridden by explicit flags.
684
709
  - Read `caspian.config.json` before running update commands.
685
- - When `prisma/schema.prisma` changes, run `npx prisma migrate dev` first, then any required seed refresh, then `npx ppy generate`. Before running `npx prisma db seed`, warn the user that it can delete or overwrite data and wait for explicit approval.
710
+ - When `prisma/schema.prisma` changes, run `npx prisma migrate dev` first, then any required seed refresh, then `npx ppy generate`. Before running `npx prisma db seed`, warn the user that it can delete or overwrite data and wait for explicit approval.
686
711
  - Never hand-edit generated Python ORM files under `src/lib/prisma/` or `settings/prisma-schema.json`.
687
712
  - Read [database.md](./database.md) before generating Prisma schema, migration, seed, or ORM guidance.
713
+ - Read [static-export.md](./static-export.md) before generating or explaining static export (`npm run static`), dynamic-route pre-rendering with `static_paths`, or the `npm run static:serve` preview server. Confirm the scripts and `settings/build-static.py` / `settings/serve-static.py` exist first.
688
714
  - Read [routing.md](./routing.md) before generating or modifying route folders under `src/app/`.
689
715
  - Read [project-structure.md](./project-structure.md) before placing generated files into the project.
@@ -95,6 +95,7 @@ The packaged Caspian docs referenced by this index live here:
95
95
  - `metadata.md` - Static and dynamic metadata, SEO inheritance, and Open Graph or Twitter card tags
96
96
  - `routing.md` - Next.js App Router-style file-based routing with `src/app`, dynamic segments, dashboard and section layouts, route groups, nested layouts, shared-shell scroll-reset ownership, single-parent authored templates, and backend Python companions that do not own visible markup
97
97
  - `project-structure.md` - Default Caspian layout and where route files, reusable UI in `src/components/`, reusable non-UI code in `src/lib/`, and database files belong
98
+ - `static-export.md` - App-owned static HTML export (SSG, like Next.js `output: export`) via `npm run static`/`settings/build-static.py`, dynamic-route pre-rendering with `static_paths`, and the robust port-aware preview server `npm run static:serve`/`settings/serve-static.py`; not a shipped feature and not gated by a `caspian.config.json` flag, so verify the scripts exist in the project
98
99
  - `testing.md` - Recommended app-owned testing, type-checking, and linting gate over `main.py` and `src/**` (pyright + ruff + pytest behind one command, so the gate and Pylance in the editor report the same thing), plus the auto-fix command and why ruff must not auto-delete component imports used as `<x-*>` tags (F401 unfixable); not a shipped feature and not gated by a `caspian.config.json` flag, so verify the actual command, tools, and config in the project
99
100
 
100
101
  ## AI Retrieval Notes
@@ -118,6 +119,7 @@ Preferred lookup order:
118
119
  13. Use `ai-validation-checklist.md` when you want to verify that the docs lead AI to the correct files and behavior checkpoints.
119
120
  14. Keep `index.md` and cross-links aligned so AI can quickly discover the right doc.
120
121
  15. If the task is about tests, type checking, linting, a quality gate, CI checks, or making an app production-ready, read `testing.md`. It is an app-owned convention (not a shipped feature and not gated by a `caspian.config.json` flag), so confirm the actual gate command in `package.json`, the orchestrator in `settings/`, and the tools and config in `pyproject.toml` before assuming they exist.
122
+ 16. If the task is about static export, static-site generation, `output: export`-style output, previewing a static build over HTTP, pre-rendering dynamic routes, or a preview server whose port is already in use, read `static-export.md`. It is also an app-owned convention (not a shipped feature, not gated by a `caspian.config.json` flag), so confirm `settings/build-static.py`, `settings/serve-static.py`, and the `static`/`static:serve` scripts exist in the project before assuming static export is available.
121
123
 
122
124
  ## Maintenance
123
125
 
@@ -29,9 +29,9 @@ Start with these rules:
29
29
  - Put application routes in `src/app/`.
30
30
  - For any route that renders a page, put the markup in `index.html`.
31
31
  - If a route is UI-only, `index.html` by itself is enough.
32
- - Add `index.py` only when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side logic.
33
- - Keep route-specific server logic in that route's `index.py`. Move logic into `src/lib/**` only when it is shared across routes, components, integrations, or features.
34
- - Use a standalone `index.py` only for non-visual routes such as redirects or action-only handlers.
32
+ - Add `index.py` only when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side logic.
33
+ - Keep route-specific server logic in that route's `index.py`. Move logic into `src/lib/**` only when it is shared across routes, components, integrations, or features.
34
+ - Use a standalone `index.py` only for non-visual routes such as redirects or action-only handlers.
35
35
  - When a folder owns child routes, use `layout.html` to wrap them. This is the default pattern for dashboards, admin sections, account areas, settings trees, and route groups.
36
36
  - In grouped shells with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content pane in `layout.html` when that pane should reset on child-route navigation while the shell sidebar or rail keeps its own scroll.
37
37
  - Use `layout.py` when a layout needs shared props or metadata before rendering. The `layout()` function may be synchronous or async.
@@ -177,17 +177,17 @@ Examples:
177
177
 
178
178
  Use `index.html` for the route template. This is the route's view layer.
179
179
 
180
- If a route renders visible page content, that content belongs here even when the route also has an `index.py` companion.
181
-
182
- Route templates can import reusable Python components with `<!-- @import ... -->` comments and render them with HTML-first `x-*` tags such as `<x-button />`. Use [components.md](./components.md) for the component authoring rules.
183
-
184
- Place those import comments at the top of the file, above the authored root element. They are file-level directives, not children of the route root.
185
-
186
- Keep route templates as composition, not as the place where all section markup accumulates. A route with tabs, dashboard panels, forms, tables, or repeated cards should import focused components for those responsibilities and assemble them with `x-*` tags. For example, a dashboard route can import `<x-dashboard-header />`, `<x-metric-strip />`, `<x-activity-tab />`, and `<x-settings-tab />` rather than placing every tab panel's full markup in `index.html` or one giant Python component.
180
+ If a route renders visible page content, that content belongs here even when the route also has an `index.py` companion.
181
+
182
+ Route templates can import reusable Python components with `<!-- @import ... -->` comments and render them with HTML-first `x-*` tags such as `<x-button />`. Use [components.md](./components.md) for the component authoring rules.
183
+
184
+ Place those import comments at the top of the file, above the authored root element. They are file-level directives, not children of the route root.
185
+
186
+ Keep route templates as composition, not as the place where all section markup accumulates. A route with tabs, dashboard panels, forms, tables, or repeated cards should import focused components for those responsibilities and assemble them with `x-*` tags. For example, a dashboard route can import `<x-dashboard-header />`, `<x-metric-strip />`, `<x-activity-tab />`, and `<x-settings-tab />` rather than placing every tab panel's full markup in `index.html` or one giant Python component.
187
187
 
188
188
  Route templates follow the same authored-vs-runtime contract documented in [pulsepoint.md](./pulsepoint.md) and the same single-root discipline documented in [components.md](./components.md): keep one authored parent node, keep any `<!-- @import ... -->` directives above that root, use a plain `<script>` inside that root when needed, and do not handwrite `pp-component` or `type="text/pp"`. That root may be a native HTML element or a single imported `x-*` component tag, but after expansion it must resolve to one final HTML root.
189
189
 
190
- When a route needs button clicks, form submits, input changes, filters, tabs, menus, uploads, polling, or reactive list updates, author those interactions as PulsePoint behavior in `index.html`. Use `onclick`, `oninput`, `onchange`, `onsubmit`, `pp.state(...)`, `pp-for`, refs, effects, and `pp.rpc(...)` instead of starting with `id` attributes plus `document.querySelector(...)` or `addEventListener(...)`. For simple form submits, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` over per-input `pp-ref` payload collection.
190
+ When a route needs button clicks, form submits, input changes, filters, tabs, menus, uploads, polling, or reactive list updates, author those interactions as PulsePoint behavior in `index.html`. Use `onclick`, `oninput`, `onchange`, `onsubmit`, `pp.state(...)`, `pp-for`, refs, effects, and `pp.rpc(...)` instead of starting with `id` attributes plus `document.querySelector(...)` or `addEventListener(...)`. For simple form submits, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` over per-input `pp-ref` payload collection.
191
191
 
192
192
  For AI-generated route templates, treat `src/app/**/index.html` the same way you would a React component body: return one parent node that contains the entire route markup. This includes any owned script.
193
193
 
@@ -225,32 +225,32 @@ Also bad:
225
225
  <aside class="dashboard-help">Tips</aside>
226
226
  ```
227
227
 
228
- Also bad:
229
-
230
- ```html
231
- <section class="dashboard-shell">
232
- <!-- @import StatsCard from "../components" -->
233
- <x-stats-card title="Users" value="42" />
234
- </section>
235
- ```
236
-
237
- Correct:
238
-
239
- ```html
240
- <!-- @import StatsCard from "../components" -->
241
-
242
- <section class="dashboard-shell">
243
- <x-stats-card title="Users" value="42" />
244
- </section>
245
- ```
228
+ Also bad:
229
+
230
+ ```html
231
+ <section class="dashboard-shell">
232
+ <!-- @import StatsCard from "../components" -->
233
+ <x-stats-card title="Users" value="42" />
234
+ </section>
235
+ ```
236
+
237
+ Correct:
238
+
239
+ ```html
240
+ <!-- @import StatsCard from "../components" -->
241
+
242
+ <section class="dashboard-shell">
243
+ <x-stats-card title="Users" value="42" />
244
+ </section>
245
+ ```
246
246
 
247
247
  Use [pulsepoint.md](./pulsepoint.md) when you need the full authored-vs-rendered example instead of this routing-focused reminder. Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when a route task names a specific PulsePoint directive or SPA behavior and you need the owning runtime file quickly.
248
248
 
249
249
  ### `index.py`
250
250
 
251
- Use `index.py` as the backend companion when a route needs metadata or async server-side logic. For routes that render UI, keep the markup in the sibling `index.html` and let `page()` call `render_page(__file__, ...)`. Do not inline route HTML inside `index.py`.
252
-
253
- If the logic belongs only to this route, keep it here. Examples include this page's first-render query, route-owned RPC actions, redirect decisions, route-specific validation, route-specific filters, and route-specific response shaping. Move code into `src/lib/**` when the same logic is reused or intentionally shared; do not extract one-route orchestration into a library just because it is Python code.
251
+ Use `index.py` as the backend companion when a route needs metadata or async server-side logic. For routes that render UI, keep the markup in the sibling `index.html` and let `page()` call `render_page(__file__, ...)`. Do not inline route HTML inside `index.py`.
252
+
253
+ If the logic belongs only to this route, keep it here. Examples include this page's first-render query, route-owned RPC actions, redirect decisions, route-specific validation, route-specific filters, and route-specific response shaping. Move code into `src/lib/**` when the same logic is reused or intentionally shared; do not extract one-route orchestration into a library just because it is Python code.
254
254
 
255
255
  Use `index.py` by itself only for non-visual routes such as redirects or action-only handlers. Because Caspian runs on FastAPI, the page entry should be async when it performs async work.
256
256
 
@@ -282,9 +282,9 @@ Use that tuple form when one route needs to influence a wrapper without turning
282
282
  Example root layout:
283
283
 
284
284
  ```html
285
- <body class="{{ layout.dashboard_body_class | default('') }}">
286
- <slot />
287
- </body>
285
+ <body class="{{ layout.dashboard_body_class | default('') }}">
286
+ <slot />
287
+ </body>
288
288
  ```
289
289
 
290
290
  Example route:
@@ -303,7 +303,7 @@ The key name is arbitrary, but it must match exactly between the dict returned f
303
303
 
304
304
  Use distinct names for those layout props. In the current router, the second dict is merged into the full layout context after path params and `request`, so a key such as `slug` or `request` can shadow an existing value.
305
305
 
306
- When a route owns a file manager or upload UI, keep the owning upload and delete `@rpc()` actions in that same `index.py` and move reusable filesystem or Prisma helpers into `src/lib/`. Do not move ordinary upload behavior into `main.py`. See [file-uploads.md](./file-uploads.md). When Prisma is enabled, database reads and writes in this route logic or its shared helpers should use the generated Prisma Python ORM.
306
+ When a route owns a file manager or upload UI, keep the owning upload and delete `@rpc()` actions in that same `index.py` and move reusable filesystem or Prisma helpers into `src/lib/`. Do not move ordinary upload behavior into `main.py`. See [file-uploads.md](./file-uploads.md). When Prisma is enabled, database reads and writes in this route logic or its shared helpers should use the generated Prisma Python ORM.
307
307
 
308
308
  For static and dynamic metadata rules, inheritance order, and social card fields, see [metadata.md](./metadata.md).
309
309
 
@@ -349,6 +349,8 @@ Use catch-all routes when the number of path segments is not fixed ahead of time
349
349
 
350
350
  Add a sibling `index.py` when that catch-all route also needs backend logic or metadata.
351
351
 
352
+ If the project uses static export (SSG) and you want a dynamic or catch-all route pre-rendered into the static build, export a `static_paths` provider from that route's `index.py` (Caspian's `getStaticPaths` equivalent). Without it, the static exporter skips the route by design. See [static-export.md](./static-export.md).
353
+
352
354
  ## Route Groups
353
355
 
354
356
  Wrap a folder name in parentheses to organize code without adding that segment to the URL.
@@ -387,11 +389,11 @@ Resolved SEO fields are exposed to layouts as `{{ metadata.* }}`, while values r
387
389
 
388
390
  Use `layout.html` for the shared wrapper markup of a subtree. Keep the visible shell here, not in `layout.py`.
389
391
 
390
- Follow the same authoring contract used by route templates: one authored parent node, top-of-file `<!-- @import ... -->` directives above that root, plain `<script>` inside the root when needed, and no handwritten `pp-component` or `type="text/pp"`. See [pulsepoint.md](./pulsepoint.md) for the canonical authored-vs-runtime explanation.
391
-
392
- Place child routes with a plain HTML `<slot />` tag. Caspian replaces that layout slot with the current child route or nested layout while rendering.
393
-
394
- For example, a page inside `/dashboard/settings` is wrapped by the root layout first and then by the dashboard layout.
392
+ Follow the same authoring contract used by route templates: one authored parent node, top-of-file `<!-- @import ... -->` directives above that root, plain `<script>` inside the root when needed, and no handwritten `pp-component` or `type="text/pp"`. See [pulsepoint.md](./pulsepoint.md) for the canonical authored-vs-runtime explanation.
393
+
394
+ Place child routes with a plain HTML `<slot />` tag. Caspian replaces that layout slot with the current child route or nested layout while rendering.
395
+
396
+ For example, a page inside `/dashboard/settings` is wrapped by the root layout first and then by the dashboard layout.
395
397
 
396
398
  Example root layout:
397
399
 
@@ -402,16 +404,16 @@ Example root layout:
402
404
  <title>{{ metadata.title }}</title>
403
405
  <meta name="description" content="{{ metadata.description }}" />
404
406
  </head>
405
- <body>
406
- <NavBar />
407
- <slot />
408
- </body>
409
- </html>
407
+ <body>
408
+ <NavBar />
409
+ <slot />
410
+ </body>
411
+ </html>
410
412
  ```
411
413
 
412
414
  ### `layout.py`
413
415
 
414
- If a layout needs shared props or metadata, add a `layout.py` file next to the HTML layout. Treat it as the backend companion for the layout, not as the place to author visible wrapper markup.
416
+ If a layout needs shared props or metadata, add a `layout.py` file next to the HTML layout. Treat it as the backend companion for the layout, not as the place to author visible wrapper markup.
415
417
 
416
418
  When `layout()` calls `render_layout(__file__, ...)`, root-validation errors are attributed to the sibling `layout.html` because that file is the authored template. If `layout()` returns a raw HTML string directly, Caspian treats that value as a runtime fragment instead of an authored template and wraps it in a runtime host root when needed.
417
419
 
@@ -441,7 +443,7 @@ In the common case, return a dict and let the sibling `layout.html` read those v
441
443
  - `(layout_html, props_dict)`: use the first item as the layout content and expose the second dict as `{{ layout.* }}`
442
444
  - `None`: fall back to the sibling `layout.html` with no extra layout props
443
445
 
444
- If you intentionally want to render `layout.html` immediately with direct local variables instead of the `layout.*` namespace, call `render_layout(__file__, {...})` and reference those keys directly in the template. `render_layout(...)` does not create a hidden child outlet; the layout template must still author the real `<slot />` element where child routes should render.
446
+ If you intentionally want to render `layout.html` immediately with direct local variables instead of the `layout.*` namespace, call `render_layout(__file__, {...})` and reference those keys directly in the template. `render_layout(...)` does not create a hidden child outlet; the layout template must still author the real `<slot />` element where child routes should render.
445
447
 
446
448
  Example:
447
449
 
@@ -466,7 +468,7 @@ def layout():
466
468
  )
467
469
  ```
468
470
 
469
- `layout()` may be synchronous or async in the installed runtime. Keep async layout work focused on shared subtree props or metadata; use `page()` or `@rpc()` when the work belongs to one route or a browser-triggered user action.
471
+ `layout()` may be synchronous or async in the installed runtime. Keep async layout work focused on shared subtree props or metadata; use `page()` or `@rpc()` when the work belongs to one route or a browser-triggered user action.
470
472
 
471
473
  Use [metadata.md](./metadata.md) when a layout also needs SEO defaults. Return dictionaries from `layout()` for visual or template props, and use `Metadata(...)` for title, description, and social tags.
472
474
 
@@ -520,15 +522,15 @@ If an AI agent is choosing where to add or update route code, apply these rules
520
522
  - Treat `src/app/` as the routing source of truth.
521
523
  - Use folder names to model URL segments.
522
524
  - If a route renders UI, create or update `index.html` for the markup.
523
- - Add `index.py` only when the same route needs metadata or server behavior; do not place route HTML in `index.py`.
524
- - Keep route-specific backend logic in the route's own `index.py`; extract to `src/lib/**` only for genuinely shared logic.
525
- - Keep visible page markup in `index.html` and shared subtree shells in `layout.html`; do not place route HTML in `index.py` or layout HTML in `layout.py`.
526
- - Keep `index.html` as a short assembly of focused `x-*` components. When a route area has its own responsibility, such as a tab panel, settings form, data table, toolbar, or metric section, create a component for that area and pass route data into it as props.
527
- - Use PulsePoint as the first-party interaction model for route and layout HTML. Avoid custom DOM wiring for normal events and reactivity.
525
+ - Add `index.py` only when the same route needs metadata or server behavior; do not place route HTML in `index.py`.
526
+ - Keep route-specific backend logic in the route's own `index.py`; extract to `src/lib/**` only for genuinely shared logic.
527
+ - Keep visible page markup in `index.html` and shared subtree shells in `layout.html`; do not place route HTML in `index.py` or layout HTML in `layout.py`.
528
+ - Keep `index.html` as a short assembly of focused `x-*` components. When a route area has its own responsibility, such as a tab panel, settings form, data table, toolbar, or metric section, create a component for that area and pass route data into it as props.
529
+ - Use PulsePoint as the first-party interaction model for route and layout HTML. Avoid custom DOM wiring for normal events and reactivity.
528
530
  - When the user asks for a dashboard, admin area, account section, or any grouped subtree of child routes, create a parent folder with `layout.html` and place the child routes beneath it. Follow the same mental model as the Next.js App Router.
529
531
  - Use a normal folder such as `dashboard/` when the segment should appear in the URL. Use `(group)/` only when the folder should organize or wrap child routes without adding a path segment.
530
532
  - Use [cache.md](./cache.md) when an `index.py` route should opt into page-level HTML caching.
531
- - Use `layout.html` for shared wrappers and `layout.py` for layout-level props or metadata.
533
+ - Use `layout.html` for shared wrappers and `layout.py` for layout-level props or metadata.
532
534
  - For grouped shells with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content pane instead of the whole shell when only the page content should reset between child routes.
533
535
  - When one route needs to change a parent layout, return `(render_page(__file__, ...), {"dashboard_body_class": ...})` from `page()` and read that value as `{{ layout.dashboard_body_class }}` in the wrapping `layout.html`.
534
536
  - Use `layout.py` for layout props that should apply across an entire subtree. Use `render_layout(__file__, {...})` only when the layout should consume direct local variables such as `{{ my_class }}` instead of the standard `{{ layout.* }}` namespace.
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: Static Export
3
+ description: Use this guide when a Caspian app is exported to static HTML (SSG, like Next.js `output: export`) and previewed over HTTP. Use when the task mentions `npm run static`, `npm run static:serve`, `settings/build-static.py`, `settings/serve-static.py`, static-site generation, `static_paths`, a static preview server, or "the default port is already in use".
4
+ related:
5
+ title: Related docs
6
+ description: Static export walks the route/component index and boots the app, so pair it with routing, components, commands, and the metadata guides.
7
+ links:
8
+ - /docs/commands
9
+ - /docs/routing
10
+ - /docs/components
11
+ - /docs/metadata
12
+ - /docs/project-structure
13
+ - /docs/testing
14
+ - /docs/index
15
+ ---
16
+
17
+ This page covers exporting a Caspian app to a folder of static HTML (SSG, the equivalent of Next.js `output: export`) and previewing that folder over HTTP with a robust, port-aware local server.
18
+
19
+ ## When This Doc Applies
20
+
21
+ Read this doc when the task is about:
22
+
23
+ - exporting the app to static HTML for a static host (Netlify, Vercel, GitHub Pages, nginx, S3, any CDN),
24
+ - previewing the exported `static/` folder over HTTP locally,
25
+ - pre-rendering dynamic routes for a static build,
26
+ - troubleshooting a failing static build or a preview server whose port is already in use.
27
+
28
+ ## Feature Status And Gate
29
+
30
+ Static export is an **app-owned build convention layered on top of Caspian**, the same category as the `npm run check` quality gate in [testing.md](./testing.md). The framework itself ships no static exporter and no preview server. There is **no `caspian.config.json` flag** for it.
31
+
32
+ Because it is not a shipped feature, do not assume it exists in every Caspian project. Confirm it in the project first:
33
+
34
+ - `package.json` defines the `static` and `static:serve` scripts, and
35
+ - `settings/build-static.py` (the exporter) and `settings/serve-static.py` (the preview server) are present.
36
+
37
+ If those are absent, the project has not adopted static export; do not invent the scripts unless the user asks you to add them.
38
+
39
+ When `caspian.config.json` has `tailwindcss: true`, the export compiles Tailwind as part of the build (see the build order below). In `backendOnly` projects a static HTML export is usually not meaningful.
40
+
41
+ ## The Two Commands
42
+
43
+ ```bash
44
+ # 1. Export the app to static/ (build metadata + Tailwind, then render every static route)
45
+ npm run static
46
+
47
+ # 2. Preview the exported static/ folder over HTTP (robust, auto-selects a free port)
48
+ npm run static:serve
49
+ ```
50
+
51
+ ### `npm run static` — the exporter
52
+
53
+ The script is composed so the export always runs against a **fresh route/component index**:
54
+
55
+ ```
56
+ "static": "npm run build && uv run python settings/build-static.py"
57
+ ```
58
+
59
+ `npm run build` is `tailwind:build` **plus** `projectName`. The `projectName` step (`settings/project-name.ts`) regenerates `settings/files-list.json` and `settings/component-map.json` and clears stale `.casp/` and `caches/`. This matters: `settings/build-static.py` boots the app and iterates `get_files_index()`, which reads `settings/files-list.json` — **the route index that decides what gets exported**. If the export ran after only `tailwind:build` (skipping `projectName`), a newly added route or component could be missing from the index and silently never exported, and a removed one could error. Always regenerate metadata before exporting; reuse `npm run build` rather than duplicating just the Tailwind half.
60
+
61
+ `settings/build-static.py` then:
62
+
63
+ - boots the real ASGI app in-process and requests every route through Starlette's `TestClient`, so exported HTML is byte-identical to what the dev server serves (layouts, components, PulsePoint deferral, security headers — the whole pipeline),
64
+ - writes each page to `static/<route>/index.html` (`/` → `static/index.html`),
65
+ - mirrors public asset trees (`css`, `js`, `assets`, `uploads`, `favicon.ico`) into `static/`,
66
+ - runs in `APP_ENV=development` so the build does not require production secrets.
67
+
68
+ **Scope policy is "warn & skip"** — anything that cannot be fully static is reported and NOT written, so nothing broken ships silently:
69
+
70
+ - **Dynamic routes** (`[id]` / `[...slug]`) are skipped unless their `index.py` exports `static_paths` — Caspian's `getStaticPaths` equivalent. It may be a list of dicts (`[{"id": 1}]`), a list of scalars (`[1, 2]`, mapped onto the route's single param), or a sync/async callable returning either.
71
+ - **Auth-gated routes** redirect instead of returning a page, so they are skipped.
72
+ - **Non-200 or non-HTML** responses are skipped.
73
+
74
+ **Caveats that hold for ANY static build of a Caspian app** (state them when a user exports an interactive app): `pp.rpc()` server actions, auth/sessions, WebSockets, streaming, and per-request server data do NOT work without the Python backend. Pages that depend on them still render, but those interactions are dead in the output; the exporter flags pages that appear to use `pp.rpc()`. Asset URLs are root-absolute (`/css/...`, `/js/...`), so serve `static/` at a domain root or rewrite the base path for a subdirectory deploy.
75
+
76
+ ### `npm run static:serve` — the preview server
77
+
78
+ ```
79
+ "static:serve": "uv run python settings/serve-static.py"
80
+ ```
81
+
82
+ `settings/serve-static.py` is a hardened local preview server for the exported folder. It exists specifically so an occupied port never aborts the preview — it replaces the fragile `python -m http.server ... 8000` one-liner, which crashes with `address already in use` the moment the port is taken. Its behavior:
83
+
84
+ - **Auto-selects a free port.** It tries a preferred start port (default `8000`) and walks upward until one binds, then prints the actual URL it landed on. Detection is by genuine bind (no probe race), and it disables `SO_REUSEADDR` so that on Windows a port another process is actively listening on truly fails to bind instead of silently colliding.
85
+ - **Serves only `static/`** — no traversal outside the exported folder — and adds `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`.
86
+ - **Binds loopback `127.0.0.1` by default**, so the preview is not exposed to the local network. Exposing is opt-in via `HOST=0.0.0.0` (only then does it print the LAN URL).
87
+ - **Fails fast** with a clear "build it first" message if `static/` was never exported.
88
+ - **Shuts down cleanly** on Ctrl+C.
89
+
90
+ Environment overrides (all optional): `HOST` (bind address), `PORT` (preferred start port), `PORT_TRIES` (how many ports to try, default 50). Invalid values fall back to the defaults with a warning instead of crashing.
91
+
92
+ Preview over HTTP — do **not** double-click `static/index.html` (`file://`): root-absolute asset paths break and browsers block ES module scripts from `file://` origins.
93
+
94
+ > The preview server's port has nothing to do with the dev BrowserSync ports in `./settings/bs-config.json`. `bs-config.json` is the source of truth for the running **dev** stack; the static preview is a separate process on its own auto-selected port. Read the port the serve command actually prints, not `bs-config.json`.
95
+
96
+ ## Files AI Usually Inspects
97
+
98
+ - `package.json` — confirm the `static` / `static:serve` scripts and their composition (the export must run `npm run build`, not just `tailwind:build`).
99
+ - `settings/build-static.py` — the exporter: route walking, `static_paths` resolution, skip policy, asset copy.
100
+ - `settings/serve-static.py` — the preview server: port-walk, loopback binding, headers, env overrides.
101
+ - `settings/project-name.ts` — regenerates `settings/files-list.json` and `settings/component-map.json` that the exporter walks.
102
+ - `src/app/**/index.py` — add `static_paths` here to pre-render a dynamic route.
103
+ - `main.py` and the installed `casp` runtime — own the actual render pipeline the exporter drives via `TestClient`; see [core-runtime-map.md](./core-runtime-map.md).
104
+
105
+ ## Verify Before Editing Or Explaining
106
+
107
+ - Confirm the scripts and both `settings/*.py` files exist before describing static export as available in the project.
108
+ - Confirm the `static` script runs `npm run build` (metadata + Tailwind) before `settings/build-static.py`; a build that only runs `tailwind:build` exports from a stale route index.
109
+ - For any dynamic route the user expects in the output, confirm its `index.py` exports `static_paths`; otherwise it is skipped by design.
110
+ - Read the port the serve command prints; do not assume `8000` or read `settings/bs-config.json` for it.
111
+ - Treat `static/`, `settings/files-list.json`, and `settings/component-map.json` as generated outputs — do not hand-edit them.
112
+
113
+ ## AI Retrieval Notes
114
+
115
+ - Use this doc when the task names static export, SSG, `output: export`, `npm run static`, `npm run static:serve`, `static_paths`, a static preview server, or a "port already in use" preview problem.
116
+ - Static export is app-owned tooling, not a shipped, config-gated Caspian feature — verify the scripts exist rather than assuming them.
117
+ - To pre-render dynamic routes, add `static_paths` to the route's `index.py` (the `getStaticPaths` equivalent); read [routing.md](./routing.md) for route/segment shape.
118
+ - Warn users that `pp.rpc()`, auth, WebSockets, streaming, and per-request data are inert in a static export; those need the Python backend.
119
+ - The preview server auto-selects a free port and binds loopback by default; exposing on the network (`HOST=0.0.0.0`) is opt-in.
120
+ - See [commands.md](./commands.md) for how `static` relates to `npm run build`, and [testing.md](./testing.md) for the sibling app-owned-convention pattern.
@@ -80,7 +80,8 @@ ignore = ["E501"]
80
80
  [tool.pyright]
81
81
  # Pylance (the VS Code Python extension) reads this same config, so the editor
82
82
  # and `npm run check` report the same thing instead of disagreeing.
83
- include = ["main.py", "src"]
83
+ # `settings/*.py` is included so the orchestrator scripts are type-checked too.
84
+ include = ["main.py", "src", "settings/*.py"]
84
85
  exclude = [".venv", "node_modules", "**/__pycache__"]
85
86
  # `basic` is Pylance's default mode. It catches reportArgumentType-style bugs
86
87
  # (e.g. passing a dict[str, str | None] to a TypedDict whose field is `str`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {