caspian-utils 0.0.1

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.
@@ -0,0 +1,368 @@
1
+ ---
2
+ title: PulsePoint Runtime Guide
3
+ description: Learn how AI agents should use PulsePoint as the default reactive frontend contract in Caspian, including the current TypeScript runtime, component script rules, and supported directives.
4
+ related:
5
+ title: Related docs
6
+ description: Read the route, component, data-fetching, and TypeScript docs alongside the PulsePoint runtime contract.
7
+ links:
8
+ - /docs/pages-and-layouts
9
+ - /docs/import-component
10
+ - /docs/fetch-data
11
+ - /docs/typescript
12
+ ---
13
+
14
+ ## Purpose
15
+
16
+ This file documents the PulsePoint runtime that is actually implemented in the current TypeScript source under `ts/` and verified by `ts/__tests__/`. Treat it as the working contract for AI-generated code.
17
+
18
+ If a task involves `pp.state`, `pp.effect`, `pp.layoutEffect`, `pp-ref`, `pp-spread`, `pp-for`, context, portals, SPA navigation, or component boundary behavior, read this page first and keep generated code aligned with the runtime implemented in this repo.
19
+
20
+ PulsePoint is the default reactive frontend layer for Caspian.
21
+
22
+ Do not assume React, Vue, Svelte, JSX, Alpine, HTMX, or older PulsePoint docs unless the task explicitly asks for a different frontend contract.
23
+
24
+ ## Default Frontend Rule
25
+
26
+ When a Caspian page needs reactive browser behavior, use PulsePoint.
27
+
28
+ - Use PulsePoint component roots, scripts, directives, and runtime helpers for interactive UI.
29
+ - Keep server-rendered HTML plus PulsePoint enhancement as the baseline architecture.
30
+ - Only introduce another frontend runtime when the user explicitly asks for it or the project already depends on one.
31
+
32
+ ## Source layer vs raw runtime
33
+
34
+ - Source authoring under `src/` may use convenience syntax that is transformed before it reaches the browser.
35
+ - This page describes the raw browser runtime implemented in `ts/`.
36
+ - When source-layer examples conflict with the TypeScript runtime, follow the TypeScript runtime.
37
+ - In the raw runtime, component logic uses `<script>`, not plain `<script>`.
38
+
39
+ ## Runtime shape
40
+
41
+ PulsePoint is a browser-side component runtime. It executes one component script per root, renders HTML from the component scope, morphs the DOM in place, binds native event handlers, restores focus, updates refs, applies portals, and then runs layout/effect hooks.
42
+
43
+ A component root is any element with `pp-component`.
44
+
45
+ Important:
46
+
47
+ - `pp-component` is the instance id used for the component registry, saved state, cached template, parent tracking, and instance lookup.
48
+ - Reusing the same `pp-component` value for multiple live roots will destroy the previous registered instance and replace it with the new one.
49
+ - If a root is manually constructed with an empty `pp-component`, the runtime will assign an anonymous id. AI-generated markup should still use stable unique ids.
50
+ - `pp.mount()` bootstraps every `[pp-component]` in the document, so do not manually instantiate `Component` in authored app code.
51
+
52
+ ## Component roots and scripts
53
+
54
+ - Each runtime component root should have at most one component script.
55
+ - The runtime only treats `<script>` as component logic.
56
+ - The script lookup walks the current root and skips nested `pp-component` boundaries, so a parent does not consume a child component's script.
57
+ - If multiple matching scripts exist in the same root, the first matching owned script wins. Generate one script per root.
58
+ - A scriptless component root still mounts and can receive props, refs, events, and nested children, but it has no local runtime scope beyond its props.
59
+ - Component scripts are plain JavaScript executed with `new Function(...)`. Do not use `import`, `export`, or top-level `await` inside them.
60
+ - The runtime auto-returns supported top-level bindings from the script. Do not rely on manual `return { ... }` objects.
61
+ - `pp.props` contains the current prop bag for the component.
62
+ - `pp.props.children` contains the root's initial inner HTML before the owning `script` is removed from the render template.
63
+
64
+ Bindings exported to the template:
65
+
66
+ - Top-level function declarations.
67
+ - Top-level identifier declarations such as `const title = "Pulse"`.
68
+ - Top-level object destructuring identifiers such as `const { title, subtitle } = pp.props`.
69
+ - Top-level array destructuring identifiers when the initializer is `pp.state(...)`, such as `const [count, setCount] = pp.state(0)`.
70
+
71
+ Bindings that should not be assumed:
72
+
73
+ - Generic top-level array destructuring is not auto-exported unless it comes from `pp.state(...)`.
74
+ - Nested declarations inside functions, conditions, loops, and callbacks are not auto-exported.
75
+ - There is no standalone `props` variable injected by the runtime. Use `pp.props`.
76
+
77
+ Example:
78
+
79
+ ```html
80
+ <div pp-component="counter-card-1">
81
+ <h2>{title}</h2>
82
+ <p>Count: {count}</p>
83
+ <button onclick="setCount(count + 1)">Increment</button>
84
+
85
+ <script>
86
+ const { title } = pp.props;
87
+ const [count, setCount] = pp.state(0);
88
+
89
+ function reset() {
90
+ setCount(0);
91
+ }
92
+ </script>
93
+ </div>
94
+ ```
95
+
96
+ ## Hooks and runtime API
97
+
98
+ Hooks exposed inside component scripts through `pp`:
99
+
100
+ - `pp.state(initial)` returns `[value, setValue]`.
101
+ - `pp.effect(callback, deps?)` runs after render and may return a cleanup function.
102
+ - `pp.layoutEffect(callback, deps?)` runs synchronously after DOM mutation/ref binding/portal application and before `pp.effect`.
103
+ - `pp.ref(initialValue?)` returns `{ current }`.
104
+ - `pp.memo(factory, deps)` memoizes a computed value.
105
+ - `pp.callback(callback, deps)` memoizes a function.
106
+ - `pp.reducer(reducer, initialState)` returns `[state, dispatch]`.
107
+ - `pp.context(token)` resolves a provided context value from ancestor components.
108
+ - `pp.provideContext(token, value)` provides a context value to descendant components.
109
+ - `pp.portal(ref, target?)` registers a ref-managed element for portal rendering and returns an object that includes `sourceParent`.
110
+ - `pp.props` exposes the current props.
111
+
112
+ Global helpers exposed on the `pp` singleton:
113
+
114
+ - `pp.createContext(defaultValue)` creates a context token.
115
+ - `pp.mount()` bootstraps the runtime. It is idempotent.
116
+ - `pp.redirect(url)` performs SPA-aware navigation when enabled.
117
+ - `pp.rpc(name, data?, optionsOrAbort?)` performs the current route RPC bridge.
118
+ - `pp.enablePerf()` enables render timing collection.
119
+ - `pp.disablePerf()` disables render timing collection.
120
+ - `pp.getPerfStats()` returns collected render timings.
121
+ - `pp.resetPerfStats()` clears collected render timings.
122
+
123
+ Notes:
124
+
125
+ - The global `pp` singleton auto-mounts once `pp-utilities.js` is loaded and the DOM is ready. Manual `pp.mount()` is still safe because it short-circuits after the first mount.
126
+ - `pp.state` setters accept either a value or an updater function.
127
+ - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Returned promises are not awaited.
128
+ - `pp.portal(ref)` defaults to `document.body` when no target is provided.
129
+ - Older docs may call the RPC helper `pp.fetchFunction()`. In the current TypeScript runtime the implemented global API is `pp.rpc()`.
130
+ - Keep template-facing bindings at the top level so the AST-based exporter can see them.
131
+
132
+ ## Context
133
+
134
+ Context is implemented in the current runtime.
135
+
136
+ How it works:
137
+
138
+ - Create a token with `pp.createContext(defaultValue)`.
139
+ - Provide a value from a component with `pp.provideContext(token, value)`.
140
+ - Read it in a descendant component with `pp.context(token)`.
141
+ - Resolution walks the logical component parent chain stored in the registry, not the live DOM.
142
+ - Portaled descendants still resolve providers through component ancestry.
143
+ - Descendant consumers rerender when a provided value changes.
144
+ - Unchanged provided values do not trigger consumer refreshes.
145
+ - If a provider stops providing a context or is destroyed, consumers fall back to the next ancestor provider or the token default.
146
+
147
+ Important:
148
+
149
+ - The same token object must be shared between provider and consumer.
150
+ - Context is component-level, not directive-based. There is no `pp-context` attribute.
151
+ - `pp.context(token)` resolves from ancestors. A component does not consume the value it provides in the same render.
152
+ - If provider and consumer live in different component script scopes, pass the token through props or store it in shared global/outer state.
153
+
154
+ Example:
155
+
156
+ ```html
157
+ <section pp-component="theme-provider-1">
158
+ <div pp-component="theme-label-1" theme-token="{ThemeContext}">
159
+ <p>{theme}</p>
160
+
161
+ <script>
162
+ const { themeToken } = pp.props;
163
+ const theme = pp.context(themeToken);
164
+ </script>
165
+ </div>
166
+
167
+ <script>
168
+ const ThemeContext = pp.createContext("light");
169
+ const [theme] = pp.state("dark");
170
+
171
+ pp.provideContext(ThemeContext, theme);
172
+ </script>
173
+ </section>
174
+ ```
175
+
176
+ ## Props and nested components
177
+
178
+ - Child component props are derived from DOM attributes.
179
+ - Attribute names are converted from kebab-case to camelCase for the prop bag.
180
+ - Native `on*` attributes and `pp-component` are not included in props.
181
+ - Empty attributes become boolean `true` props.
182
+ - Pure prop expressions such as `title="{pageTitle}"` are evaluated in the parent scope.
183
+ - Mixed strings such as `class="card {isActive ? 'active' : ''}"` are interpolated in the parent scope.
184
+ - Non-expression attribute values are passed through as strings.
185
+ - `children` is injected into props using the root's initial inner HTML.
186
+
187
+ Nested components:
188
+
189
+ - Nested `pp-component` roots are treated as component boundaries during DOM reconciliation.
190
+ - Parent prop interpolation still runs on nested child root attributes before the child refreshes.
191
+ - Nested roots that contain their own `script` are masked during parent template compilation.
192
+ - Scriptless nested component roots are not fully masked during parent template compilation in the current source. Avoid generating child-local interpolations inside a nested root unless that child has its own `script`.
193
+
194
+ ## Template expressions and attributes
195
+
196
+ - Use `{expression}` in text nodes and attribute values.
197
+ - Pure bindings like `value="{count}"` are evaluated as expressions.
198
+ - Mixed text like `class="card {isActive ? 'active' : ''}"` is supported.
199
+ - Arrays in template expressions are joined without commas.
200
+ - `null`, `undefined`, and boolean expression results render as an empty string in text output.
201
+ - Supported boolean attributes are normalized so truthy values emit the bare attribute and falsy values remove it.
202
+ - `<textarea value="{draft}"></textarea>` is normalized into textarea content.
203
+ - Use `pp-spread="{...attrs}"` to spread an object expression into attributes.
204
+ - `pp-spread` omits nullish values and escapes `&`, `"`, and `<` in emitted attribute values.
205
+ - Use plain `key` for keyed diffing. `pp-key` is not implemented.
206
+
207
+ Example:
208
+
209
+ ```html
210
+ <div pp-component="profile-card-1">
211
+ <button pp-spread="{...buttonAttrs}" hidden="{isLoading}">Save</button>
212
+
213
+ <script>
214
+ const buttonAttrs = {
215
+ class: "btn btn-primary",
216
+ "aria-label": "save",
217
+ };
218
+
219
+ const isLoading = false;
220
+ </script>
221
+ </div>
222
+ ```
223
+
224
+ ## Refs
225
+
226
+ - Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
227
+ - Use `pp-ref="{registerRef(id)}"` when you want the compiler to capture a dynamic ref expression.
228
+ - `pp.ref(null)` is the normal way to create a ref object.
229
+ - Callback refs and `{ current }` refs are both supported.
230
+ - Captured brace-form refs are compiled into an internal `data-pp-ref` token and rebound after render.
231
+ - Plain `pp-ref` bindings are preserved across rerenders, including no-op rerenders that skip DOM diffing.
232
+ - Ref callbacks may be called with `null` during cleanup.
233
+ - The runtime generates `data-pp-ref` internally. Do not author it.
234
+ - Do not author `pp-event-owner`, `pp-owner`, or `pp-dynamic-*` attributes by hand.
235
+
236
+ Example:
237
+
238
+ ```html
239
+ <div pp-component="focus-box-1">
240
+ <input pp-ref="nameInput" />
241
+ <button onclick="nameInput.current?.focus()">Focus</button>
242
+
243
+ <script>
244
+ const nameInput = pp.ref(null);
245
+ </script>
246
+ </div>
247
+ ```
248
+
249
+ ## Lists and keyed diffing
250
+
251
+ - Use `pp-for` only on `<template>`.
252
+ - Supported forms are `item in items` and `(item, index) in items`.
253
+ - The collection must be an array. Non-arrays are treated as empty lists.
254
+ - Loop content can contain interpolations, events, refs, and nested components.
255
+ - Event handlers inside loops are rewritten so loop variables still resolve after rerender.
256
+ - Use stable unique `key` values on repeated sibling elements.
257
+ - Duplicate keys trigger warnings and reduce diff quality.
258
+ - Keyed reconciliation preserves DOM identity across reorders and insertions.
259
+
260
+ Example:
261
+
262
+ ```html
263
+ <div pp-component="todo-list-1">
264
+ <ul>
265
+ <template pp-for="(todo, index) in todos">
266
+ <li key="{todo.id}">
267
+ {index + 1}. {todo.title}
268
+ <button onclick="removeTodo(todo.id)">Remove</button>
269
+ </li>
270
+ </template>
271
+ </ul>
272
+
273
+ <script>
274
+ const [todos, setTodos] = pp.state([
275
+ { id: 1, title: "First task" },
276
+ { id: 2, title: "Second task" },
277
+ ]);
278
+
279
+ function removeTodo(id) {
280
+ setTodos(todos.filter((todo) => todo.id !== id));
281
+ }
282
+ </script>
283
+ </div>
284
+ ```
285
+
286
+ ## Events
287
+
288
+ - Use native `on*` attributes such as `onclick`, `oninput`, and `onsubmit`.
289
+ - Event values may be raw code or wrapped in `{...}`.
290
+ - The runtime injects `event`, `e`, `$event`, `target`, `currentTarget`, and `el`.
291
+ - Do not use hyphenated event attrs like `on-click`.
292
+ - Event attributes are removed from the live DOM after binding and rebound after DOM morphing.
293
+ - Owned template/event-owner internals are runtime-managed. Do not author them directly.
294
+
295
+ ## SPA, loading, and navigation helpers
296
+
297
+ - `body[pp-spa="true"]` enables client-side navigation interception.
298
+ - `a[pp-spa="false"]` disables interception for that link.
299
+ - External links, downloads, `_blank`, and modifier-key clicks bypass SPA interception.
300
+ - `pp-loading-content="true"` marks the page region that gets swapped or faded during navigation.
301
+ - Route-specific loading states are looked up with `pp-loading-url`.
302
+ - `pp-loading-transition` accepts JSON with `fadeIn` and `fadeOut` timing values.
303
+ - `pp-reset-scroll="true"` resets scroll positions during navigation.
304
+ - Navigation dispatches `pp:navigation:start`, `pp:navigation:complete`, and `pp:navigation:error` events on `document`.
305
+
306
+ RPC notes:
307
+
308
+ - `pp.rpc(name, data?, optionsOrAbort?)` posts to the current route.
309
+ - Passing `true` as the third argument means `abortPrevious: true`.
310
+ - The options object supports `abortPrevious`, `onStream`, `onStreamError`, `onStreamComplete`, `onUploadProgress`, and `onUploadComplete`.
311
+ - File uploads switch to the XHR path when upload progress callbacks are needed.
312
+ - Streamed `text/event-stream` responses are supported when a stream handler is provided.
313
+ - Redirect headers are honored through `pp.redirect()`.
314
+
315
+ Notes:
316
+
317
+ - `pp.redirect()` uses SPA navigation for same-origin URLs when SPA mode is enabled. Otherwise it falls back to normal navigation.
318
+ - Root-layout mismatches during SPA navigation trigger a hard reload.
319
+ - `pp.mount()` bootstraps every `[pp-component]` it finds, so generated code should call it only through the global runtime if you are manually mounting at all.
320
+
321
+ ## AI rules
322
+
323
+ Use these rules when generating or editing PulsePoint runtime code:
324
+
325
+ - Treat PulsePoint as the default reactive frontend for Caspian app code.
326
+ - Use the current TypeScript runtime under `ts/` when it is available.
327
+ - Generate unique `pp-component` values per live instance.
328
+ - Use only `<script>` for raw runtime component logic.
329
+ - Keep template-facing variables at top level.
330
+ - Use `pp.createContext`, `pp.context`, and `pp.provideContext` for context. Do not invent `pp-context`.
331
+ - Keep `pp-for` on `<template>` and use plain `key`.
332
+ - Use native `on*` attributes, not framework-specific event syntax.
333
+ - Use refs and portals only through the implemented `pp` APIs.
334
+ - Use `pp.rpc()` for the raw TypeScript runtime API instead of older `pp.fetchFunction()` wording.
335
+ - Avoid generating internal runtime attributes.
336
+ - Avoid scriptless nested components when the child template contains its own bindings.
337
+
338
+ ## What to avoid
339
+
340
+ Do not generate these unless the current source explicitly adds support:
341
+
342
+ - React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
343
+ - `pp-context`
344
+ - `pp-key`
345
+ - `data-pp-ref`
346
+ - `pp-owner`
347
+ - `pp-event-owner`
348
+ - `pp-dynamic-script`
349
+ - `pp-dynamic-meta`
350
+ - `pp-dynamic-link`
351
+ - plain `<script>` for raw runtime component logic inside a component root
352
+ - `pp.fetchFunction()` as the current raw runtime helper name
353
+ - made-up hooks, directives, or globals not present in the current TypeScript source
354
+
355
+ ## Review notes
356
+
357
+ These are current runtime caveats that matter for authors and AI tools:
358
+
359
+ - `pp-component` is the registry key for instances, state, parent tracking, and templates. Treat it as unique per mounted root.
360
+ - Nested roots without their own `script` are not fully isolated during parent template compilation.
361
+ - The global `pp` singleton auto-mounts on DOM ready, and `pp.mount()` is idempotent.
362
+ - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
363
+ - `pp.context()` resolves through ancestor components, not the current component's own pending providers.
364
+ - `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
365
+
366
+ ## Final reminder
367
+
368
+ If a feature is not implemented in the current TypeScript runtime source, do not invent it.
@@ -0,0 +1,231 @@
1
+ ---
2
+ title: Routing
3
+ description: Understand Caspian's Next.js App Router-style file-based routing, including src/app conventions, index files, dynamic segments, route groups, and nested layouts.
4
+ related:
5
+ title: Related docs
6
+ description: Read the structure guide first, then use the metadata guide for SEO fields, the cache guide for route-level HTML reuse, and the PulsePoint runtime guide for interactive route templates.
7
+ links:
8
+ - /docs/project-structure
9
+ - /docs/cache
10
+ - /docs/metadata
11
+ - /docs/pulsepoint
12
+ - /docs/index
13
+ ---
14
+
15
+ Caspian follows the same mental model as the Next.js App Router: routes live under `src/app`, folders define URL segments, layouts nest automatically, and special folder names control grouping and dynamic matching.
16
+
17
+ The main difference is the file types. Instead of `page.tsx` and `layout.tsx`, Caspian uses `index.html`, `index.py`, `layout.html`, and optional Python companions for async server-side logic.
18
+
19
+ ## Overview
20
+
21
+ Caspian uses a high-performance file-system router built on top of FastAPI. Your directory structure becomes your URL structure.
22
+
23
+ Start with these rules:
24
+
25
+ - Put application routes in `src/app/`.
26
+ - Use `index.html` for a template-only route.
27
+ - Use `index.py` when the route needs metadata or async server-side logic.
28
+ - Use `layout.html` to wrap child routes.
29
+ - Use `layout.py` when a layout needs async data before rendering.
30
+
31
+ Use [cache.md](./cache.md) when an `index.py` route also needs declarative page caching via `Cache(...)`.
32
+
33
+ Framework internals note:
34
+
35
+ - Caspian's layout and route-resolution internals live in `.venv/Lib/site-packages/casp/layout.py`.
36
+ - Treat that file as framework code. Read it when the task is about routing internals, layout resolution, or metadata behavior inside Caspian itself.
37
+
38
+ See [metadata.md](./metadata.md) when a page or layout needs SEO fields.
39
+
40
+ ## Next.js App Router Mapping
41
+
42
+ If you already know the Next.js App Router, use this translation layer:
43
+
44
+ | Next.js concept | Caspian equivalent |
45
+ | --- | --- |
46
+ | `app/` | `src/app/` |
47
+ | `page.tsx` | `index.html` or `index.py` |
48
+ | `layout.tsx` | `layout.html` and optional `layout.py` |
49
+ | `[id]` | `[id]` |
50
+ | `[...slug]` | `[...slug]` |
51
+ | `(group)` | `(group)` |
52
+
53
+ This means most App Router habits carry over directly:
54
+
55
+ - Keep route logic close to the route folder.
56
+ - Model the URL with folders instead of a central route table.
57
+ - Use nested layouts for shared wrappers.
58
+ - Use route groups to organize code without changing the public path.
59
+
60
+ ## Core Concepts
61
+
62
+ Every route lives inside `src/app`. A route is a folder that contains either an `index.html` file, an `index.py` file, or both as part of the route implementation.
63
+
64
+ Examples:
65
+
66
+ | File | URL |
67
+ | --- | --- |
68
+ | `src/app/index.html` | `/` |
69
+ | `src/app/about/index.py` | `/about` |
70
+ | `src/app/blog/posts/index.html` | `/blog/posts` |
71
+
72
+ ### `index.html`
73
+
74
+ Use `index.html` for the route template. This is the route's view layer.
75
+
76
+ ### `index.py`
77
+
78
+ Use `index.py` when the route needs metadata or async server-side logic. Because Caspian runs on FastAPI, the page entry should be async.
79
+
80
+ Example:
81
+
82
+ ```python
83
+ from casp.layout import Metadata, render_page
84
+
85
+ metadata = Metadata(
86
+ title="Caspian Documentation | The Native Python Web Framework",
87
+ description="Explore the comprehensive documentation for Caspian.",
88
+ )
89
+
90
+ async def page():
91
+ return render_page(__file__)
92
+ ```
93
+
94
+ Use this pattern when the route needs to fetch data, compute metadata, or do other non-blocking server work before rendering.
95
+
96
+ For static and dynamic metadata rules, inheritance order, and social card fields, see [metadata.md](./metadata.md).
97
+
98
+ If the rendered HTML for that route is public and safe to reuse, declare route-level caching in the same file with `Cache(...)`. See [cache.md](./cache.md).
99
+
100
+ ## Dynamic Routes
101
+
102
+ Caspian supports dynamic URL segments using the same bracket syntax used by the Next.js App Router.
103
+
104
+ ### Dynamic Segments
105
+
106
+ Wrap a folder name in brackets to make it variable.
107
+
108
+ | File | Example URL |
109
+ | --- | --- |
110
+ | `src/app/users/[id]/index.py` | `/users/123` |
111
+
112
+ These segments are compiled into FastAPI path parameters for efficient route matching.
113
+
114
+ ### Catch-all Segments
115
+
116
+ Use an ellipsis inside brackets to match multiple path parts.
117
+
118
+ | File | Example URL |
119
+ | --- | --- |
120
+ | `src/app/docs/[...slug]/index.py` | `/docs/getting-started/setup` |
121
+
122
+ Use catch-all routes when the number of path segments is not fixed ahead of time.
123
+
124
+ ## Route Groups
125
+
126
+ Wrap a folder name in parentheses to organize code without adding that segment to the URL.
127
+
128
+ Examples:
129
+
130
+ | File | URL |
131
+ | --- | --- |
132
+ | `src/app/(auth)/login/index.py` | `/login` |
133
+ | `src/app/(auth)/register/index.py` | `/register` |
134
+
135
+ Route groups are useful when you want to:
136
+
137
+ - Keep related features together, such as auth or dashboard flows.
138
+ - Split routes under different layout boundaries.
139
+ - Improve project organization without changing the public URL structure.
140
+
141
+ ## Layouts And Nesting
142
+
143
+ Layouts work like the Next.js App Router layout system. A `layout.html` file wraps the routes beneath its folder, and nested layouts compose automatically.
144
+
145
+ Resolved SEO fields are exposed to layouts as `[[ metadata.* ]]`, while values returned from `layout.py` are exposed separately as `[[ layout.* ]]`.
146
+
147
+ For example, a page inside `/dashboard/settings` is wrapped by the root layout first and then by the dashboard layout.
148
+
149
+ Example root layout:
150
+
151
+ ```html
152
+ <!DOCTYPE html>
153
+ <html>
154
+ <head>
155
+ <title>[[ metadata.title ]]</title>
156
+ <meta name="description" content="[[ metadata.description ]]" />
157
+ </head>
158
+ <body>
159
+ <NavBar />
160
+ [[ children | safe ]]
161
+ </body>
162
+ </html>
163
+ ```
164
+
165
+ ### `layout.py`
166
+
167
+ If a layout needs async data, add a `layout.py` file next to the HTML layout.
168
+
169
+ Example:
170
+
171
+ ```python
172
+ from casp.auth import auth
173
+
174
+ async def layout(context_data):
175
+ user = auth.get_payload()
176
+
177
+ return {
178
+ "user": user,
179
+ "theme": "dark",
180
+ }
181
+ ```
182
+
183
+ `context_data` includes URL parameters such as dynamic route values.
184
+
185
+ 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.
186
+
187
+ ## Recommended Structure
188
+
189
+ This example shows a typical nested routing setup:
190
+
191
+ ```text
192
+ src/
193
+ app/
194
+ layout.html
195
+ index.html
196
+ about/
197
+ index.html
198
+ users/
199
+ [id]/
200
+ index.py
201
+ docs/
202
+ [...slug]/
203
+ index.py
204
+ (auth)/
205
+ login/
206
+ index.py
207
+ register/
208
+ index.py
209
+ dashboard/
210
+ layout.html
211
+ settings/
212
+ index.py
213
+ ```
214
+
215
+ ## AI Routing Notes
216
+
217
+ If an AI agent is choosing where to add or update route code, apply these rules first.
218
+
219
+ - Treat `src/app/` as the routing source of truth.
220
+ - Use folder names to model URL segments.
221
+ - Use `index.html` for route templates and `index.py` for route-level async logic.
222
+ - Use [cache.md](./cache.md) when an `index.py` route should opt into page-level HTML caching.
223
+ - Use `layout.html` for shared wrappers and `layout.py` for layout-level async data.
224
+ - Use [metadata.md](./metadata.md) when a route or layout needs SEO fields.
225
+ - Use `[segment]` for single dynamic parameters.
226
+ - Use `[...segment]` for catch-all route matching.
227
+ - Use `(group)` folders for organization when the folder should not appear in the URL.
228
+ - Use `.venv/Lib/site-packages/casp/layout.py` only when the task is about Caspian core routing, layout, or metadata internals.
229
+ - If you know the Next.js App Router, follow the same routing mental model but generate Caspian file names instead of React files.
230
+
231
+ Check [project-structure.md](./project-structure.md) when you need the full Caspian directory map.