caspian-utils 0.1.25 → 0.2.0
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.
- package/dist/docs/auth.md +7 -6
- package/dist/docs/commands.md +53 -27
- package/dist/docs/components.md +281 -213
- package/dist/docs/core-runtime-map.md +4 -4
- package/dist/docs/fetch-data.md +19 -1
- package/dist/docs/index.md +2 -0
- package/dist/docs/project-structure.md +62 -62
- package/dist/docs/pulsepoint-runtime-map.md +63 -17
- package/dist/docs/pulsepoint.md +183 -5
- package/dist/docs/routing.md +57 -55
- package/dist/docs/static-export.md +120 -0
- package/package.json +1 -1
package/dist/docs/pulsepoint.md
CHANGED
|
@@ -350,6 +350,10 @@ Important:
|
|
|
350
350
|
- Component scripts are plain JavaScript executed with `new Function(...)`. Do not use `import`, `export`, or top-level `await` inside them.
|
|
351
351
|
- The runtime auto-returns supported top-level bindings from the script. Do not rely on manual `return { ... }` objects.
|
|
352
352
|
- `pp.props` contains the current prop bag for the component.
|
|
353
|
+
- `pp.props` is computed from the **rendered root element's attributes**, not from any server-side signature. A prop that never lands on the root is `undefined` in the browser, with no server error and no console warning. In a Python component this means every prop the template references must be forwarded through `get_attributes({...}, props)`; see [components.md](./components.md#every-prop-a-template-reads-must-be-forwarded-to-the-root).
|
|
354
|
+
- Prop value types depend on how the attribute reaches the root. An unevaluated brace expression such as `volume="{vol}"` is evaluated in the parent component's scope and keeps its real type (number, boolean, object, function). A literal attribute such as `volume="0"` arrives as the **string** `"0"`, so `volume === 0` is false. A valueless attribute arrives as boolean `true`. Coerce or compare loosely when the value may be a server-rendered literal.
|
|
355
|
+
- Attribute names round-trip through kebab-case: `isFullscreen` renders as `is-fullscreen` and returns as `pp.props.isFullscreen`.
|
|
356
|
+
- Attribute names that are JavaScript reserved words are dropped from the prop bag, so `pp.props.class` and `pp.props.for` never exist. Use a non-reserved prop name when the script must read the value.
|
|
353
357
|
- `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
|
|
354
358
|
- Props are not auto-injected as standalone top-level template or handler variables. Read them through `pp.props` or explicitly destructure them in the component script.
|
|
355
359
|
|
|
@@ -402,6 +406,13 @@ Hooks exposed inside component scripts through `pp`:
|
|
|
402
406
|
- `pp.reducer(reducer, initialState)` returns `[state, dispatch]`.
|
|
403
407
|
- `pp.context(token)` resolves a provided context value from ancestor components.
|
|
404
408
|
- `pp.portal(ref, target?)` registers a ref-managed element for portal rendering and returns an object that includes `sourceParent`.
|
|
409
|
+
- `pp.id()` returns a stable, DOM-safe unique id for the life of that hook slot.
|
|
410
|
+
- `pp.syncExternalStore(subscribe, getSnapshot)` subscribes to a mutable source outside PulsePoint and rerenders when its snapshot changes.
|
|
411
|
+
- `pp.imperativeHandle(ref, createHandle, deps?)` publishes an imperative API on a parent-owned ref instead of the raw DOM node.
|
|
412
|
+
- `pp.transition()` returns `[isPending, startTransition]`.
|
|
413
|
+
- `pp.deferredValue(value, initialValue?)` returns a copy of `value` that catches up one commit later.
|
|
414
|
+
- `pp.optimistic(passthrough, reducer?)` returns `[optimisticState, addOptimistic]`.
|
|
415
|
+
- `pp.errorBoundary()` returns `[error, reset]` and marks the component as an error boundary.
|
|
405
416
|
- `pp.props` exposes the current props.
|
|
406
417
|
|
|
407
418
|
Global helpers exposed through the `pp` singleton and also merged into the component runtime:
|
|
@@ -425,6 +436,12 @@ Notes:
|
|
|
425
436
|
- Older docs may call the RPC helper `pp.fetchFunction()`. In the current bundled runtime the implemented global API is `pp.rpc()`.
|
|
426
437
|
- Keep template-facing bindings at the top level so the AST-based exporter can see them.
|
|
427
438
|
- For predictable code generation, prefer passing an explicit dependency array to `pp.effect`, `pp.layoutEffect`, `pp.memo`, and `pp.callback`.
|
|
439
|
+
- Top-level destructuring is exported to template scope for every form: array patterns, object patterns, nested patterns, defaults, rest elements, and holes. `const [isPending, startTransition] = pp.transition()` and `const [error, reset] = pp.errorBoundary()` reach the template the same way `pp.state` does.
|
|
440
|
+
- `pp.id()` is derived from the component id plus the hook slot, so the same slot keeps its id across rerenders and two instances of the same component never collide. Use it for `id`/`for` pairing and `aria-*` references instead of hand-rolled counters.
|
|
441
|
+
- `pp.syncExternalStore` requires a referentially stable `subscribe`. Wrap it in `pp.callback(..., [])`, otherwise the store is unsubscribed and resubscribed on every render. The runtime re-reads the snapshot immediately after subscribing so a change that lands between render and subscription is not lost.
|
|
442
|
+
- `pp.transition` does not deprioritize rendering the way React's concurrent scheduler does, because PulsePoint renders synchronously. It does give a correct `isPending` flag for both synchronous and promise-returning scopes, which is the common use (disabling a submit button while `pp.rpc()` resolves). Overlapping scopes stay pending until the last one settles.
|
|
443
|
+
- `pp.optimistic` drops its pending actions as soon as `passthrough` changes, which is the point at which the server has confirmed or rejected the guess. Without a reducer, each action replaces the value.
|
|
444
|
+
- `pp.deferredValue` lags by one commit. Use it to keep an input responsive while an expensive derived subtree catches up.
|
|
428
445
|
|
|
429
446
|
Effect with cleanup and dependencies:
|
|
430
447
|
|
|
@@ -513,6 +530,160 @@ Portal pattern for overlays that must escape clipping ancestors:
|
|
|
513
530
|
|
|
514
531
|
`pp.portal(ref)` moves the ref-managed element to `document.body` by default, or to the element passed as the second argument. Portaled content keeps its logical component ancestry, so context, props, and events keep working.
|
|
515
532
|
|
|
533
|
+
Stable ids for accessible field pairing:
|
|
534
|
+
|
|
535
|
+
```html
|
|
536
|
+
<div>
|
|
537
|
+
<label for="{emailId}">Email</label>
|
|
538
|
+
<input id="{emailId}" type="email" aria-describedby="{hintId}" />
|
|
539
|
+
<p id="{hintId}">We never share this.</p>
|
|
540
|
+
|
|
541
|
+
<script>
|
|
542
|
+
const emailId = pp.id();
|
|
543
|
+
const hintId = pp.id();
|
|
544
|
+
</script>
|
|
545
|
+
</div>
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Never derive ids from an index or a module-level counter. `pp.id()` is the only form that stays stable across rerenders and unique across instances of the same component.
|
|
549
|
+
|
|
550
|
+
External store subscription:
|
|
551
|
+
|
|
552
|
+
```html
|
|
553
|
+
<section>
|
|
554
|
+
<p>{isNarrow ? "Compact layout" : "Wide layout"}</p>
|
|
555
|
+
|
|
556
|
+
<script>
|
|
557
|
+
const subscribe = pp.callback((onStoreChange) => {
|
|
558
|
+
const query = window.matchMedia("(max-width: 640px)");
|
|
559
|
+
query.addEventListener("change", onStoreChange);
|
|
560
|
+
return () => query.removeEventListener("change", onStoreChange);
|
|
561
|
+
}, []);
|
|
562
|
+
|
|
563
|
+
const isNarrow = pp.syncExternalStore(
|
|
564
|
+
subscribe,
|
|
565
|
+
() => window.matchMedia("(max-width: 640px)").matches
|
|
566
|
+
);
|
|
567
|
+
</script>
|
|
568
|
+
</section>
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Prefer this over `pp.state` plus `pp.effect` for anything the component does not own. It reads the snapshot during render and closes the gap between render and subscription, which a hand-rolled effect silently drops.
|
|
572
|
+
|
|
573
|
+
Imperative handle for a child-owned API. The parent passes its own ref down as an ordinary prop:
|
|
574
|
+
|
|
575
|
+
```html
|
|
576
|
+
<!-- parent -->
|
|
577
|
+
<div>
|
|
578
|
+
<button onclick="{dialogControl.current.open()}">Open</button>
|
|
579
|
+
<x-confirm-dialog control-ref="{dialogControl}" />
|
|
580
|
+
|
|
581
|
+
<script>
|
|
582
|
+
const dialogControl = pp.ref(null);
|
|
583
|
+
</script>
|
|
584
|
+
</div>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
```html
|
|
588
|
+
<!-- ConfirmDialog component -->
|
|
589
|
+
<section>
|
|
590
|
+
<script>
|
|
591
|
+
const [open, setOpen] = pp.state(false);
|
|
592
|
+
|
|
593
|
+
pp.imperativeHandle(
|
|
594
|
+
pp.props.controlRef,
|
|
595
|
+
() => ({
|
|
596
|
+
open: () => setOpen(true),
|
|
597
|
+
close: () => setOpen(false),
|
|
598
|
+
}),
|
|
599
|
+
[]
|
|
600
|
+
);
|
|
601
|
+
</script>
|
|
602
|
+
|
|
603
|
+
<div hidden="{!open}">
|
|
604
|
+
<button onclick="{setOpen(false)}">Close</button>
|
|
605
|
+
</div>
|
|
606
|
+
</section>
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Notes:
|
|
610
|
+
|
|
611
|
+
- A prop whose bound expression evaluates to an object is passed through by reference, so `control-ref="{dialogControl}"` arrives as the real ref object on `pp.props.controlRef`.
|
|
612
|
+
- Do **not** author `pp-ref-forward` to make this work. That attribute is runtime-managed, and `pp-ref` on an `x-*` tag binds the child's root DOM node, which is a different feature. Use a normal prop when the parent needs a child-defined API rather than an element.
|
|
613
|
+
- The handle is published in the layout-effect phase and cleared on unmount, so parents should call into it from event handlers or effects, not during their own render.
|
|
614
|
+
|
|
615
|
+
Optimistic update around an RPC call:
|
|
616
|
+
|
|
617
|
+
```html
|
|
618
|
+
<section>
|
|
619
|
+
<button onclick="{addLike()}">Like ({likes})</button>
|
|
620
|
+
|
|
621
|
+
<script>
|
|
622
|
+
const [confirmedLikes, setConfirmedLikes] = pp.state(pp.props.likes ?? 0);
|
|
623
|
+
const [likes, addOptimisticLike] = pp.optimistic(
|
|
624
|
+
confirmedLikes,
|
|
625
|
+
(state, delta) => state + delta
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
async function addLike() {
|
|
629
|
+
addOptimisticLike(1);
|
|
630
|
+
const result = await pp.rpc("like_post", { postId: pp.props.postId });
|
|
631
|
+
setConfirmedLikes(result.likes);
|
|
632
|
+
}
|
|
633
|
+
</script>
|
|
634
|
+
</section>
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
The optimistic `+1` is discarded the moment `confirmedLikes` changes, so the server value never double-counts the guess.
|
|
638
|
+
|
|
639
|
+
Transition for pending async work:
|
|
640
|
+
|
|
641
|
+
```html
|
|
642
|
+
<section>
|
|
643
|
+
<button onclick="{save()}" disabled="{isSaving}">
|
|
644
|
+
{isSaving ? "Saving..." : "Save"}
|
|
645
|
+
</button>
|
|
646
|
+
|
|
647
|
+
<script>
|
|
648
|
+
const [isSaving, startSaving] = pp.transition();
|
|
649
|
+
|
|
650
|
+
function save() {
|
|
651
|
+
startSaving(() => pp.rpc("save_profile", { name: pp.props.name }));
|
|
652
|
+
}
|
|
653
|
+
</script>
|
|
654
|
+
</section>
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Error boundaries
|
|
658
|
+
|
|
659
|
+
`pp.errorBoundary()` marks a component as the recovery point for render and effect failures. Without one, a throwing render is logged to the console and leaves the subtree in whatever state it reached.
|
|
660
|
+
|
|
661
|
+
```html
|
|
662
|
+
<section>
|
|
663
|
+
<script>
|
|
664
|
+
const [error, reset] = pp.errorBoundary();
|
|
665
|
+
</script>
|
|
666
|
+
|
|
667
|
+
<div hidden="{!error}">
|
|
668
|
+
<p>Something went wrong: {error?.message}</p>
|
|
669
|
+
<button onclick="{reset()}">Try again</button>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<div hidden="{!!error}">
|
|
673
|
+
<x-report-chart />
|
|
674
|
+
</div>
|
|
675
|
+
</section>
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Behavior to rely on:
|
|
679
|
+
|
|
680
|
+
- Errors thrown during a child's render or effects bubble up the logical component ancestry to the nearest boundary. If nothing catches, the runtime logs `[PP-ERROR] Render Cycle Failed` as before.
|
|
681
|
+
- Unlike React, a boundary also catches throws from its **own** render and effects, because in Caspian the boundary and its fallback markup live in the same single-root component.
|
|
682
|
+
- Effect callbacks and effect cleanups take the same path as render failures, so a throwing subscription teardown is not silently swallowed.
|
|
683
|
+
- The boundary **latches**: it keeps holding the error until `reset()` is called.
|
|
684
|
+
- A boundary absorbs at most five errors without an intervening `reset()`, then logs and stops catching. This stops a fallback that itself throws from spinning the render loop. `reset()` re-arms it.
|
|
685
|
+
- Event handler errors are not routed to boundaries. Handle those with `try`/`catch` inside the handler, which matches React.
|
|
686
|
+
|
|
516
687
|
## Context
|
|
517
688
|
|
|
518
689
|
Context is implemented in the current runtime with a React-style provider pattern rather than a legacy `pp.provideContext(...)` helper. Because Caspian templates are HTML-first, authored provider tags should be written in lowercase HTML form, for example `<themecontext.provider>`, even when the JavaScript token is named `ThemeContext`.
|
|
@@ -567,11 +738,11 @@ Consumer example for a child component that receives the token through props:
|
|
|
567
738
|
|
|
568
739
|
When a child component needs the same token object, pass it from the provider scope as a prop such as `theme-token="{ThemeContext}"`.
|
|
569
740
|
|
|
570
|
-
## Props and nested components
|
|
571
|
-
|
|
572
|
-
In Caspian single-file Python components, the root element's attributes must be authored via `get_attributes(...)` and `{{ attributes }}`; see [components.md](./components.md#receiving-props-in-a-python-component). Attributes accepted from an `x-*` tag but not forwarded to the rendered root are dropped and never reach `pp.props`.
|
|
573
|
-
|
|
574
|
-
- Child component props are derived from DOM attributes.
|
|
741
|
+
## Props and nested components
|
|
742
|
+
|
|
743
|
+
In Caspian single-file Python components, the root element's attributes must be authored via `get_attributes(...)` and `{{ attributes }}`; see [components.md](./components.md#receiving-props-in-a-python-component). Attributes accepted from an `x-*` tag but not forwarded to the rendered root are dropped and never reach `pp.props`.
|
|
744
|
+
|
|
745
|
+
- Child component props are derived from DOM attributes.
|
|
575
746
|
- Attribute names are converted from kebab-case to camelCase for the prop bag.
|
|
576
747
|
- Native `on*` attributes and `pp-component` are not included in props.
|
|
577
748
|
- Empty attributes become boolean `true` props.
|
|
@@ -606,6 +777,7 @@ Nested components:
|
|
|
606
777
|
- Use `pp-spread="{...attrs}"` to spread an object expression into attributes.
|
|
607
778
|
- `pp-spread` omits nullish values, omits known HTML boolean attributes when their value is `false`, emits them bare when `true`, preserves string-valued `aria-*`/`data-*` booleans, and escapes `&`, `"`, and `<` in emitted attribute values.
|
|
608
779
|
- Use plain `key` for keyed diffing. `pp-key` is not implemented.
|
|
780
|
+
- Only top-level declarations in the component script reach template scope, but every destructuring shape is supported there: `const [a, b] = anyTuple()`, `const [, only] = pair`, `const [first = 1, ...rest] = list`, `const [{ id }] = rows`, and the object-pattern forms. A binding declared inside a block or function is not exported.
|
|
609
781
|
|
|
610
782
|
Example:
|
|
611
783
|
|
|
@@ -889,6 +1061,12 @@ These are current runtime caveats that matter for authors and AI tools:
|
|
|
889
1061
|
- `pp.context()` resolves through ancestor components, not the current component's own pending providers.
|
|
890
1062
|
- `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
|
|
891
1063
|
- `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
|
|
1064
|
+
- Top-level array destructuring is exported to template scope for any initializer, not only `pp.state` and `pp.reducer`. A binding declared inside a block or function is still not exported.
|
|
1065
|
+
- `pp.transition()` does not deprioritize work. PulsePoint renders synchronously; the hook provides an accurate `isPending` flag, not a concurrent scheduler.
|
|
1066
|
+
- `pp.syncExternalStore(...)` resubscribes whenever `subscribe` changes identity, so pass a `pp.callback(..., [])`-wrapped function.
|
|
1067
|
+
- `pp.optimistic(...)` clears its pending actions on the render where `passthrough` changes, using a render-phase update rather than an effect, so there is no frame showing stale optimistic output.
|
|
1068
|
+
- `pp.errorBoundary()` catches render, effect, and effect-cleanup throws, including the boundary component's own. It does not catch event-handler errors. It latches until `reset()` and stops catching after five captures without a reset.
|
|
1069
|
+
- `pp.imperativeHandle(...)` needs the parent's ref passed down as an ordinary prop. `pp-ref` on an `x-*` tag is a different feature that binds the child's root DOM node, and `pp-ref-forward` is runtime-managed and must not be authored.
|
|
892
1070
|
|
|
893
1071
|
## Final reminder
|
|
894
1072
|
|
package/dist/docs/routing.md
CHANGED
|
@@ -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.
|