@plitzi/sdk-server 0.38.9 → 0.38.10
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/CHANGELOG.md +84 -0
- package/dist/modules/actions/runtime/runAction.js +15 -1
- package/dist/modules/functions/manifest.js +6 -1
- package/dist/modules/project/serveProject.d.ts +5 -2
- package/dist/modules/project/serveProject.js +2 -2
- package/dist/modules/ssr/prepareRender.js +7 -5
- package/dist/modules/ssr/resolvePageSeo.d.ts +4 -1
- package/dist/modules/ssr/resolvePageSeo.js +7 -3
- package/package.json +3 -3
- package/skills/plitzi-authoring/CHEATSHEET.md +1 -1
- package/skills/plitzi-authoring/recipes/filter-a-list.ts +0 -2
- package/skills/plitzi-authoring/recipes/forms-and-modals.ts +2 -1
- package/skills/plitzi-authoring/reference/authoring-errors.md +4 -1
- package/skills/plitzi-authoring/reference/layouts.md +4 -2
- package/skills/plitzi-authoring/reference/structure.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.38.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 200da2f: - **`focus-on-field-box`** (`@plitzi/sdk-authoring`): a warning for a `focus` or `focus-visible` state on a text
|
|
8
|
+
field's or a select's `input` slot. That slot is the box the field is drawn in, a `<div>` that never takes focus, so
|
|
9
|
+
the rule was never seen and a keyboard user tabbing to the field found no ring. The fix it names: `'focus-within'` on
|
|
10
|
+
the same class, or the state on the `field` slot.
|
|
11
|
+
- **`modalContainer` says how it is laid out** (`@plitzi/sdk-elements`): its description now names its slots and how
|
|
12
|
+
they sit — the dim layer and the dialog side by side, not one inside the other; the dialog centred by `top`/`left`
|
|
13
|
+
50% and a `translate`. A space styled the dim layer as a flex container to place the dialog, which moved nothing.
|
|
14
|
+
- **A field shows where the keyboard is** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-elements`): a text field's or a select's
|
|
15
|
+
box is ringed (`2px solid currentColor`) while the field inside has keyboard focus, and a textarea while it has. No
|
|
16
|
+
text field or select showed any focus at all: the box had `outline: none`, and the field inherited it. One class of
|
|
17
|
+
specificity, so whatever a space writes about the outline wins; never in the builder's canvas, where the selection is
|
|
18
|
+
the ring. The field no longer inherits the box's outline — the box draws it.
|
|
19
|
+
- **The dim layer of a modal and a dialog is `rgb(0 0 0 / 50%)`** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-elements`), not
|
|
20
|
+
black at `opacity: 0.5`: the default looks the same, and a colour a space gives the layer is drawn as it is — it was
|
|
21
|
+
halved. A space that set its own colour sees it darker: the one it wrote.
|
|
22
|
+
- **`required-message-unused`** (`@plitzi/sdk-authoring`): a warning for a `requiredMessage` on a field nothing requires —
|
|
23
|
+
since 0.38.9 a field is optional unless `required: true`, so the message is never shown and an empty answer is sent.
|
|
24
|
+
The formControl's description, the cheat sheet and the forms recipe say the default.
|
|
25
|
+
- **A page's title can say its record** (`@plitzi/sdk-shared`, `@plitzi/sdk-server`, `@plitzi/sdk-elements`,
|
|
26
|
+
`@plitzi/sdk-authoring`): `seoTitle` and `seoDescription` may be templates over the page's `runtime: 'server'`
|
|
27
|
+
providers (its layouts' too) and `navigation` — `'{{ apiContainer_post.title }} — Blog'`. The server evaluates them
|
|
28
|
+
as it writes the head, from the answers it already has; the browser evaluates the same over `rsc.data`, so the tab
|
|
29
|
+
follows a navigation from record to record. One that reads anything else — a browser provider, `state` — is refused
|
|
30
|
+
(`page-template`): it was written into the head as it was, braces included. A declaration's `serverTemplates` names
|
|
31
|
+
the attributes the page server evaluates against a context of its own (a page's title and description, a provider's
|
|
32
|
+
`notFound`): the element runtime leaves them as written — interpolated where the page renders, a `|default(…)`
|
|
33
|
+
resolved to its fallback before the head could read the providers — and the authoring lint reads the same list.
|
|
34
|
+
- **A page says when its address shows nothing** (`@plitzi/sdk-server`, `@plitzi/sdk-authoring`): `notFound` on the
|
|
35
|
+
page, one expression over the same answers its title reads — `"{{ not (apiContainer_feed.topics|find('slug',
|
|
36
|
+
navigation.routeParams.slug)) }}"` — sends it with status 404, rendered as written. A provider's own `notFound` reads
|
|
37
|
+
only its answer, and one in a layout is every page's: a record read from the layout had no way to say it was not
|
|
38
|
+
there, and the page answered 200.
|
|
39
|
+
- **The Export keeps the flows of a plugin hosted by `custom`** (`@plitzi/sdk-authoring`): `specFromSpace` judged such
|
|
40
|
+
an element by `custom`'s own triggers and callbacks, and dropped a flow on the component's event (`onChange` on an
|
|
41
|
+
editor) as one "a custom never fires" — the space read back was not the space written.
|
|
42
|
+
- **`page check` reads every word on the page, in the colour the browser paints it** (`@plitzi/sdk-authoring`): what a
|
|
43
|
+
plugin draws inside an element of the space is measured too, named by the element and a selector; the measure is
|
|
44
|
+
WCAG's contrast (under 2:1 is unreadable), not "nearly the same colour"; and a colour the browser answers as
|
|
45
|
+
`oklch()`, `oklab()` or `color(srgb …)` — every token and every `color-mix()` — is read as that, where it was read as
|
|
46
|
+
`rgb()` numbers and came out near black. A plugin's dark-on-dark status line passed as "nothing wrong".
|
|
47
|
+
- **`verify` checks every theme the space can be painted in** (`@plitzi/cli`): light and dark, unless the space says
|
|
48
|
+
light only — "48 checks at 1440 and 390 px, light and dark". A page unreadable in the dark theme passed in the light
|
|
49
|
+
one; a failure names the theme it was found in.
|
|
50
|
+
- **`plitzi upgrade` says where fields became optional** (`@plitzi/cli`): for a project last upgraded before 0.38.9,
|
|
51
|
+
each `formControl(…)` with no `required`, at its file and line — said, never written. And a `.gitkeep` is asked for
|
|
52
|
+
only in a folder with nothing else in it: `upgrade` wrote one beside code, and `doctor` called it missing once deleted
|
|
53
|
+
as AGENTS.md asks ("nothing unused") — the three now say the same.
|
|
54
|
+
- **`devMode` and `devReload` are the project's** (`@plitzi/sdk-server`): `src/config/serverOptions.ts` may set them,
|
|
55
|
+
over `NODE_ENV`, which they still follow when left out. A deployment started without `NODE_ENV` (a container's `CMD`)
|
|
56
|
+
says `devMode: false`, and a public action answers with its output alone.
|
|
57
|
+
- **`page shot --from load`** (`@plitzi/cli`): `--frames` start as soon as the page's HTML is in, not once it settles —
|
|
58
|
+
an entrance that plays while the page loads had ended before the first picture.
|
|
59
|
+
- **`heavy-animation` says where each animation is** (`@plitzi/sdk-authoring`): the class and its variant, state or
|
|
60
|
+
slot — or the `customCss` rule — and writes the fix against that selector. It said "in `customCss`" for an animation
|
|
61
|
+
declared on a class's variant, and offered `.glow`, a class no space has.
|
|
62
|
+
- **`page shot --steps`** (`@plitzi/cli`): an interaction played step by step — `click`, `type`, `press`, `wait`,
|
|
63
|
+
`wait-for`, `shot [label]`, `frames <n> [ms]`, separated by `;` or written in a file (`@file`) — each picture labelled
|
|
64
|
+
with its step and the time since the first, and said when nothing changed since the one before. A queue filling, a
|
|
65
|
+
restart, an opening frame by frame took a Playwright script of the project's own. Targets take Playwright's selectors
|
|
66
|
+
too (`text=Saved`).
|
|
67
|
+
- **A space's task may take a `number` param** (`@plitzi/sdk-server`): saved functions refused it ("a type the builder
|
|
68
|
+
cannot draw") while a project's server ran it, so code that worked self-hosted failed on the platform — and the
|
|
69
|
+
builder draws one, a callback's. A step's params now arrive as the types the task declares, on every server: a
|
|
70
|
+
`number` written as text is the number, a `boolean` written `'true'` is `true`, as the browser reads a callback's.
|
|
71
|
+
- **`page shot --as <username>`** (`@plitzi/cli`): signs in first, as `page check --as` does, the password from
|
|
72
|
+
`PLITZI_CHECK_PASSWORD` in `.env`. A page for signed-in visitors was pictured as the sign-in page it sent the browser
|
|
73
|
+
to, without a word; `page shot` now says when the page sent it elsewhere, and that the picture is of that page.
|
|
74
|
+
- **A capture is of the page at rest** (`@plitzi/sdk-mcp`): the local screenshot client takes every picture with
|
|
75
|
+
`prefers-reduced-motion: reduce`, as the browser pod now does — the declared motion and a space's own keyframes that
|
|
76
|
+
honour the query are shown finished. A splash still covering its page, or an arrival caught halfway, was what a
|
|
77
|
+
thumbnail or a `plitzi_look` showed of a page that animates as it loads.
|
|
78
|
+
- **The server renders in the visitor's theme, not only the class** (`@plitzi/sdk-shared`): `ThemeProvider` set the
|
|
79
|
+
starting theme from a layout effect, which never runs on a server, so a page was drawn from the theme store's own
|
|
80
|
+
start — `{{ theme.resolved }}` was `light` under a `<html class="dark">`, and whatever named the scheme (a picture's
|
|
81
|
+
address, a `when` rule) was swapped for the other one as the page hydrated. The theme a host supplies (the cookie) is
|
|
82
|
+
now the server's snapshot of the store, and the hydration's; the shared store is never written on the server.
|
|
83
|
+
- Updated dependencies [200da2f]
|
|
84
|
+
- @plitzi/plitzi-sdk@0.38.10
|
|
85
|
+
- @plitzi/sdk-shared@0.38.10
|
|
86
|
+
|
|
3
87
|
## 0.38.9
|
|
4
88
|
|
|
5
89
|
### Patch Changes
|
|
@@ -148,8 +148,22 @@ var stepOf = (node, { status, phase, startTime, endTime, error, logs = [] }) =>
|
|
|
148
148
|
/** What one step may log: enough to debug with, bounded so a loop that logs cannot fill a run's record. */
|
|
149
149
|
var MAX_LOG_LINES = 100;
|
|
150
150
|
var MAX_LOG_LINE = 1e3;
|
|
151
|
+
/**
|
|
152
|
+
* A value written as text, read as the type its task declares: a `number` param written `5` — or bound to text that says
|
|
153
|
+
* it — arrives as 5, a `boolean` one written `'true'` as `true`, an empty number as nothing (so the default applies). The
|
|
154
|
+
* browser reads a callback's params the same way (`coerceDeclaredParams`): a task's author never writes `Number(…)`.
|
|
155
|
+
*/
|
|
156
|
+
var asDeclared = (type, value) => {
|
|
157
|
+
if (typeof value !== "string") return value;
|
|
158
|
+
if (type === "number") {
|
|
159
|
+
const parsed = Number(value.trim());
|
|
160
|
+
return value.trim() === "" ? void 0 : Number.isFinite(parsed) ? parsed : value;
|
|
161
|
+
}
|
|
162
|
+
return type === "boolean" && (value === "true" || value === "false") ? value === "true" : value;
|
|
163
|
+
};
|
|
151
164
|
var withDefaults = (task, params) => Object.entries(task.params).reduce((acum, [key, param]) => {
|
|
152
|
-
|
|
165
|
+
const value = asDeclared(typeof param.type === "function" ? param.type(acum) : param.type, acum[key]);
|
|
166
|
+
acum[key] = value === void 0 || value === "" ? param.defaultValue ?? "" : value;
|
|
153
167
|
return acum;
|
|
154
168
|
}, { ...params });
|
|
155
169
|
var runNode = async (node, scope, registry, buildContext) => {
|
|
@@ -57,6 +57,11 @@ var paramOf = (value) => {
|
|
|
57
57
|
type: value.type,
|
|
58
58
|
...typeof defaultValue === "string" || typeof defaultValue === "number" ? { defaultValue } : {}
|
|
59
59
|
};
|
|
60
|
+
case "number": return {
|
|
61
|
+
...base,
|
|
62
|
+
type: "number",
|
|
63
|
+
...typeof defaultValue === "number" ? { defaultValue } : {}
|
|
64
|
+
};
|
|
60
65
|
case "codemirror-text":
|
|
61
66
|
case "codemirror-json": return {
|
|
62
67
|
...base,
|
|
@@ -84,7 +89,7 @@ var paramOf = (value) => {
|
|
|
84
89
|
...typeof defaultValue === "string" ? { defaultValue } : {}
|
|
85
90
|
};
|
|
86
91
|
}
|
|
87
|
-
default: return `has a type the builder cannot draw ("${String(value.type)}"): text, textarea, codemirror-text, codemirror-json, boolean, elements or select`;
|
|
92
|
+
default: return `has a type the builder cannot draw ("${String(value.type)}"): text, number, textarea, codemirror-text, codemirror-json, boolean, elements or select`;
|
|
88
93
|
}
|
|
89
94
|
};
|
|
90
95
|
/** What each asked limit is, in words a problem is written in. */
|
|
@@ -3,11 +3,14 @@ import { ServerConfig } from '../../core/createServer';
|
|
|
3
3
|
import { ConnectorManifest } from '../connectors';
|
|
4
4
|
import { ActionEntry, SSRServer } from '@plitzi/sdk-shared';
|
|
5
5
|
/** What `serveProject` sets itself, from the project's layout — so never the project's options'. */
|
|
6
|
-
type SetByProject = 'port' | '
|
|
6
|
+
type SetByProject = 'port' | 'health' | 'adapters' | 'plugins' | 'publicDir' | 'dataDir' | 'functions' | 'action';
|
|
7
7
|
/**
|
|
8
8
|
* What a project's server does besides serving its space — its `src/config/serverOptions.ts`: what `createServer`
|
|
9
9
|
* takes, but for what `serveProject` wires from the project's layout, and an action's `lookups`, which are the
|
|
10
|
-
* project's `src/actions/`. `logLevel` and `logger` are the project's to change; `action.kv` too.
|
|
10
|
+
* project's `src/actions/`. `logLevel` and `logger` are the project's to change; `action.kv` too. So are `devMode` and
|
|
11
|
+
* `devReload`, which follow `NODE_ENV` when left out — on unless it is `production`. A deployment started without it (a
|
|
12
|
+
* container's `CMD`, a process manager with no environment) says `devMode: false` here, and a public action answers
|
|
13
|
+
* with its output alone, never the trace of every step.
|
|
11
14
|
*/
|
|
12
15
|
export type ProjectServerOptions = Partial<Omit<ServerConfig, SetByProject>> & {
|
|
13
16
|
action?: Omit<NonNullable<ServerConfig['action']>, 'lookups'>;
|
|
@@ -142,8 +142,8 @@ var startProject = async (options) => {
|
|
|
142
142
|
logger: consoleLogger,
|
|
143
143
|
...serverOptions,
|
|
144
144
|
port,
|
|
145
|
-
devMode: developing,
|
|
146
|
-
devReload: developing,
|
|
145
|
+
devMode: serverOptions.devMode ?? developing,
|
|
146
|
+
devReload: serverOptions.devReload ?? serverOptions.devMode ?? developing,
|
|
147
147
|
health: { name },
|
|
148
148
|
adapters,
|
|
149
149
|
plugins: plugins.sources,
|
|
@@ -20,7 +20,7 @@ import { sdkAssetVersion } from "../../core/sdkAssets.js";
|
|
|
20
20
|
import { buildServerInfo } from "../../helpers/buildServerInfo.js";
|
|
21
21
|
import { resolveDebugMode } from "../../helpers/resolveDebugMode.js";
|
|
22
22
|
import { flagUserFromSSR, flagValues, resolveFlags } from "@plitzi/sdk-shared/flags";
|
|
23
|
-
import { collectServerElements, notFoundProvider } from "@plitzi/sdk-shared/schema/serverElements";
|
|
23
|
+
import { collectServerElements, notFoundProvider, pageNotFound, pageServerContext } from "@plitzi/sdk-shared/schema/serverElements";
|
|
24
24
|
import { debugCookieName } from "@plitzi/sdk-shared/devTools";
|
|
25
25
|
import { themeFromCookies } from "@plitzi/sdk-shared/theme";
|
|
26
26
|
import { paintedKeys, paintedStateFor } from "@plitzi/sdk-shared/state/paintedState";
|
|
@@ -84,11 +84,13 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
84
84
|
loadOfflineData,
|
|
85
85
|
flagOverrides
|
|
86
86
|
})) : rscPath ? { serverData: {} } : void 0;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
routeParams: pageMatch.routeParams,
|
|
87
|
+
const navigation = {
|
|
88
|
+
routeParams: pageMatch?.routeParams ?? {},
|
|
90
89
|
queryParams: req.query
|
|
91
|
-
}
|
|
90
|
+
};
|
|
91
|
+
const pageContext = pageServerContext(serverElements, rscData?.serverData ?? {}, navigation);
|
|
92
|
+
const pageSeo = resolvePageSeo(schema, pageMatch?.pageId, pageContext);
|
|
93
|
+
const notFound = pageMatch !== void 0 && rscData?.serverData !== void 0 ? notFoundProvider(serverElements, rscData.serverData, navigation) !== void 0 || pageNotFound(schema?.flat[pageMatch.pageId]?.attributes.notFound, pageContext) : false;
|
|
92
94
|
const server = buildServerInfo(req, config, {
|
|
93
95
|
rscPath,
|
|
94
96
|
rscData,
|
|
@@ -14,5 +14,8 @@ export type PageSeo = {
|
|
|
14
14
|
* `seoEnabled: false` returns nothing rather than a default, so the deployment's own title stays in charge of a
|
|
15
15
|
* page that opted out. Blank strings are treated as absent for the same reason — the builder writes one for a
|
|
16
16
|
* field the author cleared, and an empty <title> is worse than a generic one.
|
|
17
|
+
*
|
|
18
|
+
* Either may be a template over the page's server providers and the address (`pageServerContext`): a detail page says
|
|
19
|
+
* the title of the record it shows, in the HTML a crawler reads. One that does not evaluate is nothing, as a blank is.
|
|
17
20
|
*/
|
|
18
|
-
export declare const resolvePageSeo: (schema: Schema | undefined, pageId: string | undefined) => PageSeo;
|
|
21
|
+
export declare const resolvePageSeo: (schema: Schema | undefined, pageId: string | undefined, context?: Record<string, unknown>) => PageSeo;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pageSeoText } from "@plitzi/sdk-shared/schema/serverElements";
|
|
1
2
|
//#region src/modules/ssr/resolvePageSeo.ts
|
|
2
3
|
/**
|
|
3
4
|
* The title and description the addressed page declares for itself.
|
|
@@ -10,13 +11,16 @@
|
|
|
10
11
|
* `seoEnabled: false` returns nothing rather than a default, so the deployment's own title stays in charge of a
|
|
11
12
|
* page that opted out. Blank strings are treated as absent for the same reason — the builder writes one for a
|
|
12
13
|
* field the author cleared, and an empty <title> is worse than a generic one.
|
|
14
|
+
*
|
|
15
|
+
* Either may be a template over the page's server providers and the address (`pageServerContext`): a detail page says
|
|
16
|
+
* the title of the record it shows, in the HTML a crawler reads. One that does not evaluate is nothing, as a blank is.
|
|
13
17
|
*/
|
|
14
|
-
var resolvePageSeo = (schema, pageId) => {
|
|
18
|
+
var resolvePageSeo = (schema, pageId, context = {}) => {
|
|
15
19
|
if (!schema || !pageId) return {};
|
|
16
20
|
const page = schema.flat[pageId];
|
|
17
21
|
if (!page?.attributes.seoEnabled) return {};
|
|
18
|
-
const title = page.attributes.seoPageTitle
|
|
19
|
-
const description = page.attributes.seoPageDescription
|
|
22
|
+
const title = pageSeoText(page.attributes.seoPageTitle, context);
|
|
23
|
+
const description = pageSeoText(page.attributes.seoPageDescription, context);
|
|
20
24
|
return {
|
|
21
25
|
...title ? { title } : {},
|
|
22
26
|
...description ? { description } : {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-server",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.10",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -96,8 +96,8 @@
|
|
|
96
96
|
"./dist/env.js"
|
|
97
97
|
],
|
|
98
98
|
"dependencies": {
|
|
99
|
-
"@plitzi/plitzi-sdk": "0.38.
|
|
100
|
-
"@plitzi/sdk-shared": "0.38.
|
|
99
|
+
"@plitzi/plitzi-sdk": "0.38.10",
|
|
100
|
+
"@plitzi/sdk-shared": "0.38.10",
|
|
101
101
|
"ejs": "^6.0.1",
|
|
102
102
|
"esbuild": "^0.28.2",
|
|
103
103
|
"jsonwebtoken": "^9.0.3",
|
|
@@ -50,7 +50,7 @@ apiContainer({ id: 'p', query: '/data/x.json', runtime: 'server', children }) //
|
|
|
50
50
|
apiContainer({ id: 'p', action: 'report', runtime: 'server', children }) // the action's output at p's root: p.rows
|
|
51
51
|
// loadingSlot: 'p-skeleton' — a child shown until it answers
|
|
52
52
|
form({ id, managedByInteractions: true, flows, children })
|
|
53
|
-
formControl({ name: 'email', label: 'Email', subType: 'email' })
|
|
53
|
+
formControl({ name: 'email', label: 'Email', subType: 'email', required: true }) // optional without required; select: options: [{ label, value }]
|
|
54
54
|
modalContainer({ id, visible: false, title, children })
|
|
55
55
|
component('card', { props: { title }, children: { body: [ … ] } })
|
|
56
56
|
custom({ renderType: 'myPlugin' }); defineElement<Props>(declaration) // a plugin
|
|
@@ -31,8 +31,6 @@ export const recipe: SpaceSpec = {
|
|
|
31
31
|
{ label: 'Graphics cards', value: 'gpu' }
|
|
32
32
|
],
|
|
33
33
|
defaultValue: 'all',
|
|
34
|
-
// A filter always has an answer: the select is not a question that must be filled in.
|
|
35
|
-
required: false,
|
|
36
34
|
flows: [
|
|
37
35
|
[named('picked', on('onChange')), setState({ key: 'category', type: 'text', value: '{{ picked.value }}' })]
|
|
38
36
|
]
|
|
@@ -38,7 +38,8 @@ export const recipe: SpaceSpec = {
|
|
|
38
38
|
[named('sent', onSubmit()), setState({ key: 'email', type: 'text', value: '{{ sent.values.email }}' })]
|
|
39
39
|
],
|
|
40
40
|
children: [
|
|
41
|
-
|
|
41
|
+
// Optional unless it says so, as an HTML field is: a sign-up without an address is no sign-up.
|
|
42
|
+
formControl({ name: 'email', label: 'Email', subType: 'email', required: true }),
|
|
42
43
|
button({ content: 'Sign up', subType: 'submit' })
|
|
43
44
|
]
|
|
44
45
|
}),
|
|
@@ -102,7 +102,7 @@ The space is not written until these are fixed.
|
|
|
102
102
|
| `motion-no-tag` | a `motion` on an element with no tag of its own — nothing to move | the motion on what it wraps, or a `subType` on it |
|
|
103
103
|
| `no-pages` | a space with no pages | `pages: [{ id: 'home', name: 'Home', slug: '', body: [] }]` |
|
|
104
104
|
| `not-found-in-browser` | `notFound` on a provider asked from the browser — its answer arrives after the page was sent with its status | `runtime: 'server'` on the provider, or `visible` on the page's "not found" part instead |
|
|
105
|
-
| `not-found-not-a-template` | `notFound
|
|
105
|
+
| `not-found-not-a-template` | `notFound`, a provider's or a page's, that is not one `{{ expression }}` — never `true`, so never a 404 | `notFound: '{{ source.found == false }}'` |
|
|
106
106
|
| `notifications-shape` | `notifications` with a field it does not have, or a value that is not one CSS value | the field it suggests, or one it lists; one CSS value, like 'var(--card)', '12px' or '500' |
|
|
107
107
|
| `outside-ancestor` | an element that reads the state of an element it is not inside | nest it in the element the message names |
|
|
108
108
|
| `page-access-level` | an `accessLevel` that is not one the router reads | one of the values the message lists |
|
|
@@ -110,6 +110,7 @@ The space is not written until these are fixed.
|
|
|
110
110
|
| `page-route-taken` | two pages at one address for the same visitors | another slug — or `accessLevel` `'public'` on one and `'authenticated'` on the other |
|
|
111
111
|
| `page-target-unknown` | a link or `navigate` to a page id that does not exist | an existing page id, or a path with its slash (`'/about'`) |
|
|
112
112
|
| `page-target-url` | a URL, `mailto:` or `tel:` in a link left in page mode | `mode: 'external'` |
|
|
113
|
+
| `page-template` | a template in a page's `seoTitle`, `seoDescription` or `notFound` that reads what is not there when the server answers — anything but the page's `runtime: 'server'` providers and `navigation` — or cannot be read | read the record from a provider with `runtime: 'server'` on the page or its layout — `'{{ apiContainer_post.title }} — Blog'` — or words of its own |
|
|
113
114
|
| `page-without-slug` | a page with no `slug` | `slug: ''` for the home page, its path for any other: `'about'`, `'blog/:slug'` |
|
|
114
115
|
| `part-missing` | a compound element — a carousel, a tab container, a dropdown — without a part it renders through | the part the message names, inside it (`carousel()` writes its own track) |
|
|
115
116
|
| `prop-missing` | an instance without a prop its component requires | `component('card', { props: { name: … } })`, or bind it |
|
|
@@ -185,6 +186,7 @@ The space renders, and renders something you probably did not mean. Fix every on
|
|
|
185
186
|
| `embed-without-title` | an `embed` with no `title` — a frame a screen reader cannot describe | say what it shows in `title` |
|
|
186
187
|
| `flag-rule-empty` | a flag rule with no conditions — skipped, never read as "always" | give it a condition, or set the flag's `value` instead |
|
|
187
188
|
| `flag-unused` | a declared flag nothing gates on and no template reads | gate what it switches (`flag: 'x'`), or remove it once the feature has shipped |
|
|
189
|
+
| `focus-on-field-box` | a `focus` or `focus-visible` state on a text field's or a select's `input` slot — the box the field is drawn in, which never takes focus, so no ring is ever shown | `'focus-within'` on the same class, or the state on the `field` slot |
|
|
188
190
|
| `FORM_SUBMIT_UNMANAGED` | a form the browser would submit itself, reloading the page | `managedByInteractions: true` |
|
|
189
191
|
| `form-control-name-taken` | two controls in one form with one name — one overwrites the other | a name each |
|
|
190
192
|
| `form-control-unnamed` | a control in a form with no `name` — its value never reaches `values` | `formControl({ name: 'email', … })` |
|
|
@@ -202,6 +204,7 @@ The space renders, and renders something you probably did not mean. Fix every on
|
|
|
202
204
|
| `path-not-in-data` | a binding onto a provider whose answer the author could read (`data`), through a path that answer does not have | the path the message lists the keys for — `p.data.plans`, not `p.data.landing.plans` |
|
|
203
205
|
| `plugin-attribute-reserved` | a plugin attribute named as one of the element’s own fields — a factory never hands it to the plugin | rename it in the plugin (`variant` → `kind`) |
|
|
204
206
|
| `provider-without-source` | an `apiContainer` that asks nothing | a `query` (or `action`, `connector`, `resource`) |
|
|
207
|
+
| `required-message-unused` | a `requiredMessage` on a field nothing requires — fields are optional unless `required: true`, so it is never shown and an empty answer is sent | `required: true` on the field, or no message |
|
|
205
208
|
| `route-param-undeclared` | `navigation.routeParams.x` read on a page whose slug has no `:x` — always empty | add `:x` to the slug, or read `navigation.queryParams.x` |
|
|
206
209
|
| `server-data-without-rsc` | a `runtime: 'server'` provider — `connector`, `action` or `query` — in a space that turns server data off (`rsc: { enabled: false }`) | drop `rsc: { enabled: false }`: server data is on unless a space turns it off |
|
|
207
210
|
| `span-holds-block` | a `container` with `subType: 'span'` or `'p'` holding a heading, a paragraph, a list, a form or prose | a `div` (leave `subType` out), or words and inline elements: a `text`, a `link` |
|
|
@@ -139,8 +139,10 @@ Every page is now its content and nothing else, and every page is guaranteed the
|
|
|
139
139
|
suggests writing your own (`not-found-page`): say it in the space's words.
|
|
140
140
|
- **A page that exists but finds nothing** — `/products/:slug` for a slug no product has — is sent with 404 too when
|
|
141
141
|
its server provider says so: `notFound: "{{ not (source.data.products|find('slug', navigation.routeParams.slug)) }}"`,
|
|
142
|
-
one expression against its answer, on a `runtime: 'server'` provider of the page
|
|
143
|
-
page shares
|
|
142
|
+
one expression against its answer, on a `runtime: 'server'` provider of the page. When the record comes from a
|
|
143
|
+
provider of its layout, which every page shares, the page says it: `notFound` on the page reads its server providers
|
|
144
|
+
by name — `"{{ not (apiContainer_feed.products|find('slug', navigation.routeParams.slug)) }}"`. The page renders as
|
|
145
|
+
written: show its "not found" part with `visible`.
|
|
144
146
|
|
|
145
147
|
## Styles follow the same rule
|
|
146
148
|
|
|
@@ -93,7 +93,9 @@ pages: [
|
|
|
93
93
|
|
|
94
94
|
Adding a page is adding an entry; two entries of one id or slug are refused as any two pages are. For one shape over
|
|
95
95
|
records a visitor browses — a product, a post — a single page with a route param (`slug: 'posts/:postId'`) reading
|
|
96
|
-
the record is the answer instead: one page, as many addresses as records.
|
|
96
|
+
the record is the answer instead: one page, as many addresses as records. Its `seoTitle` can say the record —
|
|
97
|
+
`'{{ apiContainer_post.title }} — Blog'` — read from a provider on the page with `runtime: 'server'`, or from
|
|
98
|
+
`navigation`: the server writes the head with it, so a link preview shows it. Anything else is refused (`page-template`).
|
|
97
99
|
|
|
98
100
|
## Names
|
|
99
101
|
|