@aglyn/aglyn 1.0.0-beta.252 → 1.0.0-beta.253
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/package.json +11 -11
- package/src/lib/app-utils/email-block-starters.d.ts +39 -0
- package/src/lib/app-utils/email-block-starters.js +122 -0
- package/src/lib/app-utils/email-block-starters.js.map +1 -0
- package/src/lib/app-utils/screen-route.d.ts +1 -130
- package/src/lib/app-utils/screen-route.js +2 -151
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/screen-slug.d.ts +146 -0
- package/src/lib/app-utils/screen-slug.js +173 -0
- package/src/lib/app-utils/screen-slug.js.map +1 -0
- package/src/lib/app-utils/site-theme.d.ts +11 -0
- package/src/lib/app-utils/site-theme.js +32 -1
- package/src/lib/app-utils/site-theme.js.map +1 -1
- package/src/lib/app-utils/template-slug.d.ts +101 -0
- package/src/lib/app-utils/template-slug.js +150 -0
- package/src/lib/app-utils/template-slug.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.253",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
45
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
46
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
47
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
48
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
49
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
40
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.253",
|
|
41
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.253",
|
|
42
|
+
"@aglyn/shared-data-types": "1.0.0-beta.253",
|
|
43
|
+
"@aglyn/shared-util-email": "1.0.0-beta.253",
|
|
44
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.253",
|
|
45
|
+
"@aglyn/shared-util-http": "1.0.0-beta.253",
|
|
46
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.253",
|
|
47
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.253",
|
|
48
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.253",
|
|
49
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.253",
|
|
50
50
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
51
51
|
"@msgpack/msgpack": "^3.1.3",
|
|
52
52
|
"@types/unist": "^3.0.3",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** The email plugin's bundle id: the `pluginId` of every element in its bands. */
|
|
18
|
+
export declare const EMAIL_BLOCK_STARTER_PLUGIN_ID = "email";
|
|
19
|
+
/** One element of a nested starter tree, as an element preset's `data` holds it. */
|
|
20
|
+
export interface EmailBlockStarterNode {
|
|
21
|
+
$id: null;
|
|
22
|
+
componentId: string;
|
|
23
|
+
pluginId: string;
|
|
24
|
+
props: Record<string, unknown>;
|
|
25
|
+
nodes?: EmailBlockStarterNode[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The Footer's small print, a muted gray that still reads at 5:1 on white.
|
|
29
|
+
* A literal on purpose: it is written into the sent mail's own HTML, where no
|
|
30
|
+
* theme token resolves.
|
|
31
|
+
*/
|
|
32
|
+
export declare const EMAIL_FOOTER_TEXT_COLOR = "#6b6b6b";
|
|
33
|
+
export declare const EMAIL_BLOCK_STARTER_TREES: Readonly<Record<'header' | 'footer', EmailBlockStarterNode>>;
|
|
34
|
+
/**
|
|
35
|
+
* {@link EMAIL_BLOCK_STARTER_TREES} as JSON text: the form the native apps
|
|
36
|
+
* read them in (`Contracts.emailBlockStarterTreesJson`), parsed when a block
|
|
37
|
+
* is started.
|
|
38
|
+
*/
|
|
39
|
+
export declare const EMAIL_BLOCK_STARTER_TREES_JSON: string;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /*
|
|
17
|
+
* The two email bands a Header or Footer email block starts from (AGL-3287),
|
|
18
|
+
* as plain data: the email plugin registers them as element presets (so an
|
|
19
|
+
* author can drop one into an email), the Components page starts a reusable
|
|
20
|
+
* block from the same trees, and the native apps start theirs from the values
|
|
21
|
+
* generated from here. One tree each, wherever it is started from.
|
|
22
|
+
*
|
|
23
|
+
* The words are placeholders to replace. No merge token names the company in
|
|
24
|
+
* both kinds of email — a campaign supplies `{{org.name}}`, and no token is
|
|
25
|
+
* supplied by every one of a site's own emails — while a token an email does
|
|
26
|
+
* not supply is blanked before sending, which would leave the header with no
|
|
27
|
+
* name at all.
|
|
28
|
+
*
|
|
29
|
+
* The footer carries no unsubscribe link: a campaign send appends its own,
|
|
30
|
+
* and a second one would do the same thing twice.
|
|
31
|
+
*/ /** The email plugin's bundle id: the `pluginId` of every element in its bands. */ export const EMAIL_BLOCK_STARTER_PLUGIN_ID = 'email';
|
|
32
|
+
/**
|
|
33
|
+
* The Footer's small print, a muted gray that still reads at 5:1 on white.
|
|
34
|
+
* A literal on purpose: it is written into the sent mail's own HTML, where no
|
|
35
|
+
* theme token resolves.
|
|
36
|
+
*/ export const EMAIL_FOOTER_TEXT_COLOR = '#6b6b6b';
|
|
37
|
+
const FOOTER_TEXT_COLOR = EMAIL_FOOTER_TEXT_COLOR;
|
|
38
|
+
export const EMAIL_BLOCK_STARTER_TREES = {
|
|
39
|
+
header: {
|
|
40
|
+
$id: null,
|
|
41
|
+
componentId: 'emailSection',
|
|
42
|
+
pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,
|
|
43
|
+
// Marks the band as the email's header, so the sender's own header is
|
|
44
|
+
// not drawn above it (AGL-3372).
|
|
45
|
+
props: {
|
|
46
|
+
align: 'center',
|
|
47
|
+
padding: 24,
|
|
48
|
+
emailRole: 'header'
|
|
49
|
+
},
|
|
50
|
+
nodes: [
|
|
51
|
+
{
|
|
52
|
+
$id: null,
|
|
53
|
+
componentId: 'emailImage',
|
|
54
|
+
pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,
|
|
55
|
+
props: {
|
|
56
|
+
alt: 'Your company logo',
|
|
57
|
+
width: 160,
|
|
58
|
+
align: 'center'
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
$id: null,
|
|
63
|
+
componentId: 'emailText',
|
|
64
|
+
pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,
|
|
65
|
+
props: {
|
|
66
|
+
children: 'Your company',
|
|
67
|
+
variant: 'subheading',
|
|
68
|
+
align: 'center'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
footer: {
|
|
74
|
+
$id: null,
|
|
75
|
+
componentId: 'emailSection',
|
|
76
|
+
pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,
|
|
77
|
+
// Marks the band as the email's footer, so the sender's own footer is
|
|
78
|
+
// not drawn under it (AGL-3372).
|
|
79
|
+
props: {
|
|
80
|
+
align: 'center',
|
|
81
|
+
padding: 24,
|
|
82
|
+
emailRole: 'footer'
|
|
83
|
+
},
|
|
84
|
+
nodes: [
|
|
85
|
+
{
|
|
86
|
+
$id: null,
|
|
87
|
+
componentId: 'emailDivider',
|
|
88
|
+
pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,
|
|
89
|
+
props: {}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
$id: null,
|
|
93
|
+
componentId: 'emailText',
|
|
94
|
+
pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,
|
|
95
|
+
props: {
|
|
96
|
+
children: 'Your company · 123 Main St, City',
|
|
97
|
+
variant: 'caption',
|
|
98
|
+
align: 'center',
|
|
99
|
+
color: FOOTER_TEXT_COLOR
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
$id: null,
|
|
104
|
+
componentId: 'emailText',
|
|
105
|
+
pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,
|
|
106
|
+
props: {
|
|
107
|
+
children: "You're receiving this because you're a customer of Your company.",
|
|
108
|
+
variant: 'caption',
|
|
109
|
+
align: 'center',
|
|
110
|
+
color: FOOTER_TEXT_COLOR
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* {@link EMAIL_BLOCK_STARTER_TREES} as JSON text: the form the native apps
|
|
118
|
+
* read them in (`Contracts.emailBlockStarterTreesJson`), parsed when a block
|
|
119
|
+
* is started.
|
|
120
|
+
*/ export const EMAIL_BLOCK_STARTER_TREES_JSON = JSON.stringify(EMAIL_BLOCK_STARTER_TREES);
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=email-block-starters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/email-block-starters.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * The two email bands a Header or Footer email block starts from (AGL-3287),\n * as plain data: the email plugin registers them as element presets (so an\n * author can drop one into an email), the Components page starts a reusable\n * block from the same trees, and the native apps start theirs from the values\n * generated from here. One tree each, wherever it is started from.\n *\n * The words are placeholders to replace. No merge token names the company in\n * both kinds of email — a campaign supplies `{{org.name}}`, and no token is\n * supplied by every one of a site's own emails — while a token an email does\n * not supply is blanked before sending, which would leave the header with no\n * name at all.\n *\n * The footer carries no unsubscribe link: a campaign send appends its own,\n * and a second one would do the same thing twice.\n */\n\n/** The email plugin's bundle id: the `pluginId` of every element in its bands. */\nexport const EMAIL_BLOCK_STARTER_PLUGIN_ID = 'email'\n\n/** One element of a nested starter tree, as an element preset's `data` holds it. */\nexport interface EmailBlockStarterNode {\n $id: null\n componentId: string\n pluginId: string\n props: Record<string, unknown>\n nodes?: EmailBlockStarterNode[]\n}\n\n/**\n * The Footer's small print, a muted gray that still reads at 5:1 on white.\n * A literal on purpose: it is written into the sent mail's own HTML, where no\n * theme token resolves.\n */\nexport const EMAIL_FOOTER_TEXT_COLOR = '#6b6b6b'\nconst FOOTER_TEXT_COLOR = EMAIL_FOOTER_TEXT_COLOR\n\nexport const EMAIL_BLOCK_STARTER_TREES: Readonly<\n Record<'header' | 'footer', EmailBlockStarterNode>\n> = {\n header: {\n $id: null,\n componentId: 'emailSection',\n pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,\n // Marks the band as the email's header, so the sender's own header is\n // not drawn above it (AGL-3372).\n props: { align: 'center', padding: 24, emailRole: 'header' },\n nodes: [\n {\n $id: null,\n componentId: 'emailImage',\n pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,\n props: { alt: 'Your company logo', width: 160, align: 'center' },\n },\n {\n $id: null,\n componentId: 'emailText',\n pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,\n props: {\n children: 'Your company',\n variant: 'subheading',\n align: 'center',\n },\n },\n ],\n },\n footer: {\n $id: null,\n componentId: 'emailSection',\n pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,\n // Marks the band as the email's footer, so the sender's own footer is\n // not drawn under it (AGL-3372).\n props: { align: 'center', padding: 24, emailRole: 'footer' },\n nodes: [\n {\n $id: null,\n componentId: 'emailDivider',\n pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,\n props: {},\n },\n {\n $id: null,\n componentId: 'emailText',\n pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,\n props: {\n children: 'Your company · 123 Main St, City',\n variant: 'caption',\n align: 'center',\n color: FOOTER_TEXT_COLOR,\n },\n },\n {\n $id: null,\n componentId: 'emailText',\n pluginId: EMAIL_BLOCK_STARTER_PLUGIN_ID,\n props: {\n children:\n \"You're receiving this because you're a customer of Your company.\",\n variant: 'caption',\n align: 'center',\n color: FOOTER_TEXT_COLOR,\n },\n },\n ],\n },\n}\n\n/**\n * {@link EMAIL_BLOCK_STARTER_TREES} as JSON text: the form the native apps\n * read them in (`Contracts.emailBlockStarterTreesJson`), parsed when a block\n * is started.\n */\nexport const EMAIL_BLOCK_STARTER_TREES_JSON: string = JSON.stringify(\n EMAIL_BLOCK_STARTER_TREES,\n)\n"],"names":["EMAIL_BLOCK_STARTER_PLUGIN_ID","EMAIL_FOOTER_TEXT_COLOR","FOOTER_TEXT_COLOR","EMAIL_BLOCK_STARTER_TREES","header","$id","componentId","pluginId","props","align","padding","emailRole","nodes","alt","width","children","variant","footer","color","EMAIL_BLOCK_STARTER_TREES_JSON","JSON","stringify"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;CAeC,GAED,gFAAgF,GAChF,OAAO,MAAMA,gCAAgC,QAAO;AAWpD;;;;CAIC,GACD,OAAO,MAAMC,0BAA0B,UAAS;AAChD,MAAMC,oBAAoBD;AAE1B,OAAO,MAAME,4BAET;IACFC,QAAQ;QACNC,KAAK;QACLC,aAAa;QACbC,UAAUP;QACV,sEAAsE;QACtE,iCAAiC;QACjCQ,OAAO;YAAEC,OAAO;YAAUC,SAAS;YAAIC,WAAW;QAAS;QAC3DC,OAAO;YACL;gBACEP,KAAK;gBACLC,aAAa;gBACbC,UAAUP;gBACVQ,OAAO;oBAAEK,KAAK;oBAAqBC,OAAO;oBAAKL,OAAO;gBAAS;YACjE;YACA;gBACEJ,KAAK;gBACLC,aAAa;gBACbC,UAAUP;gBACVQ,OAAO;oBACLO,UAAU;oBACVC,SAAS;oBACTP,OAAO;gBACT;YACF;SACD;IACH;IACAQ,QAAQ;QACNZ,KAAK;QACLC,aAAa;QACbC,UAAUP;QACV,sEAAsE;QACtE,iCAAiC;QACjCQ,OAAO;YAAEC,OAAO;YAAUC,SAAS;YAAIC,WAAW;QAAS;QAC3DC,OAAO;YACL;gBACEP,KAAK;gBACLC,aAAa;gBACbC,UAAUP;gBACVQ,OAAO,CAAC;YACV;YACA;gBACEH,KAAK;gBACLC,aAAa;gBACbC,UAAUP;gBACVQ,OAAO;oBACLO,UAAU;oBACVC,SAAS;oBACTP,OAAO;oBACPS,OAAOhB;gBACT;YACF;YACA;gBACEG,KAAK;gBACLC,aAAa;gBACbC,UAAUP;gBACVQ,OAAO;oBACLO,UACE;oBACFC,SAAS;oBACTP,OAAO;oBACPS,OAAOhB;gBACT;YACF;SACD;IACH;AACF,EAAC;AAED;;;;CAIC,GACD,OAAO,MAAMiB,iCAAyCC,KAAKC,SAAS,CAClElB,2BACD"}
|
|
@@ -15,136 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type ScreenUid } from '../foundation';
|
|
18
|
-
|
|
19
|
-
* Route path of a host's root screen. The tenant matcher joins the catch-all
|
|
20
|
-
* segments and collapses an empty one to this value (`load-page-data.ts`:
|
|
21
|
-
* `slugSegments.length ? slugSegments.join('/') : SCREEN_ROOT_PATH`), so the
|
|
22
|
-
* root is `'/'` and every other path is slash-joined segments WITHOUT a
|
|
23
|
-
* leading slash (`about`, later `company/about`).
|
|
24
|
-
*/
|
|
25
|
-
export declare const SCREEN_ROOT_PATH = "/";
|
|
26
|
-
/**
|
|
27
|
-
* Normalizes user slug input into the routing-map path format described on
|
|
28
|
-
* {@link SCREEN_ROOT_PATH}. `'/'` normalizes to the root path; anything else
|
|
29
|
-
* becomes a single lowercase url-safe segment.
|
|
30
|
-
*
|
|
31
|
-
* Returns `undefined` for EMPTY input and for anything that sanitizes away
|
|
32
|
-
* (`'###'`) — the two cases are deliberately the same answer, and the caller
|
|
33
|
-
* decides what it means. In the Screens page's slug field an empty string
|
|
34
|
-
* means "no address typed"; in a starter template it meant "home". Two
|
|
35
|
-
* meanings for one value is what made AGL-1575 possible, so this function
|
|
36
|
-
* refuses to pick one: a caller that wants the root must say `'/'`.
|
|
37
|
-
*
|
|
38
|
-
* This docstring used to claim empty input normalized to the root, which the
|
|
39
|
-
* code has never done (see the spec, which asserts the `undefined`). A fix
|
|
40
|
-
* written against the docstring instead of the behaviour would have shipped
|
|
41
|
-
* broken; if you are here to make the code match the comment, do not.
|
|
42
|
-
*/
|
|
43
|
-
export declare function normalizeScreenSlug(input: string | null | undefined): string | undefined;
|
|
44
|
-
/**
|
|
45
|
-
* Route segments the published site CANNOT serve, whatever the routing map
|
|
46
|
-
* says (AGL-2076).
|
|
47
|
-
*
|
|
48
|
-
* Every entry was measured against production on 2026-08-19 with a fresh
|
|
49
|
-
* `x-vercel-cache: MISS`, because a stale negative always passes. Two
|
|
50
|
-
* mechanisms, both of which resolve ahead of `[host]/[scheme]/[[...slug]]`:
|
|
51
|
-
*
|
|
52
|
-
* - `404`, `500` — Next emits `pages/404.html` and `pages/500.html` even for
|
|
53
|
-
* an app-router-only build, and the deployed filesystem answers them as
|
|
54
|
-
* static files (`content-disposition: inline; filename="404"`,
|
|
55
|
-
* `accept-ranges`, an `etag`, and no `x-nextjs-prerender`). The tenant
|
|
56
|
-
* middleware's own CSP headers are on those responses, so the middleware
|
|
57
|
-
* ran and its rewrite still lost. There is no rewrite this app can write
|
|
58
|
-
* that beats them, which is why the honest fix is to refuse the slug at
|
|
59
|
-
* authoring time rather than to keep promising an address that is dead.
|
|
60
|
-
* - `search`, `api`, `_next`, `_static` — routes and exclusions the tenant
|
|
61
|
-
* app owns. `/search` matches `app/[host]/[scheme]/search`; the other three are the
|
|
62
|
-
* middleware matcher's exclusions, so no host rewrite happens at all and
|
|
63
|
-
* the path is parsed as `[host]` with an empty slug.
|
|
64
|
-
* - `aglyn-preview` — the same mechanism as `search`, one route over
|
|
65
|
-
* (AGL-3205). The live-site preview of an unpublished collection entry is
|
|
66
|
-
* a rewrite to `app/[host]/[scheme]/aglyn-preview/[...slug]`, because it
|
|
67
|
-
* must be a DIFFERENT Next route from the ISR-cached catch-all; a static
|
|
68
|
-
* folder wins over the optional catch-all beside it, so `/aglyn-preview/x`
|
|
69
|
-
* would reach the preview page and 404 rather than render a screen. The
|
|
70
|
-
* bare `/aglyn-preview` would in fact still serve — the preview route's
|
|
71
|
-
* catch-all is required, not optional — and it is reserved anyway for the
|
|
72
|
-
* reason given below about child paths: a child's path is composed from
|
|
73
|
-
* its parent's slug, and this rule refuses that slug.
|
|
74
|
-
* - `_aglyn` — the platform's own pages on every site, the leaving notice
|
|
75
|
-
* at `/_aglyn/leaving` first (AGL-3452). The middleware rewrites that path
|
|
76
|
-
* to an api route before the catch-all could see it.
|
|
77
|
-
*
|
|
78
|
-
* NOT reserved, and deliberately so, because measuring found them fine:
|
|
79
|
-
* `401`, `403`, `503` (`/401` returns 200 through the catch-all — two of the
|
|
80
|
-
* four designed error screens have always been viewable at their slugs, which
|
|
81
|
-
* is what made the `404` case look like a Next.js defect), and `index`.
|
|
82
|
-
*
|
|
83
|
-
* `fonts` and `examples` USED to be dead for the third reason and are not
|
|
84
|
-
* here: they were exclusions inherited from the Vercel platforms starter kit
|
|
85
|
-
* for directories `apps/tenant/public` has never contained. They are given
|
|
86
|
-
* back rather than reserved — see the matcher in `apps/tenant/middleware.ts`.
|
|
87
|
-
*/
|
|
88
|
-
export declare const RESERVED_SCREEN_ROUTE_SEGMENTS: readonly string[];
|
|
89
|
-
/**
|
|
90
|
-
* The reserved segment a routing-map path would collide with, or `undefined`
|
|
91
|
-
* when the path is servable. The segment rather than a boolean, so a refusal
|
|
92
|
-
* can name what it is refusing over.
|
|
93
|
-
*
|
|
94
|
-
* Reads the FIRST segment only. For `api`/`_next`/`_static` that is exactly
|
|
95
|
-
* right — the middleware excludes the whole subtree, so `api/docs` is as dead
|
|
96
|
-
* as `api`. For `404`/`500`/`search` it over-refuses in principle (`/404/why`
|
|
97
|
-
* would serve), and that is deliberate: a child path is composed from its
|
|
98
|
-
* PARENT's slug, the parent would have to hold `404`, and this same rule
|
|
99
|
-
* refuses that slug wherever one is typed. A second list to express a case
|
|
100
|
-
* that cannot arise would be a second list to keep correct.
|
|
101
|
-
*
|
|
102
|
-
* Takes a composed routing-map path (`about`, `company/about`, `'/'`), not a
|
|
103
|
-
* raw slug field — `'/'` is the home screen and is never reserved.
|
|
104
|
-
*/
|
|
105
|
-
export declare function reservedScreenRouteSegment(path: string | null | undefined): string | undefined;
|
|
106
|
-
/**
|
|
107
|
-
* The refusal an author reads when {@link reservedScreenRouteSegment} names a
|
|
108
|
-
* segment. One sentence, in one place, because the Screens page, the version
|
|
109
|
-
* view and the besigner all have to say the same thing — three surfaces
|
|
110
|
-
* wording a platform constraint three ways is how AGL-2093's precheck drifted.
|
|
111
|
-
*/
|
|
112
|
-
export declare function reservedScreenRouteMessage(segment: string): string;
|
|
113
|
-
/**
|
|
114
|
-
* Does a raw slug value carry a path separator the one segment it describes
|
|
115
|
-
* cannot hold (AGL-2572)?
|
|
116
|
-
*
|
|
117
|
-
* {@link normalizeScreenSlug} answers with a SINGLE segment, and it gets there
|
|
118
|
-
* by deleting every character outside `[a-z0-9-_]` — an interior `/` included.
|
|
119
|
-
* Whitespace becomes a `-`, but a `/` becomes nothing at all, so
|
|
120
|
-
* `alternatives/webflow` normalizes to `alternativeswebflow` and the two words
|
|
121
|
-
* are stored glued. Nothing about that is visible: the field accepts the
|
|
122
|
-
* keystroke, the composed path reads plausibly, and the screen ends up
|
|
123
|
-
* answering an address nobody typed. Two screens on `aglyn-marketing` carry a
|
|
124
|
-
* slug of that shape.
|
|
125
|
-
*
|
|
126
|
-
* So the separator is a question a slug field asks BEFORE normalizing, and the
|
|
127
|
-
* answer is a refusal rather than a silent repair — the caller cannot know
|
|
128
|
-
* which segment the author meant, and guessing is how the glue happened.
|
|
129
|
-
*
|
|
130
|
-
* LEADING and TRAILING slashes are not separators here. `normalizeScreenSlug`
|
|
131
|
-
* strips them deliberately (`/about/` → `about`), an author typing an address
|
|
132
|
-
* writes them without meaning a hierarchy, and refusing them would reject a
|
|
133
|
-
* value the normalizer has always handled correctly. `'/'` alone is the home
|
|
134
|
-
* page and is never a separator.
|
|
135
|
-
*/
|
|
136
|
-
export declare function screenSlugHasPathSeparator(input: string | null | undefined): boolean;
|
|
137
|
-
/**
|
|
138
|
-
* The refusal an author reads when {@link screenSlugHasPathSeparator} answers
|
|
139
|
-
* true. One sentence, in one place, for the same reason
|
|
140
|
-
* {@link reservedScreenRouteMessage} is: the Screens page, the version view
|
|
141
|
-
* and the besigner all take a slug, and a constraint worded three ways drifts
|
|
142
|
-
* into three different constraints.
|
|
143
|
-
*
|
|
144
|
-
* It names the alternative, because a `/` in this field almost always means
|
|
145
|
-
* "nest me" and the hierarchy is what actually composes the longer path.
|
|
146
|
-
*/
|
|
147
|
-
export declare const SCREEN_SLUG_PATH_SEPARATOR_MESSAGE: string;
|
|
18
|
+
export { normalizeScreenSlug, RESERVED_SCREEN_ROUTE_SEGMENTS, reservedScreenRouteMessage, reservedScreenRouteSegment, SCREEN_ROOT_PATH, SCREEN_SLUG_PATH_SEPARATOR_MESSAGE, screenSlugHasPathSeparator, } from './screen-slug';
|
|
148
19
|
/**
|
|
149
20
|
* `kind` of a page GROUP (AGL-3463): a folder in the Pages list that holds
|
|
150
21
|
* pages and is not one.
|
|
@@ -16,159 +16,10 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { HOST_ERROR_SCREEN_SLOTS } from "../foundation/index.js";
|
|
18
18
|
import { collectionListUrl } from "./collection-entries.js";
|
|
19
|
-
import { ENTRY_PREVIEW_ROUTE_SEGMENT } from "./entry-preview-link.js";
|
|
20
|
-
import { LEAVING_NOTICE_ROUTE_SEGMENT } from "./leaving-notice.js";
|
|
21
|
-
import { PLATFORM_BRAND_NAME } from "./platform-brand.js";
|
|
22
19
|
import { SCREEN_KIND_EMAIL } from "./screen-kind.js";
|
|
20
|
+
import { SCREEN_ROOT_PATH } from "./screen-slug.js";
|
|
23
21
|
import { formatCollectionLinkValue, formatEntryLinkValue, formatFeedLinkValue, parseEntryLinkValue } from "./screen-link-value.js";
|
|
24
|
-
|
|
25
|
-
* Route path of a host's root screen. The tenant matcher joins the catch-all
|
|
26
|
-
* segments and collapses an empty one to this value (`load-page-data.ts`:
|
|
27
|
-
* `slugSegments.length ? slugSegments.join('/') : SCREEN_ROOT_PATH`), so the
|
|
28
|
-
* root is `'/'` and every other path is slash-joined segments WITHOUT a
|
|
29
|
-
* leading slash (`about`, later `company/about`).
|
|
30
|
-
*/ export const SCREEN_ROOT_PATH = '/';
|
|
31
|
-
/**
|
|
32
|
-
* Normalizes user slug input into the routing-map path format described on
|
|
33
|
-
* {@link SCREEN_ROOT_PATH}. `'/'` normalizes to the root path; anything else
|
|
34
|
-
* becomes a single lowercase url-safe segment.
|
|
35
|
-
*
|
|
36
|
-
* Returns `undefined` for EMPTY input and for anything that sanitizes away
|
|
37
|
-
* (`'###'`) — the two cases are deliberately the same answer, and the caller
|
|
38
|
-
* decides what it means. In the Screens page's slug field an empty string
|
|
39
|
-
* means "no address typed"; in a starter template it meant "home". Two
|
|
40
|
-
* meanings for one value is what made AGL-1575 possible, so this function
|
|
41
|
-
* refuses to pick one: a caller that wants the root must say `'/'`.
|
|
42
|
-
*
|
|
43
|
-
* This docstring used to claim empty input normalized to the root, which the
|
|
44
|
-
* code has never done (see the spec, which asserts the `undefined`). A fix
|
|
45
|
-
* written against the docstring instead of the behaviour would have shipped
|
|
46
|
-
* broken; if you are here to make the code match the comment, do not.
|
|
47
|
-
*/ export function normalizeScreenSlug(input) {
|
|
48
|
-
const trimmed = (input != null ? input : '').trim();
|
|
49
|
-
if (!trimmed) return undefined;
|
|
50
|
-
if (trimmed === SCREEN_ROOT_PATH) return SCREEN_ROOT_PATH;
|
|
51
|
-
const segment = trimmed.toLowerCase().replace(/^\/+|\/+$/g, '').replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '').replace(/-{2,}/g, '-').replace(/^-+|-+$/g, '');
|
|
52
|
-
return segment || undefined;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Route segments the published site CANNOT serve, whatever the routing map
|
|
56
|
-
* says (AGL-2076).
|
|
57
|
-
*
|
|
58
|
-
* Every entry was measured against production on 2026-08-19 with a fresh
|
|
59
|
-
* `x-vercel-cache: MISS`, because a stale negative always passes. Two
|
|
60
|
-
* mechanisms, both of which resolve ahead of `[host]/[scheme]/[[...slug]]`:
|
|
61
|
-
*
|
|
62
|
-
* - `404`, `500` — Next emits `pages/404.html` and `pages/500.html` even for
|
|
63
|
-
* an app-router-only build, and the deployed filesystem answers them as
|
|
64
|
-
* static files (`content-disposition: inline; filename="404"`,
|
|
65
|
-
* `accept-ranges`, an `etag`, and no `x-nextjs-prerender`). The tenant
|
|
66
|
-
* middleware's own CSP headers are on those responses, so the middleware
|
|
67
|
-
* ran and its rewrite still lost. There is no rewrite this app can write
|
|
68
|
-
* that beats them, which is why the honest fix is to refuse the slug at
|
|
69
|
-
* authoring time rather than to keep promising an address that is dead.
|
|
70
|
-
* - `search`, `api`, `_next`, `_static` — routes and exclusions the tenant
|
|
71
|
-
* app owns. `/search` matches `app/[host]/[scheme]/search`; the other three are the
|
|
72
|
-
* middleware matcher's exclusions, so no host rewrite happens at all and
|
|
73
|
-
* the path is parsed as `[host]` with an empty slug.
|
|
74
|
-
* - `aglyn-preview` — the same mechanism as `search`, one route over
|
|
75
|
-
* (AGL-3205). The live-site preview of an unpublished collection entry is
|
|
76
|
-
* a rewrite to `app/[host]/[scheme]/aglyn-preview/[...slug]`, because it
|
|
77
|
-
* must be a DIFFERENT Next route from the ISR-cached catch-all; a static
|
|
78
|
-
* folder wins over the optional catch-all beside it, so `/aglyn-preview/x`
|
|
79
|
-
* would reach the preview page and 404 rather than render a screen. The
|
|
80
|
-
* bare `/aglyn-preview` would in fact still serve — the preview route's
|
|
81
|
-
* catch-all is required, not optional — and it is reserved anyway for the
|
|
82
|
-
* reason given below about child paths: a child's path is composed from
|
|
83
|
-
* its parent's slug, and this rule refuses that slug.
|
|
84
|
-
* - `_aglyn` — the platform's own pages on every site, the leaving notice
|
|
85
|
-
* at `/_aglyn/leaving` first (AGL-3452). The middleware rewrites that path
|
|
86
|
-
* to an api route before the catch-all could see it.
|
|
87
|
-
*
|
|
88
|
-
* NOT reserved, and deliberately so, because measuring found them fine:
|
|
89
|
-
* `401`, `403`, `503` (`/401` returns 200 through the catch-all — two of the
|
|
90
|
-
* four designed error screens have always been viewable at their slugs, which
|
|
91
|
-
* is what made the `404` case look like a Next.js defect), and `index`.
|
|
92
|
-
*
|
|
93
|
-
* `fonts` and `examples` USED to be dead for the third reason and are not
|
|
94
|
-
* here: they were exclusions inherited from the Vercel platforms starter kit
|
|
95
|
-
* for directories `apps/tenant/public` has never contained. They are given
|
|
96
|
-
* back rather than reserved — see the matcher in `apps/tenant/middleware.ts`.
|
|
97
|
-
*/ export const RESERVED_SCREEN_ROUTE_SEGMENTS = [
|
|
98
|
-
'404',
|
|
99
|
-
'500',
|
|
100
|
-
'search',
|
|
101
|
-
ENTRY_PREVIEW_ROUTE_SEGMENT,
|
|
102
|
-
'api',
|
|
103
|
-
'_next',
|
|
104
|
-
'_static',
|
|
105
|
-
LEAVING_NOTICE_ROUTE_SEGMENT
|
|
106
|
-
];
|
|
107
|
-
/**
|
|
108
|
-
* The reserved segment a routing-map path would collide with, or `undefined`
|
|
109
|
-
* when the path is servable. The segment rather than a boolean, so a refusal
|
|
110
|
-
* can name what it is refusing over.
|
|
111
|
-
*
|
|
112
|
-
* Reads the FIRST segment only. For `api`/`_next`/`_static` that is exactly
|
|
113
|
-
* right — the middleware excludes the whole subtree, so `api/docs` is as dead
|
|
114
|
-
* as `api`. For `404`/`500`/`search` it over-refuses in principle (`/404/why`
|
|
115
|
-
* would serve), and that is deliberate: a child path is composed from its
|
|
116
|
-
* PARENT's slug, the parent would have to hold `404`, and this same rule
|
|
117
|
-
* refuses that slug wherever one is typed. A second list to express a case
|
|
118
|
-
* that cannot arise would be a second list to keep correct.
|
|
119
|
-
*
|
|
120
|
-
* Takes a composed routing-map path (`about`, `company/about`, `'/'`), not a
|
|
121
|
-
* raw slug field — `'/'` is the home screen and is never reserved.
|
|
122
|
-
*/ export function reservedScreenRouteSegment(path) {
|
|
123
|
-
if (!path || path === SCREEN_ROOT_PATH) return undefined;
|
|
124
|
-
const [first] = path.replace(/^\/+/, '').split('/');
|
|
125
|
-
return RESERVED_SCREEN_ROUTE_SEGMENTS.includes(first) ? first : undefined;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* The refusal an author reads when {@link reservedScreenRouteSegment} names a
|
|
129
|
-
* segment. One sentence, in one place, because the Screens page, the version
|
|
130
|
-
* view and the besigner all have to say the same thing — three surfaces
|
|
131
|
-
* wording a platform constraint three ways is how AGL-2093's precheck drifted.
|
|
132
|
-
*/ export function reservedScreenRouteMessage(segment) {
|
|
133
|
-
return `"/${segment}" is a reserved address on every ${PLATFORM_BRAND_NAME} site — pick another slug`;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Does a raw slug value carry a path separator the one segment it describes
|
|
137
|
-
* cannot hold (AGL-2572)?
|
|
138
|
-
*
|
|
139
|
-
* {@link normalizeScreenSlug} answers with a SINGLE segment, and it gets there
|
|
140
|
-
* by deleting every character outside `[a-z0-9-_]` — an interior `/` included.
|
|
141
|
-
* Whitespace becomes a `-`, but a `/` becomes nothing at all, so
|
|
142
|
-
* `alternatives/webflow` normalizes to `alternativeswebflow` and the two words
|
|
143
|
-
* are stored glued. Nothing about that is visible: the field accepts the
|
|
144
|
-
* keystroke, the composed path reads plausibly, and the screen ends up
|
|
145
|
-
* answering an address nobody typed. Two screens on `aglyn-marketing` carry a
|
|
146
|
-
* slug of that shape.
|
|
147
|
-
*
|
|
148
|
-
* So the separator is a question a slug field asks BEFORE normalizing, and the
|
|
149
|
-
* answer is a refusal rather than a silent repair — the caller cannot know
|
|
150
|
-
* which segment the author meant, and guessing is how the glue happened.
|
|
151
|
-
*
|
|
152
|
-
* LEADING and TRAILING slashes are not separators here. `normalizeScreenSlug`
|
|
153
|
-
* strips them deliberately (`/about/` → `about`), an author typing an address
|
|
154
|
-
* writes them without meaning a hierarchy, and refusing them would reject a
|
|
155
|
-
* value the normalizer has always handled correctly. `'/'` alone is the home
|
|
156
|
-
* page and is never a separator.
|
|
157
|
-
*/ export function screenSlugHasPathSeparator(input) {
|
|
158
|
-
const trimmed = (input != null ? input : '').trim();
|
|
159
|
-
if (!trimmed || trimmed === SCREEN_ROOT_PATH) return false;
|
|
160
|
-
return trimmed.replace(/^\/+|\/+$/g, '').includes('/');
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* The refusal an author reads when {@link screenSlugHasPathSeparator} answers
|
|
164
|
-
* true. One sentence, in one place, for the same reason
|
|
165
|
-
* {@link reservedScreenRouteMessage} is: the Screens page, the version view
|
|
166
|
-
* and the besigner all take a slug, and a constraint worded three ways drifts
|
|
167
|
-
* into three different constraints.
|
|
168
|
-
*
|
|
169
|
-
* It names the alternative, because a `/` in this field almost always means
|
|
170
|
-
* "nest me" and the hierarchy is what actually composes the longer path.
|
|
171
|
-
*/ export const SCREEN_SLUG_PATH_SEPARATOR_MESSAGE = 'A slug is one path segment — remove the "/" and nest the page under a ' + 'parent page instead';
|
|
22
|
+
export { normalizeScreenSlug, RESERVED_SCREEN_ROUTE_SEGMENTS, reservedScreenRouteMessage, reservedScreenRouteSegment, SCREEN_ROOT_PATH, SCREEN_SLUG_PATH_SEPARATOR_MESSAGE, screenSlugHasPathSeparator } from "./screen-slug.js";
|
|
172
23
|
/**
|
|
173
24
|
* `kind` of a page GROUP (AGL-3463): a folder in the Pages list that holds
|
|
174
25
|
* pages and is not one.
|