@proveanything/smartlinks 2.0.37 → 2.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/functions.d.ts +2 -2
- package/dist/api/functions.js +7 -3
- package/dist/docs/API_SUMMARY.md +78 -11
- package/dist/docs/headless-providers.md +112 -0
- package/dist/docs/site-seo.md +68 -0
- package/dist/headless.d.ts +45 -0
- package/dist/headless.js +249 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/openapi.yaml +159 -0
- package/dist/seo.d.ts +85 -0
- package/dist/seo.js +172 -0
- package/dist/site.d.ts +10 -0
- package/dist/site.js +18 -0
- package/dist/types/appManifest.d.ts +11 -0
- package/dist/types/appManifest.js +0 -1
- package/dist/types/headless.d.ts +96 -0
- package/dist/types/headless.js +16 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/docs/API_SUMMARY.md +78 -11
- package/docs/headless-providers.md +112 -0
- package/docs/site-seo.md +68 -0
- package/openapi.yaml +159 -0
- package/package.json +4 -2
- package/scripts/headless-check.mjs +87 -0
package/dist/headless.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// headless — validate an app's headless-provider declaration (manifest `data` + `headless`).
|
|
3
|
+
//
|
|
4
|
+
// SL.headless.validate(manifest, { samples }) → { ok, errors, warnings, recipes }
|
|
5
|
+
//
|
|
6
|
+
// Pure (no network): the `smartlinks-headless` CLI and Forge both call it. `samples` are real items
|
|
7
|
+
// (each type's `data` zone JSON) — the check then reports fields the real data has but the
|
|
8
|
+
// declaration doesn't, and values that don't match their declared type.
|
|
9
|
+
// Spec: docs/headless-providers.md.
|
|
10
|
+
// =============================================================================
|
|
11
|
+
import { HEADLESS_CATEGORIES } from './types/headless.js';
|
|
12
|
+
const FIELD_TYPES = new Set([
|
|
13
|
+
'string', 'text', 'richtext', 'markdown', 'number', 'boolean', 'date', 'datetime', 'enum', 'url',
|
|
14
|
+
'image', 'file', 'ref', 'string[]', 'ref[]', 'json',
|
|
15
|
+
]);
|
|
16
|
+
const TEXT_TYPES = new Set(['string', 'text', 'richtext', 'markdown']);
|
|
17
|
+
const PLATFORM_REFS = new Set(['product', 'contact', 'proof']);
|
|
18
|
+
const SEO_HELPERS = new Set(['faqPage', 'product', 'article', 'breadcrumbs', 'organization', 'localBusiness']);
|
|
19
|
+
const STORAGE_KINDS = new Set(['record', 'case', 'thread', 'config']);
|
|
20
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
21
|
+
/**
|
|
22
|
+
* What a type's read calls return, and where one item's declared fields live — fixed by the storage
|
|
23
|
+
* kind (see app-objects.md "Paginated List Responses"). Sites must read items from `response.data`.
|
|
24
|
+
*/
|
|
25
|
+
export function responseShape(type) {
|
|
26
|
+
const s = type.storage;
|
|
27
|
+
const first = Object.keys(type.fields || {})[0] || 'field';
|
|
28
|
+
const paged = (name) => `list → { data: ${name}[], pagination: { total, limit, offset, hasMore } }. The items are in response.data (not response.items / response.records); page with offset/limit while pagination.hasMore. get → one ${name}.`;
|
|
29
|
+
switch (s && s.kind) {
|
|
30
|
+
case 'record':
|
|
31
|
+
return { returns: paged('AppRecord'), item: `Each record's declared fields are in record.data (e.g. record.data.${first}); record.id, record.productId, record.status, record.createdAt are top-level.` };
|
|
32
|
+
case 'case':
|
|
33
|
+
return { returns: paged('AppCase'), item: `Each case's declared fields are in case.data (e.g. case.data.${first}); id, status, category and dates are top-level.` };
|
|
34
|
+
case 'thread':
|
|
35
|
+
return { returns: paged('AppThread'), item: `Each thread's declared fields are in thread.data (e.g. thread.data.${first}); replies are in thread.replies.` };
|
|
36
|
+
case 'config': {
|
|
37
|
+
const key = s.key;
|
|
38
|
+
return { returns: "The app's configuration object for the collection.", item: key ? `The items are in config.${key}; each item's fields are its declared fields.` : 'The declared fields are top-level properties of the config object.' };
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
return { returns: '(unknown storage)', item: '' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The standard SDK read calls for a type, from how it's stored. `appId` / `collectionId` are the caller's variables. */
|
|
45
|
+
export function standardRecipe(type) {
|
|
46
|
+
const s = type.storage;
|
|
47
|
+
switch (s && s.kind) {
|
|
48
|
+
case 'record':
|
|
49
|
+
return {
|
|
50
|
+
list: `SL.app.records.list(collectionId, appId, { recordType: '${s.recordType}', limit: 50 })`,
|
|
51
|
+
get: 'SL.app.records.get(collectionId, appId, recordId)',
|
|
52
|
+
};
|
|
53
|
+
case 'case':
|
|
54
|
+
return { list: 'SL.app.cases.list(collectionId, appId, { limit: 50 })', get: 'SL.app.cases.get(collectionId, appId, caseId)' };
|
|
55
|
+
case 'thread':
|
|
56
|
+
return { list: 'SL.app.threads.list(collectionId, appId, { limit: 50 })', get: 'SL.app.threads.get(collectionId, appId, threadId)' };
|
|
57
|
+
case 'config':
|
|
58
|
+
return { list: `SL.appConfiguration.getConfig({ collectionId, appId })${s.key ? ` // → config.${s.key}` : ''}` };
|
|
59
|
+
default:
|
|
60
|
+
return { list: '(unknown storage)' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function isPlaceholder(v) {
|
|
64
|
+
return typeof v === 'string' && /lorem ipsum|^(todo|tbd|example|test|foo|bar)$/i.test(v.trim());
|
|
65
|
+
}
|
|
66
|
+
/** Does a value fit a declared field? Returns a reason when it doesn't. */
|
|
67
|
+
function mismatch(field, value) {
|
|
68
|
+
if (value === null || value === undefined)
|
|
69
|
+
return null;
|
|
70
|
+
const t = field.type;
|
|
71
|
+
const isLocalizedObject = field.localized && typeof value === 'object' && !Array.isArray(value) &&
|
|
72
|
+
Object.values(value).every((x) => typeof x === 'string');
|
|
73
|
+
switch (t) {
|
|
74
|
+
case 'string':
|
|
75
|
+
case 'text':
|
|
76
|
+
case 'richtext':
|
|
77
|
+
case 'markdown':
|
|
78
|
+
return typeof value === 'string' || isLocalizedObject ? null : `expected ${t} (a string${field.localized ? ' or { lang: string }' : ''})`;
|
|
79
|
+
case 'url':
|
|
80
|
+
return typeof value === 'string' ? null : 'expected a URL string';
|
|
81
|
+
case 'number':
|
|
82
|
+
return typeof value === 'number' && Number.isFinite(value) ? null : 'expected a number';
|
|
83
|
+
case 'boolean':
|
|
84
|
+
return typeof value === 'boolean' ? null : 'expected true/false';
|
|
85
|
+
case 'date':
|
|
86
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value) ? null : 'expected an ISO date (YYYY-MM-DD)';
|
|
87
|
+
case 'datetime':
|
|
88
|
+
return typeof value === 'string' && !Number.isNaN(Date.parse(value)) ? null : 'expected an ISO date-time';
|
|
89
|
+
case 'enum':
|
|
90
|
+
return (field.options || []).includes(value) ? null : `expected one of ${(field.options || []).join(', ')}`;
|
|
91
|
+
case 'image':
|
|
92
|
+
case 'file':
|
|
93
|
+
return typeof value === 'string' || (typeof value === 'object' && !Array.isArray(value) && typeof value.url === 'string')
|
|
94
|
+
? null : `expected a URL string or { url }`;
|
|
95
|
+
case 'ref':
|
|
96
|
+
return typeof value === 'string' ? null : 'expected an id string';
|
|
97
|
+
case 'string[]':
|
|
98
|
+
case 'ref[]':
|
|
99
|
+
return Array.isArray(value) && value.every((x) => typeof x === 'string') ? null : 'expected an array of strings';
|
|
100
|
+
default:
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function checkItems(label, typeId, type, items, out) {
|
|
105
|
+
const fields = type.fields || {};
|
|
106
|
+
const undeclared = new Map();
|
|
107
|
+
items.forEach((item, i) => {
|
|
108
|
+
const at = `data.types.${typeId}.${label === 'example' ? 'examples' : 'realData'}[${i}]`;
|
|
109
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
110
|
+
out.errors.push({ path: at, message: `${label} must be an object (the item's data JSON)` });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
114
|
+
if ((field.zone || 'data') !== 'data')
|
|
115
|
+
continue; // examples / samples are the data zone
|
|
116
|
+
const value = item[key];
|
|
117
|
+
if (field.required && (value === undefined || value === null || value === '')) {
|
|
118
|
+
;
|
|
119
|
+
(label === 'example' ? out.errors : out.warnings).push({ path: `${at}.${key}`, message: `required field "${key}" is missing` });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const why = mismatch(field, value);
|
|
123
|
+
if (why)
|
|
124
|
+
(label === 'example' ? out.errors : out.warnings).push({ path: `${at}.${key}`, message: `"${key}": ${why}` });
|
|
125
|
+
if (label === 'example' && isPlaceholder(value))
|
|
126
|
+
out.warnings.push({ path: `${at}.${key}`, message: 'looks like placeholder text — use realistic content' });
|
|
127
|
+
}
|
|
128
|
+
for (const key of Object.keys(item))
|
|
129
|
+
if (!(key in fields))
|
|
130
|
+
undeclared.set(key, (undeclared.get(key) || 0) + 1);
|
|
131
|
+
});
|
|
132
|
+
for (const [key, n] of undeclared) {
|
|
133
|
+
out.warnings.push({
|
|
134
|
+
path: `data.types.${typeId}.fields`,
|
|
135
|
+
message: label === 'example'
|
|
136
|
+
? `example uses "${key}", which isn't a declared field`
|
|
137
|
+
: `real data has "${key}" (in ${n} of ${items.length} items) but it isn't declared — declare it, or confirm it's internal`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Validate a manifest's `data` + `headless` blocks. */
|
|
142
|
+
export function validate(manifest, opts = {}) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
const warnings = [];
|
|
145
|
+
const recipes = {};
|
|
146
|
+
const err = (path, message) => errors.push({ path, message });
|
|
147
|
+
const warn = (path, message) => warnings.push({ path, message });
|
|
148
|
+
const data = manifest && manifest.data;
|
|
149
|
+
const headless = manifest && manifest.headless;
|
|
150
|
+
// ---- data
|
|
151
|
+
if (!data) {
|
|
152
|
+
err('data', headless ? 'a headless app needs a `data` block declaring its types' : 'no `data` block');
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (!data.schemaVersion || !SEMVER.test(data.schemaVersion))
|
|
156
|
+
err('data.schemaVersion', 'set a semver, e.g. "1.0.0"');
|
|
157
|
+
const types = data.types || {};
|
|
158
|
+
if (!Object.keys(types).length)
|
|
159
|
+
err('data.types', 'declare at least one type');
|
|
160
|
+
const typeIds = new Set(Object.keys(types));
|
|
161
|
+
for (const [typeId, type] of Object.entries(types)) {
|
|
162
|
+
const at = `data.types.${typeId}`;
|
|
163
|
+
if (!/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$/.test(typeId))
|
|
164
|
+
err(at, 'type ids are lowercase, dot-separated (e.g. "faq.item")');
|
|
165
|
+
if (!type.description || type.description.trim().length < 10)
|
|
166
|
+
err(`${at}.description`, 'say what one item is, in a sentence');
|
|
167
|
+
const kind = type.storage && type.storage.kind;
|
|
168
|
+
if (!STORAGE_KINDS.has(kind))
|
|
169
|
+
err(`${at}.storage`, 'storage.kind must be record, case, thread or config');
|
|
170
|
+
if (kind === 'record' && !type.storage.recordType)
|
|
171
|
+
err(`${at}.storage.recordType`, 'record storage needs the recordType the app writes');
|
|
172
|
+
if (type.visibility && !['public', 'owner', 'admin'].includes(type.visibility))
|
|
173
|
+
err(`${at}.visibility`, 'public, owner or admin');
|
|
174
|
+
const fields = type.fields || {};
|
|
175
|
+
if (!Object.keys(fields).length)
|
|
176
|
+
err(`${at}.fields`, 'declare the fields');
|
|
177
|
+
for (const [key, f] of Object.entries(fields)) {
|
|
178
|
+
const fat = `${at}.fields.${key}`;
|
|
179
|
+
if (!f || !FIELD_TYPES.has(f.type)) {
|
|
180
|
+
err(fat, `unknown type "${f && f.type}" (one of ${[...FIELD_TYPES].join(', ')})`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (f.type === 'enum' && !(f.options && f.options.length))
|
|
184
|
+
err(fat, 'an enum needs options');
|
|
185
|
+
if ((f.type === 'ref' || f.type === 'ref[]') && !(f.to && (typeIds.has(f.to) || PLATFORM_REFS.has(f.to)))) {
|
|
186
|
+
err(fat, `"to" must name a declared type or product/contact/proof`);
|
|
187
|
+
}
|
|
188
|
+
if (f.localized && !TEXT_TYPES.has(f.type))
|
|
189
|
+
warn(fat, 'only text fields can be localized');
|
|
190
|
+
if (f.public && f.zone && f.zone !== 'data')
|
|
191
|
+
err(fat, `a ${f.zone}-zone field can't be public — only the data zone is readable publicly`);
|
|
192
|
+
}
|
|
193
|
+
for (const k of (type.listing && type.listing.sort) || [])
|
|
194
|
+
if (!(k.replace(/^-/, '') in fields))
|
|
195
|
+
err(`${at}.listing.sort`, `"${k}" isn't a field`);
|
|
196
|
+
for (const k of (type.listing && type.listing.filters) || [])
|
|
197
|
+
if (!(k in fields))
|
|
198
|
+
err(`${at}.listing.filters`, `"${k}" isn't a field`);
|
|
199
|
+
if (type.examples)
|
|
200
|
+
checkItems('example', typeId, type, type.examples, { errors, warnings });
|
|
201
|
+
const samples = opts.samples && opts.samples[typeId];
|
|
202
|
+
if (samples && samples.length)
|
|
203
|
+
checkItems('real item', typeId, type, samples, { errors, warnings });
|
|
204
|
+
const std = standardRecipe(type);
|
|
205
|
+
const get = (type.read && type.read.get) || std.get;
|
|
206
|
+
recipes[typeId] = Object.assign(Object.assign({ list: (type.read && type.read.list) || std.list }, (get ? { get } : {})), responseShape(type));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ---- headless
|
|
210
|
+
if (headless) {
|
|
211
|
+
if (!headless.purpose || headless.purpose.trim().length < 40) {
|
|
212
|
+
err('headless.purpose', 'explain what content this holds and when a site should use it (a few sentences)');
|
|
213
|
+
}
|
|
214
|
+
const cats = headless.categories || [];
|
|
215
|
+
if (!cats.length)
|
|
216
|
+
err('headless.categories', `pick at least one: ${HEADLESS_CATEGORIES.join(', ')}`);
|
|
217
|
+
for (const c of cats)
|
|
218
|
+
if (!HEADLESS_CATEGORIES.includes(c))
|
|
219
|
+
err('headless.categories', `"${c}" isn't a category (${HEADLESS_CATEGORIES.join(', ')})`);
|
|
220
|
+
if (!headless.editedIn || !headless.editedIn.label)
|
|
221
|
+
err('headless.editedIn.label', 'say where the business edits this content (the app admin screen)');
|
|
222
|
+
for (const s of (headless.render && headless.render.seo) || [])
|
|
223
|
+
if (!SEO_HELPERS.has(s))
|
|
224
|
+
err('headless.render.seo', `"${s}" isn't an SEO helper (${[...SEO_HELPERS].join(', ')})`);
|
|
225
|
+
const types = (data && data.types) || {};
|
|
226
|
+
const primary = headless.primaryTypes || [];
|
|
227
|
+
if (!primary.length)
|
|
228
|
+
err('headless.primaryTypes', 'name the type(s) a site renders');
|
|
229
|
+
for (const id of primary) {
|
|
230
|
+
const type = types[id];
|
|
231
|
+
if (!type) {
|
|
232
|
+
err('headless.primaryTypes', `"${id}" isn't a declared type`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (type.visibility === 'admin')
|
|
236
|
+
err(`data.types.${id}.visibility`, 'a primary type must be readable by visitors (visibility public)');
|
|
237
|
+
const publicFields = Object.entries(type.fields || {}).filter(([, f]) => f.public);
|
|
238
|
+
if (!publicFields.length)
|
|
239
|
+
err(`data.types.${id}.fields`, 'mark the fields a visitor may read with "public": true');
|
|
240
|
+
if (!type.examples || !type.examples.length)
|
|
241
|
+
err(`data.types.${id}.examples`, 'add at least one realistic example item');
|
|
242
|
+
if (!type.read)
|
|
243
|
+
warn(`data.types.${id}.read`, 'no read recipe — the standard one for its storage is used (see recipes)');
|
|
244
|
+
}
|
|
245
|
+
if (!(headless.render && headless.render.guidance))
|
|
246
|
+
warn('headless.render.guidance', 'add a line on how a site should present this content');
|
|
247
|
+
}
|
|
248
|
+
return { ok: errors.length === 0, errors, warnings, recipes };
|
|
249
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export type { InvalidateCacheOptions } from "./http.js";
|
|
|
8
8
|
export * as cache from './cache.js';
|
|
9
9
|
export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder.js';
|
|
10
10
|
export * as utils from './utils/index.js';
|
|
11
|
+
export * as seo from './seo.js';
|
|
12
|
+
export * as site from './site.js';
|
|
13
|
+
export * as headless from './headless.js';
|
|
11
14
|
export { SHARED_DEPENDENCY_CONTRACT_VERSION, SHARED_DEPENDENCIES, SHARED_DEPENDENCY_SPECIFIERS, importMapPathFor, getHostSharedDependencies, } from './shared-dependencies.js';
|
|
12
15
|
export type { SharedDependency, HostSharedDependencies } from './shared-dependencies.js';
|
|
13
16
|
export { AI_TOOL_NAMES, BUILTIN_AI_TOOLS, getBuiltinAiTool } from './ai-tools.js';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,12 @@ export { cache_1 as cache };
|
|
|
13
13
|
export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder.js';
|
|
14
14
|
import * as utils_1 from './utils/index.js';
|
|
15
15
|
export { utils_1 as utils };
|
|
16
|
+
import * as seo_1 from './seo.js';
|
|
17
|
+
export { seo_1 as seo };
|
|
18
|
+
import * as site_1 from './site.js';
|
|
19
|
+
export { site_1 as site };
|
|
20
|
+
import * as headless_1 from './headless.js';
|
|
21
|
+
export { headless_1 as headless };
|
|
16
22
|
// Shared dependency contract (host↔app) — one source of truth for externalized deps
|
|
17
23
|
export { SHARED_DEPENDENCY_CONTRACT_VERSION, SHARED_DEPENDENCIES, SHARED_DEPENDENCY_SPECIFIERS, importMapPathFor, getHostSharedDependencies, } from './shared-dependencies.js';
|
|
18
24
|
// Built-in AI tool catalog (design-time discovery of the core agentic toolset)
|
package/dist/openapi.yaml
CHANGED
|
@@ -19459,6 +19459,10 @@ components:
|
|
|
19459
19459
|
$ref: "#/components/schemas/AppManifestExecutor"
|
|
19460
19460
|
functions:
|
|
19461
19461
|
$ref: "#/components/schemas/AppManifestFunctions"
|
|
19462
|
+
data:
|
|
19463
|
+
$ref: "#/components/schemas/AppDataDeclaration"
|
|
19464
|
+
headless:
|
|
19465
|
+
$ref: "#/components/schemas/AppHeadlessDeclaration"
|
|
19462
19466
|
required:
|
|
19463
19467
|
- name
|
|
19464
19468
|
- version
|
|
@@ -25155,6 +25159,161 @@ components:
|
|
|
25155
25159
|
type: boolean
|
|
25156
25160
|
FacetValueDefinition:
|
|
25157
25161
|
$ref: "#/components/schemas/FacetValue"
|
|
25162
|
+
AppDataField:
|
|
25163
|
+
type: object
|
|
25164
|
+
properties:
|
|
25165
|
+
type:
|
|
25166
|
+
$ref: "#/components/schemas/AppDataFieldType"
|
|
25167
|
+
label:
|
|
25168
|
+
type: string
|
|
25169
|
+
description:
|
|
25170
|
+
type: string
|
|
25171
|
+
required:
|
|
25172
|
+
type: boolean
|
|
25173
|
+
localized:
|
|
25174
|
+
type: boolean
|
|
25175
|
+
zone:
|
|
25176
|
+
type: string
|
|
25177
|
+
enum:
|
|
25178
|
+
- data
|
|
25179
|
+
- owner
|
|
25180
|
+
- admin
|
|
25181
|
+
public:
|
|
25182
|
+
type: boolean
|
|
25183
|
+
to:
|
|
25184
|
+
type: string
|
|
25185
|
+
options:
|
|
25186
|
+
type: array
|
|
25187
|
+
items:
|
|
25188
|
+
type: string
|
|
25189
|
+
required:
|
|
25190
|
+
- type
|
|
25191
|
+
AppDataType:
|
|
25192
|
+
type: object
|
|
25193
|
+
properties:
|
|
25194
|
+
description:
|
|
25195
|
+
type: string
|
|
25196
|
+
storage:
|
|
25197
|
+
$ref: "#/components/schemas/AppDataStorage"
|
|
25198
|
+
visibility:
|
|
25199
|
+
type: string
|
|
25200
|
+
enum:
|
|
25201
|
+
- public
|
|
25202
|
+
- owner
|
|
25203
|
+
- admin
|
|
25204
|
+
anchors:
|
|
25205
|
+
type: array
|
|
25206
|
+
items:
|
|
25207
|
+
type: string
|
|
25208
|
+
enum:
|
|
25209
|
+
- product
|
|
25210
|
+
- variant
|
|
25211
|
+
- batch
|
|
25212
|
+
- proof
|
|
25213
|
+
- contact
|
|
25214
|
+
fields:
|
|
25215
|
+
type: object
|
|
25216
|
+
additionalProperties:
|
|
25217
|
+
$ref: "#/components/schemas/AppDataField"
|
|
25218
|
+
listing:
|
|
25219
|
+
type: object
|
|
25220
|
+
additionalProperties: true
|
|
25221
|
+
examples:
|
|
25222
|
+
type: array
|
|
25223
|
+
items:
|
|
25224
|
+
type: object
|
|
25225
|
+
additionalProperties: true
|
|
25226
|
+
read:
|
|
25227
|
+
type: object
|
|
25228
|
+
additionalProperties: true
|
|
25229
|
+
since:
|
|
25230
|
+
type: string
|
|
25231
|
+
required:
|
|
25232
|
+
- description
|
|
25233
|
+
- storage
|
|
25234
|
+
- fields
|
|
25235
|
+
AppDataDeclaration:
|
|
25236
|
+
type: object
|
|
25237
|
+
properties:
|
|
25238
|
+
schemaVersion:
|
|
25239
|
+
type: string
|
|
25240
|
+
types:
|
|
25241
|
+
type: object
|
|
25242
|
+
additionalProperties:
|
|
25243
|
+
$ref: "#/components/schemas/AppDataType"
|
|
25244
|
+
required:
|
|
25245
|
+
- schemaVersion
|
|
25246
|
+
- types
|
|
25247
|
+
AppHeadlessDeclaration:
|
|
25248
|
+
type: object
|
|
25249
|
+
properties:
|
|
25250
|
+
purpose:
|
|
25251
|
+
type: string
|
|
25252
|
+
categories:
|
|
25253
|
+
type: array
|
|
25254
|
+
items:
|
|
25255
|
+
$ref: "#/components/schemas/HeadlessCategory"
|
|
25256
|
+
primaryTypes:
|
|
25257
|
+
type: array
|
|
25258
|
+
items:
|
|
25259
|
+
type: string
|
|
25260
|
+
editedIn:
|
|
25261
|
+
type: object
|
|
25262
|
+
additionalProperties: true
|
|
25263
|
+
render:
|
|
25264
|
+
type: object
|
|
25265
|
+
additionalProperties: true
|
|
25266
|
+
required:
|
|
25267
|
+
- purpose
|
|
25268
|
+
- categories
|
|
25269
|
+
- primaryTypes
|
|
25270
|
+
- editedIn
|
|
25271
|
+
AppDataFieldType:
|
|
25272
|
+
type: string
|
|
25273
|
+
enum:
|
|
25274
|
+
- string
|
|
25275
|
+
- text
|
|
25276
|
+
- richtext
|
|
25277
|
+
- markdown
|
|
25278
|
+
- number
|
|
25279
|
+
- boolean
|
|
25280
|
+
- date
|
|
25281
|
+
- datetime
|
|
25282
|
+
- enum
|
|
25283
|
+
- url
|
|
25284
|
+
- image
|
|
25285
|
+
- file
|
|
25286
|
+
- ref
|
|
25287
|
+
- "string[]"
|
|
25288
|
+
- "ref[]"
|
|
25289
|
+
- json
|
|
25290
|
+
AppDataStorage:
|
|
25291
|
+
type: object
|
|
25292
|
+
additionalProperties: true
|
|
25293
|
+
HeadlessCategory:
|
|
25294
|
+
type: string
|
|
25295
|
+
enum:
|
|
25296
|
+
- faq
|
|
25297
|
+
- media
|
|
25298
|
+
- pages
|
|
25299
|
+
- articles
|
|
25300
|
+
- catalog
|
|
25301
|
+
- events
|
|
25302
|
+
- locations
|
|
25303
|
+
- people
|
|
25304
|
+
- reviews
|
|
25305
|
+
- documents
|
|
25306
|
+
- forms
|
|
25307
|
+
- other
|
|
25308
|
+
HeadlessSeoHelper:
|
|
25309
|
+
type: string
|
|
25310
|
+
enum:
|
|
25311
|
+
- faqPage
|
|
25312
|
+
- product
|
|
25313
|
+
- article
|
|
25314
|
+
- breadcrumbs
|
|
25315
|
+
- organization
|
|
25316
|
+
- localBusiness
|
|
25158
25317
|
CachedData:
|
|
25159
25318
|
type: object
|
|
25160
25319
|
properties:
|
package/dist/seo.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export interface SeoHead {
|
|
2
|
+
title?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
/** Absolute image URL for link previews (og:image / twitter:image). */
|
|
5
|
+
image?: string;
|
|
6
|
+
/** Absolute canonical URL. Usually leave unset: the platform sends the site's canonical address. */
|
|
7
|
+
canonical?: string;
|
|
8
|
+
/** Keep this page out of search engines. */
|
|
9
|
+
noindex?: boolean;
|
|
10
|
+
/** og:type — 'website' (default), 'article' or 'product'. */
|
|
11
|
+
type?: 'website' | 'article' | 'product';
|
|
12
|
+
/** The site's name (og:site_name). */
|
|
13
|
+
siteName?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Set this route's head tags. Call on every route change; unset fields are removed (if we added them). */
|
|
16
|
+
export declare function head(h: SeoHead): void;
|
|
17
|
+
/**
|
|
18
|
+
* Place a JSON-LD structured-data block in <head>. `id` names the block, so calling again replaces it
|
|
19
|
+
* (e.g. per route); pass null to remove it. `<` is escaped so data can't close the script tag.
|
|
20
|
+
*/
|
|
21
|
+
export declare function jsonLd(id: string, data: object | object[] | null): void;
|
|
22
|
+
type Obj = Record<string, any>;
|
|
23
|
+
export interface ProductSchemaOptions {
|
|
24
|
+
/** The page's absolute URL. */
|
|
25
|
+
url?: string;
|
|
26
|
+
/** Brand name (defaults to none). */
|
|
27
|
+
brand?: string;
|
|
28
|
+
/** An offer, when the product is sold: price as a number or string, ISO currency, availability. */
|
|
29
|
+
offer?: {
|
|
30
|
+
price: number | string;
|
|
31
|
+
currency: string;
|
|
32
|
+
availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
|
|
33
|
+
url?: string;
|
|
34
|
+
};
|
|
35
|
+
/** Aggregate rating, when the site shows one. */
|
|
36
|
+
rating?: {
|
|
37
|
+
value: number;
|
|
38
|
+
count: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export declare const schema: {
|
|
42
|
+
/** schema.org Product from a SmartLinks product. */
|
|
43
|
+
product(product: Obj, opts?: ProductSchemaOptions): Obj;
|
|
44
|
+
/** schema.org FAQPage — answer-shaped content AI search can cite, and FAQ rich results. */
|
|
45
|
+
faqPage(items: Array<{
|
|
46
|
+
question: string;
|
|
47
|
+
answer: string;
|
|
48
|
+
}>): Obj;
|
|
49
|
+
/** schema.org Organization from a collection (the brand behind the site). */
|
|
50
|
+
organization(collection: Obj, opts?: {
|
|
51
|
+
url?: string;
|
|
52
|
+
sameAs?: string[];
|
|
53
|
+
}): Obj;
|
|
54
|
+
/** schema.org LocalBusiness — for a business with a place (shop, restaurant, studio). */
|
|
55
|
+
localBusiness(collection: Obj, opts?: {
|
|
56
|
+
url?: string;
|
|
57
|
+
telephone?: string;
|
|
58
|
+
address?: {
|
|
59
|
+
street?: string;
|
|
60
|
+
locality?: string;
|
|
61
|
+
region?: string;
|
|
62
|
+
postalCode?: string;
|
|
63
|
+
country?: string;
|
|
64
|
+
};
|
|
65
|
+
openingHours?: string[];
|
|
66
|
+
type?: string;
|
|
67
|
+
}): Obj;
|
|
68
|
+
/** schema.org BreadcrumbList from a trail of { name, url } (home first). */
|
|
69
|
+
breadcrumbs(trail: Array<{
|
|
70
|
+
name: string;
|
|
71
|
+
url: string;
|
|
72
|
+
}>): Obj;
|
|
73
|
+
/** schema.org Article (a blog post, news item, guide). Dates as ISO strings. */
|
|
74
|
+
article(a: {
|
|
75
|
+
headline: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
image?: string;
|
|
78
|
+
url?: string;
|
|
79
|
+
datePublished?: string;
|
|
80
|
+
dateModified?: string;
|
|
81
|
+
author?: string;
|
|
82
|
+
publisher?: string;
|
|
83
|
+
}): Obj;
|
|
84
|
+
};
|
|
85
|
+
export {};
|