@aglyn/aglyn 1.0.0-beta.146 → 1.0.0-beta.149
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 +10 -10
- package/src/lib/app-utils/binding-token-catalog.js +9 -2
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +95 -3
- package/src/lib/app-utils/collection-entries.js +146 -10
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +2 -0
- package/src/lib/app-utils/console-routes.js +14 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +3 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +2 -2
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +136 -2
- package/src/lib/app-utils/notifications.js +139 -3
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/foundation/definitions/host-content-collections.d.ts +42 -40
- package/src/lib/foundation/definitions/host-content-collections.js +47 -109
- package/src/lib/foundation/definitions/host-content-collections.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +70 -0
- package/src/lib/plugin-manager/feature-plugins.js +27 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +13 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +257 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +16 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +27 -5
- package/src/lib/plugin-manager/plugin-host-collections.js +25 -16
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-org-capacity.d.ts +57 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js +68 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -0
- package/src/lib/plugin-manager/record-captured-contact.d.ts +89 -0
- package/src/lib/plugin-manager/record-captured-contact.js +128 -0
- package/src/lib/plugin-manager/record-captured-contact.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.149",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
36
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.149",
|
|
37
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.149",
|
|
38
|
+
"@aglyn/shared-data-types": "1.0.0-beta.149",
|
|
39
|
+
"@aglyn/shared-util-email": "1.0.0-beta.149",
|
|
40
|
+
"@aglyn/shared-util-http": "1.0.0-beta.149",
|
|
41
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.149",
|
|
42
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.149",
|
|
43
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.149",
|
|
44
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.149",
|
|
45
45
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
46
46
|
"@msgpack/msgpack": "^3.1.3",
|
|
47
47
|
"@types/unist": "^3.0.3",
|
|
@@ -177,7 +177,11 @@ import { humanizeDatasetFieldId } from "./datasets.js";
|
|
|
177
177
|
{
|
|
178
178
|
token: '{{pagination.totalPages}}',
|
|
179
179
|
label: 'Total pages',
|
|
180
|
-
|
|
180
|
+
// A listing bigger than one read cannot be counted honestly (AGL-3219):
|
|
181
|
+
// the count and the entries were read at two different moments, and the
|
|
182
|
+
// pager built from them disagreed with itself at the page boundary. It
|
|
183
|
+
// still resolves, so a template binding it keeps rendering.
|
|
184
|
+
description: 'Pages in the listing, after any category filter. Empty on a listing too large to count — bind the next link instead.'
|
|
181
185
|
},
|
|
182
186
|
{
|
|
183
187
|
token: '{{pagination.prevUrl}}',
|
|
@@ -187,7 +191,10 @@ import { humanizeDatasetFieldId } from "./datasets.js";
|
|
|
187
191
|
{
|
|
188
192
|
token: '{{pagination.nextUrl}}',
|
|
189
193
|
label: 'Next page link',
|
|
190
|
-
|
|
194
|
+
// The reliable "is there more" (AGL-3219): it is set from a read that
|
|
195
|
+
// asked for one entry more than the page needed, so empty means there was
|
|
196
|
+
// no such entry rather than that a total said so.
|
|
197
|
+
description: 'Keeps the category; empty when there is nothing older to show.'
|
|
191
198
|
}
|
|
192
199
|
];
|
|
193
200
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/binding-token-catalog.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\nimport {\n DATASET_FIELD_TYPE_LABELS,\n type DatasetModel,\n} from './dataset-models'\nimport { humanizeDatasetFieldId } from './datasets'\n\n/**\n * Browsable data-placeholder catalogs for the designer's insert picker\n * (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these\n * catalogs give every token a friendly label + description so editors can\n * browse and insert instead of memorizing the grammar.\n *\n * The token STRINGS are owned elsewhere — `collectionEntryTokens`\n * (collection-entries.ts) and the tenant compose pipeline resolve them —\n * this module only names them for pickers. The spec cross-checks the entry\n * catalog against the resolver so the two can never drift.\n */\nexport interface BindingTokenCatalogEntry {\n /** The literal token inserted into the prop, e.g. `{{entry.title}}`. */\n token: string\n /** Friendly picker label, e.g. `Title`. */\n label: string\n /** One-line description shown as the option's secondary text. */\n description?: string\n}\n\n/**\n * `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a\n * Collection entries block and page-wide on entry-template screens.\n * Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.\n */\nexport const ENTRY_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n { token: '{{entry.title}}', label: 'Title', description: 'The entry headline.' },\n {\n token: '{{entry.excerpt}}',\n label: 'Excerpt',\n description: 'Short summary text.',\n },\n {\n token: '{{entry.body}}',\n label: 'Body',\n description: 'The full entry body.',\n },\n {\n token: '{{entry.url}}',\n label: 'Link URL',\n description: 'Auto-route to the entry page.',\n },\n {\n token: '{{entry.date}}',\n label: 'Published date',\n description: 'Formatted publish date.',\n },\n /*\n Beside the readable date on purpose: a designer reaching for \"the date\" in\n a Video element's Publication date field has to see that the readable one\n is the wrong pick there, and why.\n */\n {\n token: '{{entry.publishedAt}}',\n label: 'Publish timestamp',\n description:\n 'The publish date and time in ISO 8601, for a field that needs a ' +\n 'machine-readable date, such as a Video element’s publication date.',\n },\n {\n token: '{{entry.author}}',\n label: 'Author',\n description: 'The byline set on the entry.',\n },\n {\n token: '{{entry.authorBio}}',\n label: 'Author bio',\n description: 'Blurb from the author’s record.',\n },\n {\n token: '{{entry.authorImage}}',\n label: 'Author portrait',\n description: 'Portrait or logo from the author’s record.',\n },\n {\n token: '{{entry.authorUrl}}',\n label: 'Author link',\n description: 'The author’s own page.',\n },\n {\n token: '{{entry.authorPageUrl}}',\n label: 'Author page',\n description:\n 'This author’s page on this site — everything they wrote, across every ' +\n 'collection. Separate from Author link, which is their own site.',\n },\n {\n token: '{{entry.slug}}',\n label: 'Slug',\n description: 'URL-safe entry identifier.',\n },\n /*\n Named \"Its collection…\" rather than \"Collection…\", which is what these\n describe and also exactly what the `{{collection.*}}` entries below are\n called. The picker groups options by heading, so the two sets sit apart —\n but a designer scanning it reads the LABEL, and two options reading\n \"Collection name\" three lines apart is a choice made by guessing. The\n pronoun is doing real work: on the one page where both resolve, they mean\n different collections.\n */\n {\n token: '{{entry.collection}}',\n label: 'Its collection',\n description:\n 'Which section this entry belongs to. Worth binding on a listing that ' +\n 'MIXES collections — an author page — where a card otherwise cannot ' +\n 'tell a release note from an essay.',\n },\n {\n token: '{{entry.collectionSlug}}',\n label: 'Its collection slug',\n description: 'That collection’s URL segment.',\n },\n {\n token: '{{entry.collectionUrl}}',\n label: 'Its collection link',\n description: 'The listing this entry belongs to.',\n },\n {\n token: '{{entry.coverImage}}',\n label: 'Cover image',\n description: 'Cover image URL.',\n },\n {\n token: '{{entry.coverVideo}}',\n label: 'Featured video',\n description:\n 'The featured video’s source — a library film, a video link or a ' +\n 'Wistia link — for a Video element.',\n },\n {\n token: '{{entry.category}}',\n label: 'Category',\n description: 'The entry category.',\n },\n {\n token: '{{entry.tags}}',\n label: 'Tags',\n description: 'Tags, comma separated.',\n },\n {\n token: '{{entry.seoTitle}}',\n label: 'SEO title',\n description: 'Search title; falls back to Title.',\n },\n {\n token: '{{entry.seoDescription}}',\n label: 'SEO description',\n description: 'Meta description; falls back to Excerpt.',\n },\n]\n\n/**\n * `{{collection.*}}` and `{{pagination.*}}` tokens (AGL-551/1321/1386) —\n * resolve on collection list/entry template screens (see the tenant compose\n * pipeline's collection tokens).\n *\n * The category and pagination tokens all resolve to the empty string where\n * they do not apply, so they are safe to bind unconditionally: ONE list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, and a template has no runtime conditional to vary\n * itself with.\n */\nexport const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{collection.name}}',\n label: 'Collection name',\n description: 'Display name of the routed collection.',\n },\n {\n token: '{{collection.slug}}',\n label: 'Collection slug',\n description: 'URL slug of the routed collection.',\n },\n {\n token: '{{collection.category}}',\n label: 'Filtered category',\n description: 'Category the URL filtered on; empty when unfiltered.',\n },\n {\n token: '{{collection.categorySlug}}',\n label: 'Filtered category slug',\n description: 'That category’s URL segment; empty when unfiltered.',\n },\n {\n token: '{{pagination.page}}',\n label: 'Current page',\n description: 'Page number this URL is showing.',\n },\n {\n token: '{{pagination.totalPages}}',\n label: 'Total pages',\n description: 'Pages in the listing, after any category filter.',\n },\n {\n token: '{{pagination.prevUrl}}',\n label: 'Previous page link',\n description: 'Keeps the category; empty on the first page.',\n },\n {\n token: '{{pagination.nextUrl}}',\n label: 'Next page link',\n description: 'Keeps the category; empty on the last page.',\n },\n]\n\n/**\n * `{{author.*}}` tokens (AGL-2518) — resolve on an author's own page,\n * `/author/{slug}`.\n *\n * Empty everywhere else, like the category tokens above and for the same\n * reason: a template has no runtime conditional, so the tokens have to be the\n * thing that varies. A heading bound to `{{author.name}}` prints a name on an\n * author page and nothing anywhere else.\n *\n * The `{{pagination.*}}` tokens resolve HERE TOO, over this author's own\n * archive — deliberately the same four names a collection listing uses, so a\n * pager built once works on both.\n */\nexport const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{author.name}}',\n label: 'Name',\n description: 'The byline this page collects.',\n },\n {\n token: '{{author.bio}}',\n label: 'Bio',\n description: 'Their blurb, from the author record.',\n },\n {\n token: '{{author.image}}',\n label: 'Portrait',\n description: 'Their portrait or logo, from the author record.',\n },\n {\n token: '{{author.jobTitle}}',\n label: 'Role',\n description: 'Their job title; empty for an Organization author.',\n },\n {\n token: '{{author.worksFor}}',\n label: 'Organization',\n description: 'Who they write for; empty for an Organization author.',\n },\n {\n token: '{{author.url}}',\n label: 'Their own site',\n description:\n 'The url on their record — a personal site, not this page. Empty when ' +\n 'they have none.',\n },\n {\n token: '{{author.pageUrl}}',\n label: 'This page',\n description: 'The canonical address of the page you are designing.',\n },\n {\n token: '{{author.entryCount}}',\n label: 'Post count',\n description: 'How many entries they have published, across every collection.',\n },\n {\n token: '{{author.entryCountLabel}}',\n label: 'Post count, worded',\n description:\n '\"1 post\" or \"12 posts\" — pluralized for you, because a template has no ' +\n 'conditional to do it with.',\n },\n]\n\n/**\n * Renders an `{{item.*}}` token for a dataset field, optionally hopping\n * one reference to a field of the referenced record (AGL-180). Always\n * takes the stable model field ID — display names are labels only.\n */\nexport function datasetItemToken(\n fieldId: string,\n targetFieldId?: string,\n): string {\n return targetFieldId\n ? `{{item.${fieldId}.${targetFieldId}}}`\n : `{{item.${fieldId}}}`\n}\n\n/**\n * Dataset-item token entries for a repeatable container's model: one per\n * field in model order, labeled with the CURRENT display name while the\n * token carries the stable reference id (AGL-578 — renaming a label never\n * breaks bindings). Reference fields with a configured display field also\n * get their one-hop token (`{{item.author.name}}`).\n */\nexport function datasetItemTokens(\n model: DatasetModel,\n): BindingTokenCatalogEntry[] {\n const entries: BindingTokenCatalogEntry[] = []\n for (const fieldId of model.order ?? []) {\n const field = model.fields?.[fieldId]\n if (!field) continue\n const label = field.name?.trim() || humanizeDatasetFieldId(fieldId)\n entries.push({\n token: datasetItemToken(fieldId),\n label,\n description: DATASET_FIELD_TYPE_LABELS[field.type] ?? field.type,\n })\n // One reference hop (AGL-180): surface the configured display field of\n // the referenced record — the hop editors reach for most.\n const hopId =\n field.type === 'reference' ? field.reference?.displayFieldId : undefined\n if (hopId) {\n entries.push({\n token: datasetItemToken(fieldId, hopId),\n label: `${label} → ${humanizeDatasetFieldId(hopId)}`,\n description: 'Field from the referenced record.',\n })\n }\n }\n return entries\n}\n"],"names":["DATASET_FIELD_TYPE_LABELS","humanizeDatasetFieldId","ENTRY_TOKEN_CATALOG","token","label","description","COLLECTION_TOKEN_CATALOG","AUTHOR_TOKEN_CATALOG","datasetItemToken","fieldId","targetFieldId","datasetItemTokens","model","entries","order","field","fields","name","trim","push","type","hopId","reference","displayFieldId","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAEpB,sBAAkB;AACzB,SAASC,sBAAsB,QAAQ,gBAAY;AAsBnD;;;;CAIC,GACD,OAAO,MAAMC,sBAA2D;IACtE;QAAEC,OAAO;QAAmBC,OAAO;QAASC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;EAIA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,2EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;;;;;EAQA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,2BAAgE;IAC3E;QACEH,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAME,uBAA4D;IACvE;QACEJ,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,4EACA;IACJ;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,SAASG,iBACdC,OAAe,EACfC,aAAsB;IAEtB,OAAOA,gBACH,CAAC,OAAO,EAAED,QAAQ,CAAC,EAAEC,cAAc,EAAE,CAAC,GACtC,CAAC,OAAO,EAAED,QAAQ,EAAE,CAAC;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,kBACdC,KAAmB;QAGGA;IADtB,MAAMC,UAAsC,EAAE;IAC9C,KAAK,MAAMJ,YAAWG,eAAAA,MAAME,KAAK,YAAXF,eAAe,EAAE,CAAE;YAOxBZ;YANDY,eAEAG,aASiBA;QAX/B,MAAMA,SAAQH,gBAAAA,MAAMI,MAAM,qBAAZJ,aAAc,CAACH,QAAQ;QACrC,IAAI,CAACM,OAAO;QACZ,MAAMX,QAAQW,EAAAA,cAAAA,MAAME,IAAI,qBAAVF,YAAYG,IAAI,OAAMjB,uBAAuBQ;QAC3DI,QAAQM,IAAI,CAAC;YACXhB,OAAOK,iBAAiBC;YACxBL;YACAC,WAAW,GAAEL,wCAAAA,yBAAyB,CAACe,MAAMK,IAAI,CAAC,YAArCpB,wCAAyCe,MAAMK,IAAI;QAClE;QACA,uEAAuE;QACvE,0DAA0D;QAC1D,MAAMC,QACJN,MAAMK,IAAI,KAAK,eAAcL,mBAAAA,MAAMO,SAAS,qBAAfP,iBAAiBQ,cAAc,GAAGC;QACjE,IAAIH,OAAO;YACTR,QAAQM,IAAI,CAAC;gBACXhB,OAAOK,iBAAiBC,SAASY;gBACjCjB,OAAO,GAAGA,MAAM,GAAG,EAAEH,uBAAuBoB,QAAQ;gBACpDhB,aAAa;YACf;QACF;IACF;IACA,OAAOQ;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/binding-token-catalog.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\nimport {\n DATASET_FIELD_TYPE_LABELS,\n type DatasetModel,\n} from './dataset-models'\nimport { humanizeDatasetFieldId } from './datasets'\n\n/**\n * Browsable data-placeholder catalogs for the designer's insert picker\n * (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these\n * catalogs give every token a friendly label + description so editors can\n * browse and insert instead of memorizing the grammar.\n *\n * The token STRINGS are owned elsewhere — `collectionEntryTokens`\n * (collection-entries.ts) and the tenant compose pipeline resolve them —\n * this module only names them for pickers. The spec cross-checks the entry\n * catalog against the resolver so the two can never drift.\n */\nexport interface BindingTokenCatalogEntry {\n /** The literal token inserted into the prop, e.g. `{{entry.title}}`. */\n token: string\n /** Friendly picker label, e.g. `Title`. */\n label: string\n /** One-line description shown as the option's secondary text. */\n description?: string\n}\n\n/**\n * `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a\n * Collection entries block and page-wide on entry-template screens.\n * Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.\n */\nexport const ENTRY_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n { token: '{{entry.title}}', label: 'Title', description: 'The entry headline.' },\n {\n token: '{{entry.excerpt}}',\n label: 'Excerpt',\n description: 'Short summary text.',\n },\n {\n token: '{{entry.body}}',\n label: 'Body',\n description: 'The full entry body.',\n },\n {\n token: '{{entry.url}}',\n label: 'Link URL',\n description: 'Auto-route to the entry page.',\n },\n {\n token: '{{entry.date}}',\n label: 'Published date',\n description: 'Formatted publish date.',\n },\n /*\n Beside the readable date on purpose: a designer reaching for \"the date\" in\n a Video element's Publication date field has to see that the readable one\n is the wrong pick there, and why.\n */\n {\n token: '{{entry.publishedAt}}',\n label: 'Publish timestamp',\n description:\n 'The publish date and time in ISO 8601, for a field that needs a ' +\n 'machine-readable date, such as a Video element’s publication date.',\n },\n {\n token: '{{entry.author}}',\n label: 'Author',\n description: 'The byline set on the entry.',\n },\n {\n token: '{{entry.authorBio}}',\n label: 'Author bio',\n description: 'Blurb from the author’s record.',\n },\n {\n token: '{{entry.authorImage}}',\n label: 'Author portrait',\n description: 'Portrait or logo from the author’s record.',\n },\n {\n token: '{{entry.authorUrl}}',\n label: 'Author link',\n description: 'The author’s own page.',\n },\n {\n token: '{{entry.authorPageUrl}}',\n label: 'Author page',\n description:\n 'This author’s page on this site — everything they wrote, across every ' +\n 'collection. Separate from Author link, which is their own site.',\n },\n {\n token: '{{entry.slug}}',\n label: 'Slug',\n description: 'URL-safe entry identifier.',\n },\n /*\n Named \"Its collection…\" rather than \"Collection…\", which is what these\n describe and also exactly what the `{{collection.*}}` entries below are\n called. The picker groups options by heading, so the two sets sit apart —\n but a designer scanning it reads the LABEL, and two options reading\n \"Collection name\" three lines apart is a choice made by guessing. The\n pronoun is doing real work: on the one page where both resolve, they mean\n different collections.\n */\n {\n token: '{{entry.collection}}',\n label: 'Its collection',\n description:\n 'Which section this entry belongs to. Worth binding on a listing that ' +\n 'MIXES collections — an author page — where a card otherwise cannot ' +\n 'tell a release note from an essay.',\n },\n {\n token: '{{entry.collectionSlug}}',\n label: 'Its collection slug',\n description: 'That collection’s URL segment.',\n },\n {\n token: '{{entry.collectionUrl}}',\n label: 'Its collection link',\n description: 'The listing this entry belongs to.',\n },\n {\n token: '{{entry.coverImage}}',\n label: 'Cover image',\n description: 'Cover image URL.',\n },\n {\n token: '{{entry.coverVideo}}',\n label: 'Featured video',\n description:\n 'The featured video’s source — a library film, a video link or a ' +\n 'Wistia link — for a Video element.',\n },\n {\n token: '{{entry.category}}',\n label: 'Category',\n description: 'The entry category.',\n },\n {\n token: '{{entry.tags}}',\n label: 'Tags',\n description: 'Tags, comma separated.',\n },\n {\n token: '{{entry.seoTitle}}',\n label: 'SEO title',\n description: 'Search title; falls back to Title.',\n },\n {\n token: '{{entry.seoDescription}}',\n label: 'SEO description',\n description: 'Meta description; falls back to Excerpt.',\n },\n]\n\n/**\n * `{{collection.*}}` and `{{pagination.*}}` tokens (AGL-551/1321/1386) —\n * resolve on collection list/entry template screens (see the tenant compose\n * pipeline's collection tokens).\n *\n * The category and pagination tokens all resolve to the empty string where\n * they do not apply, so they are safe to bind unconditionally: ONE list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, and a template has no runtime conditional to vary\n * itself with.\n */\nexport const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{collection.name}}',\n label: 'Collection name',\n description: 'Display name of the routed collection.',\n },\n {\n token: '{{collection.slug}}',\n label: 'Collection slug',\n description: 'URL slug of the routed collection.',\n },\n {\n token: '{{collection.category}}',\n label: 'Filtered category',\n description: 'Category the URL filtered on; empty when unfiltered.',\n },\n {\n token: '{{collection.categorySlug}}',\n label: 'Filtered category slug',\n description: 'That category’s URL segment; empty when unfiltered.',\n },\n {\n token: '{{pagination.page}}',\n label: 'Current page',\n description: 'Page number this URL is showing.',\n },\n {\n token: '{{pagination.totalPages}}',\n label: 'Total pages',\n // A listing bigger than one read cannot be counted honestly (AGL-3219):\n // the count and the entries were read at two different moments, and the\n // pager built from them disagreed with itself at the page boundary. It\n // still resolves, so a template binding it keeps rendering.\n description:\n 'Pages in the listing, after any category filter. Empty on a listing too large to count — bind the next link instead.',\n },\n {\n token: '{{pagination.prevUrl}}',\n label: 'Previous page link',\n description: 'Keeps the category; empty on the first page.',\n },\n {\n token: '{{pagination.nextUrl}}',\n label: 'Next page link',\n // The reliable \"is there more\" (AGL-3219): it is set from a read that\n // asked for one entry more than the page needed, so empty means there was\n // no such entry rather than that a total said so.\n description:\n 'Keeps the category; empty when there is nothing older to show.',\n },\n]\n\n/**\n * `{{author.*}}` tokens (AGL-2518) — resolve on an author's own page,\n * `/author/{slug}`.\n *\n * Empty everywhere else, like the category tokens above and for the same\n * reason: a template has no runtime conditional, so the tokens have to be the\n * thing that varies. A heading bound to `{{author.name}}` prints a name on an\n * author page and nothing anywhere else.\n *\n * The `{{pagination.*}}` tokens resolve HERE TOO, over this author's own\n * archive — deliberately the same four names a collection listing uses, so a\n * pager built once works on both.\n */\nexport const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{author.name}}',\n label: 'Name',\n description: 'The byline this page collects.',\n },\n {\n token: '{{author.bio}}',\n label: 'Bio',\n description: 'Their blurb, from the author record.',\n },\n {\n token: '{{author.image}}',\n label: 'Portrait',\n description: 'Their portrait or logo, from the author record.',\n },\n {\n token: '{{author.jobTitle}}',\n label: 'Role',\n description: 'Their job title; empty for an Organization author.',\n },\n {\n token: '{{author.worksFor}}',\n label: 'Organization',\n description: 'Who they write for; empty for an Organization author.',\n },\n {\n token: '{{author.url}}',\n label: 'Their own site',\n description:\n 'The url on their record — a personal site, not this page. Empty when ' +\n 'they have none.',\n },\n {\n token: '{{author.pageUrl}}',\n label: 'This page',\n description: 'The canonical address of the page you are designing.',\n },\n {\n token: '{{author.entryCount}}',\n label: 'Post count',\n description: 'How many entries they have published, across every collection.',\n },\n {\n token: '{{author.entryCountLabel}}',\n label: 'Post count, worded',\n description:\n '\"1 post\" or \"12 posts\" — pluralized for you, because a template has no ' +\n 'conditional to do it with.',\n },\n]\n\n/**\n * Renders an `{{item.*}}` token for a dataset field, optionally hopping\n * one reference to a field of the referenced record (AGL-180). Always\n * takes the stable model field ID — display names are labels only.\n */\nexport function datasetItemToken(\n fieldId: string,\n targetFieldId?: string,\n): string {\n return targetFieldId\n ? `{{item.${fieldId}.${targetFieldId}}}`\n : `{{item.${fieldId}}}`\n}\n\n/**\n * Dataset-item token entries for a repeatable container's model: one per\n * field in model order, labeled with the CURRENT display name while the\n * token carries the stable reference id (AGL-578 — renaming a label never\n * breaks bindings). Reference fields with a configured display field also\n * get their one-hop token (`{{item.author.name}}`).\n */\nexport function datasetItemTokens(\n model: DatasetModel,\n): BindingTokenCatalogEntry[] {\n const entries: BindingTokenCatalogEntry[] = []\n for (const fieldId of model.order ?? []) {\n const field = model.fields?.[fieldId]\n if (!field) continue\n const label = field.name?.trim() || humanizeDatasetFieldId(fieldId)\n entries.push({\n token: datasetItemToken(fieldId),\n label,\n description: DATASET_FIELD_TYPE_LABELS[field.type] ?? field.type,\n })\n // One reference hop (AGL-180): surface the configured display field of\n // the referenced record — the hop editors reach for most.\n const hopId =\n field.type === 'reference' ? field.reference?.displayFieldId : undefined\n if (hopId) {\n entries.push({\n token: datasetItemToken(fieldId, hopId),\n label: `${label} → ${humanizeDatasetFieldId(hopId)}`,\n description: 'Field from the referenced record.',\n })\n }\n }\n return entries\n}\n"],"names":["DATASET_FIELD_TYPE_LABELS","humanizeDatasetFieldId","ENTRY_TOKEN_CATALOG","token","label","description","COLLECTION_TOKEN_CATALOG","AUTHOR_TOKEN_CATALOG","datasetItemToken","fieldId","targetFieldId","datasetItemTokens","model","entries","order","field","fields","name","trim","push","type","hopId","reference","displayFieldId","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAEpB,sBAAkB;AACzB,SAASC,sBAAsB,QAAQ,gBAAY;AAsBnD;;;;CAIC,GACD,OAAO,MAAMC,sBAA2D;IACtE;QAAEC,OAAO;QAAmBC,OAAO;QAASC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;EAIA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,2EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;;;;;EAQA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,2BAAgE;IAC3E;QACEH,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,4DAA4D;QAC5DC,aACE;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,sEAAsE;QACtE,0EAA0E;QAC1E,kDAAkD;QAClDC,aACE;IACJ;CACD,CAAA;AAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAME,uBAA4D;IACvE;QACEJ,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,4EACA;IACJ;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,SAASG,iBACdC,OAAe,EACfC,aAAsB;IAEtB,OAAOA,gBACH,CAAC,OAAO,EAAED,QAAQ,CAAC,EAAEC,cAAc,EAAE,CAAC,GACtC,CAAC,OAAO,EAAED,QAAQ,EAAE,CAAC;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,kBACdC,KAAmB;QAGGA;IADtB,MAAMC,UAAsC,EAAE;IAC9C,KAAK,MAAMJ,YAAWG,eAAAA,MAAME,KAAK,YAAXF,eAAe,EAAE,CAAE;YAOxBZ;YANDY,eAEAG,aASiBA;QAX/B,MAAMA,SAAQH,gBAAAA,MAAMI,MAAM,qBAAZJ,aAAc,CAACH,QAAQ;QACrC,IAAI,CAACM,OAAO;QACZ,MAAMX,QAAQW,EAAAA,cAAAA,MAAME,IAAI,qBAAVF,YAAYG,IAAI,OAAMjB,uBAAuBQ;QAC3DI,QAAQM,IAAI,CAAC;YACXhB,OAAOK,iBAAiBC;YACxBL;YACAC,WAAW,GAAEL,wCAAAA,yBAAyB,CAACe,MAAMK,IAAI,CAAC,YAArCpB,wCAAyCe,MAAMK,IAAI;QAClE;QACA,uEAAuE;QACvE,0DAA0D;QAC1D,MAAMC,QACJN,MAAMK,IAAI,KAAK,eAAcL,mBAAAA,MAAMO,SAAS,qBAAfP,iBAAiBQ,cAAc,GAAGC;QACjE,IAAIH,OAAO;YACTR,QAAQM,IAAI,CAAC;gBACXhB,OAAOK,iBAAiBC,SAASY;gBACjCjB,OAAO,GAAGA,MAAM,GAAG,EAAEH,uBAAuBoB,QAAQ;gBACpDhB,aAAa;YACf;QACF;IACF;IACA,OAAOQ;AACT"}
|
|
@@ -81,6 +81,24 @@ export declare const COLLECTION_SEARCH_COMPONENT_ID = "collectionSearch";
|
|
|
81
81
|
*/
|
|
82
82
|
export declare const COLLECTION_PAGE_ROUTE_SEGMENT = "page";
|
|
83
83
|
export declare const COLLECTION_CATEGORY_ROUTE_SEGMENT = "category";
|
|
84
|
+
/**
|
|
85
|
+
* `/{collection}/after/{cursor}` — the older page (AGL-3219).
|
|
86
|
+
*
|
|
87
|
+
* ## Why a PATH segment and not `?after=`
|
|
88
|
+
*
|
|
89
|
+
* The tenant catch-all is statically rendered and ISR-cached with the URL
|
|
90
|
+
* PATH as its key (`revalidate = 3600`, AGL-1152). A query string is not part
|
|
91
|
+
* of that key, so every cursor address would share ONE cache entry and serve
|
|
92
|
+
* whichever of them rendered first to everybody — the same reason a routed
|
|
93
|
+
* category is `/{collection}/category/{slug}` rather than `?category=`. And
|
|
94
|
+
* reading `searchParams` at all opts the whole catch-all out of static
|
|
95
|
+
* rendering, which is the opposite of what that page is for.
|
|
96
|
+
*
|
|
97
|
+
* A path segment is cached per address, crawlable, and costs nothing.
|
|
98
|
+
*/
|
|
99
|
+
export declare const COLLECTION_AFTER_ROUTE_SEGMENT = "after";
|
|
100
|
+
/** `/{collection}/before/{cursor}` — the newer page (AGL-3219). */
|
|
101
|
+
export declare const COLLECTION_BEFORE_ROUTE_SEGMENT = "before";
|
|
84
102
|
/** Namespaces cloned template ids per container/entry (cf. `rep__`). */
|
|
85
103
|
export declare const COLLECTION_ENTRIES_NODE_ID_PREFIX = "centry__";
|
|
86
104
|
/** Hard bound on entries a single block renders, before `entriesLimit`. */
|
|
@@ -140,6 +158,38 @@ export declare const COLLECTION_LIST_PAGE_SIZE = 10;
|
|
|
140
158
|
* empty collection still has a page 1 (AGL-620).
|
|
141
159
|
*/
|
|
142
160
|
export declare function collectionTotalPages(total: number, perPage: number): number;
|
|
161
|
+
/**
|
|
162
|
+
* The slice of a listing's entries that the page named by `pagination`
|
|
163
|
+
* actually shows (AGL-3213).
|
|
164
|
+
*
|
|
165
|
+
* The loader hands the WHOLE narrowed set down to compose, because
|
|
166
|
+
* {@link expandCollectionEntries} does its own windowing off the block's
|
|
167
|
+
* props — a Collection entries block may carry its own `perPage`, its own
|
|
168
|
+
* pinned `page`, a `firstPageOnly` switch and its own filters, none of which
|
|
169
|
+
* this function can see. Slicing before compose would double-window and empty
|
|
170
|
+
* every page after the first.
|
|
171
|
+
*
|
|
172
|
+
* Everything the loader serializes into page PROPS is downstream of compose,
|
|
173
|
+
* and there the full set is not merely wasted bytes — it is a false
|
|
174
|
+
* statement. The `ItemList` JSON-LD numbers its items
|
|
175
|
+
* `(page - 1) * perPage + index + 1` on the premise that `entries` is the
|
|
176
|
+
* window; handed all one hundred, `/changelog/page/7` published
|
|
177
|
+
* `numberOfItems: 100` and positions 61 through 160, most of them past the
|
|
178
|
+
* end of the collection. The legacy fallback renderer read it the same way
|
|
179
|
+
* and rendered the whole list on every page.
|
|
180
|
+
*
|
|
181
|
+
* So: the loader keeps the full set until compose has had it, and narrows
|
|
182
|
+
* HERE on the way into props, where `pagination` is the route's own answer to
|
|
183
|
+
* which page this is.
|
|
184
|
+
*
|
|
185
|
+
* An unpaginated listing (an entry route, the RSS feed) passes no pagination
|
|
186
|
+
* and gets its entries back untouched.
|
|
187
|
+
*/
|
|
188
|
+
export declare function collectionEntriesPageWindow<T>(entries: readonly T[], pagination: {
|
|
189
|
+
page?: number;
|
|
190
|
+
perPage?: number;
|
|
191
|
+
windowStart?: number;
|
|
192
|
+
} | null | undefined): T[];
|
|
143
193
|
/** Default/most related posts a Related posts block renders (AGL-582). */
|
|
144
194
|
export declare const COLLECTION_RELATED_DEFAULT_LIMIT = 3;
|
|
145
195
|
export declare const COLLECTION_RELATED_MAX = 12;
|
|
@@ -334,6 +384,17 @@ export interface CollectionEntriesSource {
|
|
|
334
384
|
* wins: that is a deliberately pinned window, not a paginated list.
|
|
335
385
|
*/
|
|
336
386
|
page?: number;
|
|
387
|
+
/**
|
|
388
|
+
* The absolute index `entries[0]` holds in the collection's own order
|
|
389
|
+
* (AGL-3213). Zero everywhere except a listing page past the cached read's
|
|
390
|
+
* bound, which is served by a window read for that page alone.
|
|
391
|
+
*
|
|
392
|
+
* Without it a windowed source is silently empty: every consumer here
|
|
393
|
+
* slices `[(page - 1) * perPage, …)` on the premise that `entries` starts
|
|
394
|
+
* at the beginning of the collection, so page 11 of a windowed ten-entry
|
|
395
|
+
* source slices from 100 and renders nothing.
|
|
396
|
+
*/
|
|
397
|
+
windowStart?: number;
|
|
337
398
|
}
|
|
338
399
|
/**
|
|
339
400
|
* Display name of an entry's category (AGL-582): the `categoryId` lookup
|
|
@@ -432,16 +493,25 @@ export declare function collectionListUrl(options: {
|
|
|
432
493
|
collectionSlug: string;
|
|
433
494
|
categorySlug?: string | null;
|
|
434
495
|
page?: number | null;
|
|
496
|
+
/** Continue after this entry's document id — the older direction. */
|
|
497
|
+
after?: string | null;
|
|
498
|
+
/** Continue before this entry's document id — the newer direction. */
|
|
499
|
+
before?: string | null;
|
|
435
500
|
}): string;
|
|
436
501
|
/** Where a listing's pager can go from here (AGL-1386). */
|
|
437
502
|
export interface CollectionPaginationLinks {
|
|
438
503
|
/** 1-based current page; 1 when the listing is unpaginated. */
|
|
439
504
|
page: number;
|
|
440
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* Total pages in the CURRENT set.
|
|
507
|
+
*
|
|
508
|
+
* @deprecated AGL-3219 — 0 when it cannot be known, which is any collection
|
|
509
|
+
* past one read's bound. See `CollectionPagination.totalPages`.
|
|
510
|
+
*/
|
|
441
511
|
totalPages: number;
|
|
442
512
|
/** The previous page's URL, or `''` on the first page. */
|
|
443
513
|
prevUrl: string;
|
|
444
|
-
/** The next page's URL, or `''`
|
|
514
|
+
/** The next page's URL, or `''` when nothing older follows. */
|
|
445
515
|
nextUrl: string;
|
|
446
516
|
}
|
|
447
517
|
/**
|
|
@@ -468,8 +538,26 @@ export declare function collectionPaginationLinks(options: {
|
|
|
468
538
|
categorySlug?: string | null;
|
|
469
539
|
/** 1-based current page; anything unusable reads as page 1. */
|
|
470
540
|
page?: number | null;
|
|
471
|
-
/**
|
|
541
|
+
/**
|
|
542
|
+
* Total pages.
|
|
543
|
+
*
|
|
544
|
+
* @deprecated AGL-3219 — no longer consulted for whether a next link
|
|
545
|
+
* exists; {@link collectionPaginationLinks} asks `nextCursor` instead.
|
|
546
|
+
*/
|
|
472
547
|
totalPages?: number | null;
|
|
548
|
+
/**
|
|
549
|
+
* The document id the older page continues from, or `''` when there is no
|
|
550
|
+
* older page (AGL-3219).
|
|
551
|
+
*
|
|
552
|
+
* This is the ONLY thing that decides whether `nextUrl` is a link. It comes
|
|
553
|
+
* from a `limit(perPage + 1)` probe, so an empty string means the read
|
|
554
|
+
* looked for an eleventh entry and there was not one — which is a fact
|
|
555
|
+
* about the collection, unlike a total that two differently-cached reads
|
|
556
|
+
* could disagree about.
|
|
557
|
+
*/
|
|
558
|
+
nextCursor?: string | null;
|
|
559
|
+
/** The document id the newer page continues before, or `''`. */
|
|
560
|
+
prevCursor?: string | null;
|
|
473
561
|
}): CollectionPaginationLinks;
|
|
474
562
|
/** A content-collection path resolved to what it addresses (AGL-1321). */
|
|
475
563
|
export interface CollectionRoute {
|
|
@@ -480,6 +568,10 @@ export interface CollectionRoute {
|
|
|
480
568
|
categorySlug?: string;
|
|
481
569
|
/** 1-based list page; always 1 for entry routes. */
|
|
482
570
|
page: number;
|
|
571
|
+
/** Set only for `/{collection}/after/{cursor}` (AGL-3219). */
|
|
572
|
+
after?: string;
|
|
573
|
+
/** Set only for `/{collection}/before/{cursor}` (AGL-3219). */
|
|
574
|
+
before?: string;
|
|
483
575
|
}
|
|
484
576
|
/**
|
|
485
577
|
* Parses a non-screen path against the content-collection route shapes
|
|
@@ -72,6 +72,22 @@ export * from "./collection-entry-date.js";
|
|
|
72
72
|
* slugged `page` or `category` keeps serving at `/{collection}/{entry}`.
|
|
73
73
|
*/ export const COLLECTION_PAGE_ROUTE_SEGMENT = 'page';
|
|
74
74
|
export const COLLECTION_CATEGORY_ROUTE_SEGMENT = 'category';
|
|
75
|
+
/**
|
|
76
|
+
* `/{collection}/after/{cursor}` — the older page (AGL-3219).
|
|
77
|
+
*
|
|
78
|
+
* ## Why a PATH segment and not `?after=`
|
|
79
|
+
*
|
|
80
|
+
* The tenant catch-all is statically rendered and ISR-cached with the URL
|
|
81
|
+
* PATH as its key (`revalidate = 3600`, AGL-1152). A query string is not part
|
|
82
|
+
* of that key, so every cursor address would share ONE cache entry and serve
|
|
83
|
+
* whichever of them rendered first to everybody — the same reason a routed
|
|
84
|
+
* category is `/{collection}/category/{slug}` rather than `?category=`. And
|
|
85
|
+
* reading `searchParams` at all opts the whole catch-all out of static
|
|
86
|
+
* rendering, which is the opposite of what that page is for.
|
|
87
|
+
*
|
|
88
|
+
* A path segment is cached per address, crawlable, and costs nothing.
|
|
89
|
+
*/ export const COLLECTION_AFTER_ROUTE_SEGMENT = 'after';
|
|
90
|
+
/** `/{collection}/before/{cursor}` — the newer page (AGL-3219). */ export const COLLECTION_BEFORE_ROUTE_SEGMENT = 'before';
|
|
75
91
|
/** Namespaces cloned template ids per container/entry (cf. `rep__`). */ export const COLLECTION_ENTRIES_NODE_ID_PREFIX = 'centry__';
|
|
76
92
|
/** Hard bound on entries a single block renders, before `entriesLimit`. */ export const COLLECTION_ENTRIES_MAX = 100;
|
|
77
93
|
/**
|
|
@@ -133,6 +149,48 @@ export const COLLECTION_CATEGORY_ROUTE_SEGMENT = 'category';
|
|
|
133
149
|
if (perPage <= 0) return 1;
|
|
134
150
|
return Math.max(1, Math.ceil(total / perPage));
|
|
135
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* The slice of a listing's entries that the page named by `pagination`
|
|
154
|
+
* actually shows (AGL-3213).
|
|
155
|
+
*
|
|
156
|
+
* The loader hands the WHOLE narrowed set down to compose, because
|
|
157
|
+
* {@link expandCollectionEntries} does its own windowing off the block's
|
|
158
|
+
* props — a Collection entries block may carry its own `perPage`, its own
|
|
159
|
+
* pinned `page`, a `firstPageOnly` switch and its own filters, none of which
|
|
160
|
+
* this function can see. Slicing before compose would double-window and empty
|
|
161
|
+
* every page after the first.
|
|
162
|
+
*
|
|
163
|
+
* Everything the loader serializes into page PROPS is downstream of compose,
|
|
164
|
+
* and there the full set is not merely wasted bytes — it is a false
|
|
165
|
+
* statement. The `ItemList` JSON-LD numbers its items
|
|
166
|
+
* `(page - 1) * perPage + index + 1` on the premise that `entries` is the
|
|
167
|
+
* window; handed all one hundred, `/changelog/page/7` published
|
|
168
|
+
* `numberOfItems: 100` and positions 61 through 160, most of them past the
|
|
169
|
+
* end of the collection. The legacy fallback renderer read it the same way
|
|
170
|
+
* and rendered the whole list on every page.
|
|
171
|
+
*
|
|
172
|
+
* So: the loader keeps the full set until compose has had it, and narrows
|
|
173
|
+
* HERE on the way into props, where `pagination` is the route's own answer to
|
|
174
|
+
* which page this is.
|
|
175
|
+
*
|
|
176
|
+
* An unpaginated listing (an entry route, the RSS feed) passes no pagination
|
|
177
|
+
* and gets its entries back untouched.
|
|
178
|
+
*/ export function collectionEntriesPageWindow(entries, pagination) {
|
|
179
|
+
const perPage = Number(pagination == null ? void 0 : pagination.perPage);
|
|
180
|
+
if (!Number.isFinite(perPage) || perPage <= 0) return [
|
|
181
|
+
...entries
|
|
182
|
+
];
|
|
183
|
+
const pageRaw = Number(pagination == null ? void 0 : pagination.page);
|
|
184
|
+
const page = Number.isFinite(pageRaw) && pageRaw >= 1 ? Math.floor(pageRaw) : 1;
|
|
185
|
+
// `windowStart` is where `entries` begins in the collection's own order
|
|
186
|
+
// (AGL-3213). A listing past the cached read's bound is served by a window
|
|
187
|
+
// read for that page alone, and slicing it from the page's absolute offset
|
|
188
|
+
// would return nothing.
|
|
189
|
+
const startRaw = Number(pagination == null ? void 0 : pagination.windowStart);
|
|
190
|
+
const windowStart = Number.isFinite(startRaw) && startRaw > 0 ? Math.floor(startRaw) : 0;
|
|
191
|
+
const start = Math.max(0, (page - 1) * Math.floor(perPage) - windowStart);
|
|
192
|
+
return entries.slice(start, start + Math.floor(perPage));
|
|
193
|
+
}
|
|
136
194
|
/** Default/most related posts a Related posts block renders (AGL-582). */ export const COLLECTION_RELATED_DEFAULT_LIMIT = 3;
|
|
137
195
|
export const COLLECTION_RELATED_MAX = 12;
|
|
138
196
|
/** Most categories a collection's taxonomy holds (AGL-582). */ export const COLLECTION_CATEGORIES_MAX = 50;
|
|
@@ -404,7 +462,23 @@ categories) {
|
|
|
404
462
|
* a second address for the page that already exists.
|
|
405
463
|
*/ export function collectionListUrl(options) {
|
|
406
464
|
const scoped = options.categorySlug ? `/${options.collectionSlug}/${COLLECTION_CATEGORY_ROUTE_SEGMENT}/` + collectionCategorySlug(options.categorySlug) : `/${options.collectionSlug}`;
|
|
407
|
-
|
|
465
|
+
/*
|
|
466
|
+
* A cursor address (AGL-3219) wins over a page number when both are given,
|
|
467
|
+
* because it is the one that survives a publish at the head of the list.
|
|
468
|
+
*/ const cursor = (options.before || options.after || '').trim();
|
|
469
|
+
if (cursor) {
|
|
470
|
+
const segment = options.before ? COLLECTION_BEFORE_ROUTE_SEGMENT : COLLECTION_AFTER_ROUTE_SEGMENT;
|
|
471
|
+
return `${scoped}/${segment}/${encodeURIComponent(cursor)}`;
|
|
472
|
+
}
|
|
473
|
+
/*
|
|
474
|
+
* `/{collection}/page/{n}` is still MINTED for a collection that fits
|
|
475
|
+
* inside one read, where it is exactly as stable as it ever was: every one
|
|
476
|
+
* of those pages is sliced out of a single cached snapshot, so no publish
|
|
477
|
+
* can move one page's entries relative to another's. It is past the bound —
|
|
478
|
+
* where the head and the tail were two different reads of a moving list —
|
|
479
|
+
* that a number stopped being an address, and there the pager mints
|
|
480
|
+
* cursors instead.
|
|
481
|
+
*/ const page = Number(options.page);
|
|
408
482
|
return Number.isFinite(page) && page > 1 ? `${scoped}/${COLLECTION_PAGE_ROUTE_SEGMENT}/${Math.floor(page)}` : scoped;
|
|
409
483
|
}
|
|
410
484
|
/**
|
|
@@ -426,24 +500,59 @@ categories) {
|
|
|
426
500
|
* template binds. A second derivation is how a fallback and a template start
|
|
427
501
|
* disagreeing about which page they are on.
|
|
428
502
|
*/ export function collectionPaginationLinks(options) {
|
|
503
|
+
var _options_nextCursor, _options_prevCursor;
|
|
429
504
|
const positive = (value)=>{
|
|
430
505
|
const parsed = Math.floor(Number(value));
|
|
431
506
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
|
|
432
507
|
};
|
|
433
508
|
const page = positive(options.page);
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
509
|
+
const totalPagesRaw = Math.floor(Number(options.totalPages));
|
|
510
|
+
/*
|
|
511
|
+
* Zero means UNKNOWN, and only a caller that deals in cursors can mean it
|
|
512
|
+
* (AGL-3219).
|
|
513
|
+
*
|
|
514
|
+
* A listing past the bound passes cursors and no total, and 0 is how it
|
|
515
|
+
* says the number cannot be had. A caller that passes neither — an entry
|
|
516
|
+
* route, which has no listing at all — is the inert case the tokens have
|
|
517
|
+
* always answered with "page 1 of 1", and it keeps that answer rather than
|
|
518
|
+
* telling an entry template it is on page 1 of 0.
|
|
519
|
+
*/ const knowsCursors = options.nextCursor !== undefined || options.prevCursor !== undefined;
|
|
520
|
+
const totalPages = Number.isFinite(totalPagesRaw) && totalPagesRaw > 0 ? totalPagesRaw : knowsCursors ? 0 : 1;
|
|
521
|
+
const scope = _extends({
|
|
522
|
+
collectionSlug: options.collectionSlug
|
|
523
|
+
}, options.categorySlug ? {
|
|
524
|
+
categorySlug: options.categorySlug
|
|
525
|
+
} : {});
|
|
526
|
+
const next = ((_options_nextCursor = options.nextCursor) != null ? _options_nextCursor : '').trim();
|
|
527
|
+
const prev = ((_options_prevCursor = options.prevCursor) != null ? _options_prevCursor : '').trim();
|
|
528
|
+
/*
|
|
529
|
+
* Numbered while the whole set fits inside ONE read, cursors once it does
|
|
530
|
+
* not (AGL-3219).
|
|
531
|
+
*
|
|
532
|
+
* A total is only ever stated for a collection the shared read holds
|
|
533
|
+
* entirely, and there every page is sliced out of that one cached snapshot:
|
|
534
|
+
* no publish can move one page's entries relative to another's, so a number
|
|
535
|
+
* is a perfectly good address and stays the one readers and crawlers
|
|
536
|
+
* already have. Past the bound the head and the tail were two reads of a
|
|
537
|
+
* moving list, a number stopped naming the same ten entries from one render
|
|
538
|
+
* to the next, and the pager mints the cursor its own page proves instead.
|
|
539
|
+
*/ const numbered = totalPages > 0;
|
|
540
|
+
const numberedHref = (n)=>collectionListUrl(_extends({}, scope, {
|
|
440
541
|
page: n
|
|
441
542
|
}));
|
|
442
543
|
return {
|
|
443
544
|
page,
|
|
444
545
|
totalPages,
|
|
445
|
-
|
|
446
|
-
|
|
546
|
+
/*
|
|
547
|
+
* Page 2 going back lands on the bare listing either way: it is the head
|
|
548
|
+
* of the collection, and `/{collection}` is its address — there is no
|
|
549
|
+
* cursor for "the beginning".
|
|
550
|
+
*/ prevUrl: page === 2 ? collectionListUrl(scope) : page > 2 ? numbered ? numberedHref(page - 1) : prev ? collectionListUrl(_extends({}, scope, {
|
|
551
|
+
before: prev
|
|
552
|
+
})) : '' : '',
|
|
553
|
+
nextUrl: numbered ? page < totalPages ? numberedHref(page + 1) : '' : next ? collectionListUrl(_extends({}, scope, {
|
|
554
|
+
after: next
|
|
555
|
+
})) : ''
|
|
447
556
|
};
|
|
448
557
|
}
|
|
449
558
|
const POSITIVE_INTEGER = /^[1-9]\d*$/;
|
|
@@ -483,6 +592,28 @@ const POSITIVE_INTEGER = /^[1-9]\d*$/;
|
|
|
483
592
|
page: Number(rest[1])
|
|
484
593
|
} : null;
|
|
485
594
|
}
|
|
595
|
+
/*
|
|
596
|
+
* `/{collection}/after/{cursor}` and its `before` twin (AGL-3219).
|
|
597
|
+
*
|
|
598
|
+
* Checked BEFORE the category shape and after the one-segment entry shape,
|
|
599
|
+
* so an entry whose slug happens to be `after` still answers at
|
|
600
|
+
* `/{collection}/after` — that is one segment, and this is two.
|
|
601
|
+
*
|
|
602
|
+
* `page` stays 1: nothing counts a cursor address, and it is the display
|
|
603
|
+
* label rather than a position anything reads back.
|
|
604
|
+
*/ if (rest.length === 2 && (rest[0] === COLLECTION_AFTER_ROUTE_SEGMENT || rest[0] === COLLECTION_BEFORE_ROUTE_SEGMENT) && rest[1]) {
|
|
605
|
+
const cursor = decodeURIComponent(rest[1]).trim();
|
|
606
|
+
if (!cursor) return null;
|
|
607
|
+
return rest[0] === COLLECTION_AFTER_ROUTE_SEGMENT ? {
|
|
608
|
+
collectionSlug,
|
|
609
|
+
after: cursor,
|
|
610
|
+
page: 1
|
|
611
|
+
} : {
|
|
612
|
+
collectionSlug,
|
|
613
|
+
before: cursor,
|
|
614
|
+
page: 1
|
|
615
|
+
};
|
|
616
|
+
}
|
|
486
617
|
if (rest.length === 2 && rest[0] === COLLECTION_CATEGORY_ROUTE_SEGMENT) {
|
|
487
618
|
const categorySlug = collectionCategorySlug(rest[1]);
|
|
488
619
|
return categorySlug ? {
|
|
@@ -779,7 +910,12 @@ const POSITIVE_INTEGER = /^[1-9]\d*$/;
|
|
|
779
910
|
// counting it, because the count cannot see a read that came back full
|
|
780
911
|
// and was then thinned by the liveness gate.
|
|
781
912
|
const sourceCapped = collectionSourceIsBounded(source);
|
|
782
|
-
|
|
913
|
+
// Where this source's first entry sits in the collection (AGL-3213):
|
|
914
|
+
// zero for a whole read, and the page's own offset for a windowed one.
|
|
915
|
+
const windowStartRaw = Number(source.windowStart);
|
|
916
|
+
const windowStart = Number.isFinite(windowStartRaw) && windowStartRaw > 0 ? Math.floor(windowStartRaw) : 0;
|
|
917
|
+
const pageStart = perPage ? Math.max(0, (page - 1) * perPage - windowStart) : 0;
|
|
918
|
+
const windowed = suppressedBeyondFirstPage ? [] : perPage ? filtered.slice(pageStart, pageStart + perPage) : filtered.slice(0, limit);
|
|
783
919
|
const childIds = [];
|
|
784
920
|
windowed.forEach((entry, index)=>{
|
|
785
921
|
const prefix = `${COLLECTION_ENTRIES_NODE_ID_PREFIX}${containerId}__${index}__`;
|