@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/seo.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// SEO + GEO helpers for apps served as websites (framework-agnostic).
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// seo.head() per-route <title>, description, Open Graph / Twitter tags, canonical, robots
|
|
5
|
+
// seo.jsonLd() place a structured-data block (replaces the previous one with the same id)
|
|
6
|
+
// seo.schema.* schema.org builders from platform data (Product, FAQPage, Organization, …)
|
|
7
|
+
//
|
|
8
|
+
// They write plain DOM, so whatever pre-renders the page (the platform's renderer, a crawler that runs
|
|
9
|
+
// JavaScript) captures them. In SSR / no-document environments they're no-ops; the builders are pure.
|
|
10
|
+
// =============================================================================
|
|
11
|
+
const MARK = 'data-sl-seo';
|
|
12
|
+
function doc() {
|
|
13
|
+
return typeof document !== 'undefined' && document && document.head ? document : null;
|
|
14
|
+
}
|
|
15
|
+
function setMeta(d, attr, key, content) {
|
|
16
|
+
let el = d.head.querySelector(`meta[${attr}="${key}"]`);
|
|
17
|
+
if (content === undefined || content === null || content === '') {
|
|
18
|
+
if (el && el.getAttribute(MARK) !== null)
|
|
19
|
+
el.remove();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!el) {
|
|
23
|
+
el = d.createElement('meta');
|
|
24
|
+
el.setAttribute(attr, key);
|
|
25
|
+
el.setAttribute(MARK, '');
|
|
26
|
+
d.head.appendChild(el);
|
|
27
|
+
}
|
|
28
|
+
el.setAttribute('content', content);
|
|
29
|
+
}
|
|
30
|
+
/** Set this route's head tags. Call on every route change; unset fields are removed (if we added them). */
|
|
31
|
+
export function head(h) {
|
|
32
|
+
const d = doc();
|
|
33
|
+
if (!d)
|
|
34
|
+
return;
|
|
35
|
+
if (h.title)
|
|
36
|
+
d.title = h.title;
|
|
37
|
+
setMeta(d, 'name', 'description', h.description);
|
|
38
|
+
setMeta(d, 'property', 'og:title', h.title);
|
|
39
|
+
setMeta(d, 'property', 'og:description', h.description);
|
|
40
|
+
setMeta(d, 'property', 'og:image', h.image);
|
|
41
|
+
setMeta(d, 'property', 'og:type', h.type || 'website');
|
|
42
|
+
setMeta(d, 'property', 'og:site_name', h.siteName);
|
|
43
|
+
setMeta(d, 'name', 'twitter:card', h.image ? 'summary_large_image' : 'summary');
|
|
44
|
+
setMeta(d, 'name', 'twitter:title', h.title);
|
|
45
|
+
setMeta(d, 'name', 'twitter:description', h.description);
|
|
46
|
+
setMeta(d, 'name', 'twitter:image', h.image);
|
|
47
|
+
setMeta(d, 'name', 'robots', h.noindex ? 'noindex, nofollow' : undefined);
|
|
48
|
+
let link = d.head.querySelector('link[rel="canonical"]');
|
|
49
|
+
if (h.canonical) {
|
|
50
|
+
if (!link) {
|
|
51
|
+
link = d.createElement('link');
|
|
52
|
+
link.setAttribute('rel', 'canonical');
|
|
53
|
+
link.setAttribute(MARK, '');
|
|
54
|
+
d.head.appendChild(link);
|
|
55
|
+
}
|
|
56
|
+
link.setAttribute('href', h.canonical);
|
|
57
|
+
}
|
|
58
|
+
else if (link && link.getAttribute(MARK) !== null) {
|
|
59
|
+
link.remove();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Place a JSON-LD structured-data block in <head>. `id` names the block, so calling again replaces it
|
|
64
|
+
* (e.g. per route); pass null to remove it. `<` is escaped so data can't close the script tag.
|
|
65
|
+
*/
|
|
66
|
+
export function jsonLd(id, data) {
|
|
67
|
+
const d = doc();
|
|
68
|
+
if (!d)
|
|
69
|
+
return;
|
|
70
|
+
const sel = `script[type="application/ld+json"][${MARK}="${id}"]`;
|
|
71
|
+
let el = d.head.querySelector(sel);
|
|
72
|
+
if (data === null) {
|
|
73
|
+
if (el)
|
|
74
|
+
el.remove();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!el) {
|
|
78
|
+
el = d.createElement('script');
|
|
79
|
+
el.setAttribute('type', 'application/ld+json');
|
|
80
|
+
el.setAttribute(MARK, id);
|
|
81
|
+
d.head.appendChild(el);
|
|
82
|
+
}
|
|
83
|
+
el.textContent = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
84
|
+
}
|
|
85
|
+
const CTX = 'https://schema.org';
|
|
86
|
+
/** Drop undefined / null / empty values so the output stays valid and small. */
|
|
87
|
+
function clean(o) {
|
|
88
|
+
for (const k of Object.keys(o)) {
|
|
89
|
+
const v = o[k];
|
|
90
|
+
if (v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0))
|
|
91
|
+
delete o[k];
|
|
92
|
+
}
|
|
93
|
+
return o;
|
|
94
|
+
}
|
|
95
|
+
const imageOf = (x) => { var _a, _b, _c; return ((_a = x === null || x === void 0 ? void 0 : x.heroImage) === null || _a === void 0 ? void 0 : _a.url) || ((_b = x === null || x === void 0 ? void 0 : x.logoImage) === null || _b === void 0 ? void 0 : _b.url) || ((_c = x === null || x === void 0 ? void 0 : x.image) === null || _c === void 0 ? void 0 : _c.url) || (typeof (x === null || x === void 0 ? void 0 : x.image) === 'string' ? x.image : undefined); };
|
|
96
|
+
export const schema = {
|
|
97
|
+
/** schema.org Product from a SmartLinks product. */
|
|
98
|
+
product(product, opts = {}) {
|
|
99
|
+
const images = [imageOf(product), ...(product.additionalImages || []).map((i) => i === null || i === void 0 ? void 0 : i.url)].filter(Boolean);
|
|
100
|
+
return clean({
|
|
101
|
+
'@context': CTX,
|
|
102
|
+
'@type': 'Product',
|
|
103
|
+
name: product.name,
|
|
104
|
+
description: product.description || undefined,
|
|
105
|
+
image: images.length > 1 ? images : images[0],
|
|
106
|
+
sku: product.sku || undefined,
|
|
107
|
+
gtin: product.gtin || product.ownGtin || undefined,
|
|
108
|
+
url: opts.url,
|
|
109
|
+
brand: opts.brand ? { '@type': 'Brand', name: opts.brand } : undefined,
|
|
110
|
+
offers: opts.offer ? clean({
|
|
111
|
+
'@type': 'Offer',
|
|
112
|
+
price: String(opts.offer.price),
|
|
113
|
+
priceCurrency: opts.offer.currency,
|
|
114
|
+
availability: opts.offer.availability ? `${CTX}/${opts.offer.availability}` : undefined,
|
|
115
|
+
url: opts.offer.url || opts.url,
|
|
116
|
+
}) : undefined,
|
|
117
|
+
aggregateRating: opts.rating ? { '@type': 'AggregateRating', ratingValue: opts.rating.value, reviewCount: opts.rating.count } : undefined,
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
/** schema.org FAQPage — answer-shaped content AI search can cite, and FAQ rich results. */
|
|
121
|
+
faqPage(items) {
|
|
122
|
+
return {
|
|
123
|
+
'@context': CTX,
|
|
124
|
+
'@type': 'FAQPage',
|
|
125
|
+
mainEntity: items.filter((i) => i && i.question && i.answer).map((i) => ({
|
|
126
|
+
'@type': 'Question',
|
|
127
|
+
name: i.question,
|
|
128
|
+
acceptedAnswer: { '@type': 'Answer', text: i.answer },
|
|
129
|
+
})),
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
/** schema.org Organization from a collection (the brand behind the site). */
|
|
133
|
+
organization(collection, opts = {}) {
|
|
134
|
+
return clean({
|
|
135
|
+
'@context': CTX,
|
|
136
|
+
'@type': 'Organization',
|
|
137
|
+
name: collection.title || collection.name,
|
|
138
|
+
description: collection.description || undefined,
|
|
139
|
+
logo: imageOf(collection),
|
|
140
|
+
url: opts.url || (collection.hubCustomDomain ? `https://${collection.hubCustomDomain}` : collection.siteHost ? `https://${collection.siteHost}` : undefined),
|
|
141
|
+
sameAs: opts.sameAs,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
/** schema.org LocalBusiness — for a business with a place (shop, restaurant, studio). */
|
|
145
|
+
localBusiness(collection, opts = {}) {
|
|
146
|
+
const a = opts.address;
|
|
147
|
+
return clean(Object.assign(Object.assign({}, schema.organization(collection, { url: opts.url })), { '@type': opts.type || 'LocalBusiness', telephone: opts.telephone, address: a ? clean({ '@type': 'PostalAddress', streetAddress: a.street, addressLocality: a.locality, addressRegion: a.region, postalCode: a.postalCode, addressCountry: a.country }) : undefined, openingHours: opts.openingHours }));
|
|
148
|
+
},
|
|
149
|
+
/** schema.org BreadcrumbList from a trail of { name, url } (home first). */
|
|
150
|
+
breadcrumbs(trail) {
|
|
151
|
+
return {
|
|
152
|
+
'@context': CTX,
|
|
153
|
+
'@type': 'BreadcrumbList',
|
|
154
|
+
itemListElement: trail.map((t, i) => ({ '@type': 'ListItem', position: i + 1, name: t.name, item: t.url })),
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
/** schema.org Article (a blog post, news item, guide). Dates as ISO strings. */
|
|
158
|
+
article(a) {
|
|
159
|
+
return clean({
|
|
160
|
+
'@context': CTX,
|
|
161
|
+
'@type': 'Article',
|
|
162
|
+
headline: a.headline,
|
|
163
|
+
description: a.description,
|
|
164
|
+
image: a.image,
|
|
165
|
+
url: a.url,
|
|
166
|
+
datePublished: a.datePublished,
|
|
167
|
+
dateModified: a.dateModified || a.datePublished,
|
|
168
|
+
author: a.author ? { '@type': 'Person', name: a.author } : undefined,
|
|
169
|
+
publisher: a.publisher ? { '@type': 'Organization', name: a.publisher } : undefined,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
};
|
package/dist/site.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
var __SL_READY__: boolean | undefined;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Tell the platform this page has finished rendering its content (data loaded, head tags set). The
|
|
6
|
+
* platform's page renderer takes its snapshot for search engines and AI crawlers at this point;
|
|
7
|
+
* without it, it waits for the network to go idle (capped). Call once per page, after your data
|
|
8
|
+
* renders. Harmless anywhere else.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ready(): void;
|
package/dist/site.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// site — for apps served as websites.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
/**
|
|
5
|
+
* Tell the platform this page has finished rendering its content (data loaded, head tags set). The
|
|
6
|
+
* platform's page renderer takes its snapshot for search engines and AI crawlers at this point;
|
|
7
|
+
* without it, it waits for the network to go idle (capped). Call once per page, after your data
|
|
8
|
+
* renders. Harmless anywhere else.
|
|
9
|
+
*/
|
|
10
|
+
export function ready() {
|
|
11
|
+
if (typeof globalThis === 'undefined')
|
|
12
|
+
return;
|
|
13
|
+
globalThis.__SL_READY__ = true;
|
|
14
|
+
const w = typeof window !== 'undefined' ? window : null;
|
|
15
|
+
if (w && typeof w.dispatchEvent === 'function' && typeof w.CustomEvent === 'function') {
|
|
16
|
+
w.dispatchEvent(new w.CustomEvent('smartlinks:ready'));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
+
import type { AppDataDeclaration, AppHeadlessDeclaration } from './headless.js';
|
|
3
4
|
/**
|
|
4
5
|
* A bundle (widget or container) as returned by the collection widgets endpoint.
|
|
5
6
|
*
|
|
@@ -662,6 +663,16 @@ export interface AppManifest {
|
|
|
662
663
|
* @see AppManifestFunctions
|
|
663
664
|
*/
|
|
664
665
|
functions?: AppManifestFunctions;
|
|
666
|
+
/**
|
|
667
|
+
* The app's public data model: its data types, how each is stored, fields, examples and read
|
|
668
|
+
* recipes. Required for `headless`. @see AppDataDeclaration · docs/headless-providers.md
|
|
669
|
+
*/
|
|
670
|
+
data?: AppDataDeclaration;
|
|
671
|
+
/**
|
|
672
|
+
* Opt-in: other sites and apps may install this app and use its content (a headless content
|
|
673
|
+
* provider). Validated by `smartlinks-headless`. @see AppHeadlessDeclaration
|
|
674
|
+
*/
|
|
675
|
+
headless?: AppHeadlessDeclaration;
|
|
665
676
|
[key: string]: any;
|
|
666
677
|
}
|
|
667
678
|
/**
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Field value types. Keep this set small: generated editors, typed reads and sites rely on it. */
|
|
2
|
+
export type AppDataFieldType = 'string' | 'text' | 'richtext' | 'markdown' | 'number' | 'boolean' | 'date' | 'datetime' | 'enum' | 'url' | 'image' | 'file' | 'ref' | 'string[]' | 'ref[]' | 'json';
|
|
3
|
+
export interface AppDataField {
|
|
4
|
+
type: AppDataFieldType;
|
|
5
|
+
label?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
/** Translated per language (string / text / richtext / markdown only). */
|
|
9
|
+
localized?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The JSONB zone it lives in (app objects): `data` (default), `owner`, or `admin`. Only `data`
|
|
12
|
+
* can be public; `admin` is never readable on public endpoints.
|
|
13
|
+
*/
|
|
14
|
+
zone?: 'data' | 'owner' | 'admin';
|
|
15
|
+
/** Readable by anonymous visitors (when the item's visibility is public). */
|
|
16
|
+
public?: boolean;
|
|
17
|
+
/** For `ref` / `ref[]`: the declared type id, or 'product' | 'contact' | 'proof'. */
|
|
18
|
+
to?: string;
|
|
19
|
+
/** For `enum`: the allowed values. */
|
|
20
|
+
options?: string[];
|
|
21
|
+
}
|
|
22
|
+
/** How a type's items are stored. */
|
|
23
|
+
export type AppDataStorage =
|
|
24
|
+
/** App records (`SL.app.records`), filtered by recordType. The usual home for content. */
|
|
25
|
+
{
|
|
26
|
+
kind: 'record';
|
|
27
|
+
recordType: string;
|
|
28
|
+
}
|
|
29
|
+
/** App cases (`SL.app.cases`) — requests that get resolved. */
|
|
30
|
+
| {
|
|
31
|
+
kind: 'case';
|
|
32
|
+
category?: string;
|
|
33
|
+
}
|
|
34
|
+
/** App threads (`SL.app.threads`) — discussions, Q&A, reviews. */
|
|
35
|
+
| {
|
|
36
|
+
kind: 'thread';
|
|
37
|
+
slug?: string;
|
|
38
|
+
}
|
|
39
|
+
/** App configuration (`SL.appConfiguration.getConfig` / data items) — settings, small lists. */
|
|
40
|
+
| {
|
|
41
|
+
kind: 'config';
|
|
42
|
+
key?: string;
|
|
43
|
+
};
|
|
44
|
+
export interface AppDataType {
|
|
45
|
+
/** What one item is, in a sentence. */
|
|
46
|
+
description: string;
|
|
47
|
+
storage: AppDataStorage;
|
|
48
|
+
/** The visibility items normally have (app objects): public, owner or admin. Default public. */
|
|
49
|
+
visibility?: 'public' | 'owner' | 'admin';
|
|
50
|
+
/** What an item can be attached to (app objects' anchor columns). */
|
|
51
|
+
anchors?: Array<'product' | 'variant' | 'batch' | 'proof' | 'contact'>;
|
|
52
|
+
fields: Record<string, AppDataField>;
|
|
53
|
+
/** How a site normally lists them. */
|
|
54
|
+
listing?: {
|
|
55
|
+
sort?: string[];
|
|
56
|
+
filters?: string[];
|
|
57
|
+
};
|
|
58
|
+
/** Real-shaped sample items (the `data` zone's JSON). At least one for a headless type. */
|
|
59
|
+
examples?: Array<Record<string, unknown>>;
|
|
60
|
+
/** SDK recipes a site uses. If omitted, the checker prints the standard ones for the storage. */
|
|
61
|
+
read?: {
|
|
62
|
+
list?: string;
|
|
63
|
+
get?: string;
|
|
64
|
+
notes?: string;
|
|
65
|
+
};
|
|
66
|
+
/** The app version that introduced this type. */
|
|
67
|
+
since?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface AppDataDeclaration {
|
|
70
|
+
/** Semver of the data contract itself (not the app's version). Major = breaking. */
|
|
71
|
+
schemaVersion: string;
|
|
72
|
+
types: Record<string, AppDataType>;
|
|
73
|
+
}
|
|
74
|
+
/** What kind of content a provider supplies — how site builders find it. */
|
|
75
|
+
export type HeadlessCategory = 'faq' | 'media' | 'pages' | 'articles' | 'catalog' | 'events' | 'locations' | 'people' | 'reviews' | 'documents' | 'forms' | 'other';
|
|
76
|
+
export declare const HEADLESS_CATEGORIES: readonly HeadlessCategory[];
|
|
77
|
+
/** SEO structured-data helpers a site should use with this content (`SL.seo.schema.*`). */
|
|
78
|
+
export type HeadlessSeoHelper = 'faqPage' | 'product' | 'article' | 'breadcrumbs' | 'organization' | 'localBusiness';
|
|
79
|
+
export interface AppHeadlessDeclaration {
|
|
80
|
+
/** What content this app holds, and when a site should use it rather than build its own. */
|
|
81
|
+
purpose: string;
|
|
82
|
+
categories: HeadlessCategory[];
|
|
83
|
+
/** The `data` types a site renders (the rest are supporting types, e.g. categories). */
|
|
84
|
+
primaryTypes: string[];
|
|
85
|
+
/** Where the business edits the content — the app's admin inside SmartLinks. Sites never build an editor for it. */
|
|
86
|
+
editedIn: {
|
|
87
|
+
label: string;
|
|
88
|
+
adminPath?: string;
|
|
89
|
+
notes?: string;
|
|
90
|
+
};
|
|
91
|
+
/** How a site should present it. */
|
|
92
|
+
render?: {
|
|
93
|
+
guidance?: string;
|
|
94
|
+
seo?: HeadlessSeoHelper[];
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Headless providers — an app's declared public data model, and its promise that other sites and
|
|
3
|
+
// apps can install it and use its content (FAQ, media, content pages, articles…).
|
|
4
|
+
//
|
|
5
|
+
// Two manifest blocks:
|
|
6
|
+
// data the app's data types: how each is stored, its fields (JSON types, zone, public or not),
|
|
7
|
+
// examples, and how to read it. Useful on its own (typed reads, generated editors).
|
|
8
|
+
// headless the opt-in: purpose, categories, which types a site renders, where the owner edits the
|
|
9
|
+
// content, rendering + SEO guidance. Requires a complete `data` block.
|
|
10
|
+
//
|
|
11
|
+
// Validate with `smartlinks-headless` (scripts/headless-check.mjs) or validateHeadless() (SDK).
|
|
12
|
+
// Docs: docs/headless-providers.md.
|
|
13
|
+
// =============================================================================
|
|
14
|
+
export const HEADLESS_CATEGORIES = [
|
|
15
|
+
'faq', 'media', 'pages', 'articles', 'catalog', 'events', 'locations', 'people', 'reviews', 'documents', 'forms', 'other',
|
|
16
|
+
];
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.39 | Generated: 2026-10-05T14:14:46.558Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -26,6 +26,12 @@ For detailed guides on specific features:
|
|
|
26
26
|
- **[iframe Responder](iframe-responder.md)** - iframe integration and cross-origin communication (incl. hand-rolled streaming protocol)
|
|
27
27
|
- **[Utilities](utils.md)** - Helper functions for building portal paths, URLs, and common tasks
|
|
28
28
|
- **[UI Utils](ui-utils.md)** - Reusable, themeable admin UI React component library for microapps
|
|
29
|
+
- **[Headless Providers](headless-providers.md)** - Declaring an app's content for other sites: the manifest `data` + `headless` blocks (types, storage, public fields, examples, read recipes, categories), `SL.headless.validate`, `smartlinks-headless`, and the procedure for adding a headless mode
|
|
30
|
+
- **[Websites: SEO + GEO](site-seo.md)** - For apps served as websites: platform-generated robots/sitemap/llms.txt, canonical addresses, `SL.seo.head` / `SL.seo.jsonLd` / `SL.seo.schema.*`, `SL.site.ready()`, routes in `sitemap-paths.txt`
|
|
31
|
+
- **[Agent Tools](agent-tools.md)** - Exposing app functions as AI agent tools
|
|
32
|
+
- **[Host Dependency Contract](host-dependency-contract.md)** - The shared dependencies (React, the SDK…) the host provides, and what an app must externalise
|
|
33
|
+
- **[Theme Tokens](theme-tokens.md)** - The `--sl-*` semantic token contract hosts set and apps bind to (`theme.css`)
|
|
34
|
+
- **[CSS Baseline](css-baseline.md)** - The frozen `sl-*` structural helper classes hosts guarantee
|
|
29
35
|
- **[Caching](caching.md)** - Multi-tier caching strategy (in-memory, SessionStorage, IndexedDB) used by the SDK
|
|
30
36
|
- **[Native Facade](native-facade.md)** - Contract layer for accessing device capabilities (share, NFC, haptics) across host shells
|
|
31
37
|
- **[i18n](i18n.md)** - Internationalization and localization
|
|
@@ -2748,6 +2754,8 @@ interface AppManifest {
|
|
|
2748
2754
|
publicViews?: PublicView[];
|
|
2749
2755
|
executor?: AppManifestExecutor;
|
|
2750
2756
|
functions?: AppManifestFunctions;
|
|
2757
|
+
data?: AppDataDeclaration;
|
|
2758
|
+
headless?: AppHeadlessDeclaration;
|
|
2751
2759
|
[key: string]: any;
|
|
2752
2760
|
}
|
|
2753
2761
|
```
|
|
@@ -6459,6 +6467,65 @@ interface FacetValueGetParams {
|
|
|
6459
6467
|
|
|
6460
6468
|
**FacetValueDefinition** = `FacetValue`
|
|
6461
6469
|
|
|
6470
|
+
### headless
|
|
6471
|
+
|
|
6472
|
+
**AppDataField** (interface)
|
|
6473
|
+
```typescript
|
|
6474
|
+
interface AppDataField {
|
|
6475
|
+
type: AppDataFieldType;
|
|
6476
|
+
label?: string;
|
|
6477
|
+
description?: string;
|
|
6478
|
+
required?: boolean;
|
|
6479
|
+
localized?: boolean;
|
|
6480
|
+
zone?: 'data' | 'owner' | 'admin';
|
|
6481
|
+
public?: boolean;
|
|
6482
|
+
to?: string;
|
|
6483
|
+
options?: string[];
|
|
6484
|
+
}
|
|
6485
|
+
```
|
|
6486
|
+
|
|
6487
|
+
**AppDataType** (interface)
|
|
6488
|
+
```typescript
|
|
6489
|
+
interface AppDataType {
|
|
6490
|
+
description: string;
|
|
6491
|
+
storage: AppDataStorage;
|
|
6492
|
+
visibility?: 'public' | 'owner' | 'admin';
|
|
6493
|
+
anchors?: Array<'product' | 'variant' | 'batch' | 'proof' | 'contact'>;
|
|
6494
|
+
fields: Record<string, AppDataField>;
|
|
6495
|
+
listing?: { sort?: string[]; filters?: string[] };
|
|
6496
|
+
examples?: Array<Record<string, unknown>>;
|
|
6497
|
+
read?: { list?: string; get?: string; notes?: string };
|
|
6498
|
+
since?: string;
|
|
6499
|
+
}
|
|
6500
|
+
```
|
|
6501
|
+
|
|
6502
|
+
**AppDataDeclaration** (interface)
|
|
6503
|
+
```typescript
|
|
6504
|
+
interface AppDataDeclaration {
|
|
6505
|
+
schemaVersion: string;
|
|
6506
|
+
types: Record<string, AppDataType>;
|
|
6507
|
+
}
|
|
6508
|
+
```
|
|
6509
|
+
|
|
6510
|
+
**AppHeadlessDeclaration** (interface)
|
|
6511
|
+
```typescript
|
|
6512
|
+
interface AppHeadlessDeclaration {
|
|
6513
|
+
purpose: string;
|
|
6514
|
+
categories: HeadlessCategory[];
|
|
6515
|
+
primaryTypes: string[];
|
|
6516
|
+
editedIn: { label: string; adminPath?: string; notes?: string };
|
|
6517
|
+
render?: { guidance?: string; seo?: HeadlessSeoHelper[] };
|
|
6518
|
+
}
|
|
6519
|
+
```
|
|
6520
|
+
|
|
6521
|
+
**AppDataFieldType** = ``
|
|
6522
|
+
|
|
6523
|
+
**AppDataStorage** = ``
|
|
6524
|
+
|
|
6525
|
+
**HeadlessCategory** = ``
|
|
6526
|
+
|
|
6527
|
+
**HeadlessSeoHelper** = `'faqPage' | 'product' | 'article' | 'breadcrumbs' | 'organization' | 'localBusiness'`
|
|
6528
|
+
|
|
6462
6529
|
### iframeResponder
|
|
6463
6530
|
|
|
6464
6531
|
**CachedData** (interface)
|
|
@@ -11131,22 +11198,22 @@ The release channel a call targets, or undefined for "the collection's installed
|
|
|
11131
11198
|
**functionPath**(surface: 'public' | 'admin', collectionId: string, name: string, opts: FunctionCallOptions = {}) → `string`
|
|
11132
11199
|
The API path a function call goes to (exported for hosts/tests that need the exact URL).
|
|
11133
11200
|
|
|
11134
|
-
**siteUrl**(collection: { siteHost?: string | null } | string,
|
|
11135
|
-
name: string,
|
|
11201
|
+
**siteUrl**(collection: { siteHost?: string | null } | string,
|
|
11202
|
+
name: string,
|
|
11136
11203
|
opts: FunctionSiteUrlOptions & { appId?: string } = {}) → `string`
|
|
11137
11204
|
The PUBLIC address of an app function on the collection's own site — what you give a third party as a webhook URL, or call from the collection's public pages: `https://<siteHost>/_fn/<appId>[/<channel>]/<name>[/<path>]`. Every HTTP method the function declares works there, with the raw body for signature checks. Pass the collection (or its siteHost). This address is for public/integration calls; signed-in calls from your app keep using {@link call} / {@link callAdmin}, so the user's SmartLinks session never goes to a tenant hostname. const col = await SL.collection.get(collectionId) const hookUrl = SL.functions.siteUrl(col, 'stripeWebhook', { appId: 'my-shop' }) // → https://acme.smartlinks.host/_fn/my-shop/stripeWebhook
|
|
11138
11205
|
|
|
11139
|
-
**call**(collectionId: string,
|
|
11140
|
-
name: string,
|
|
11141
|
-
body: Record<string, any> = {},
|
|
11206
|
+
**call**(collectionId: string,
|
|
11207
|
+
name: string,
|
|
11208
|
+
body: Record<string, any> = {},
|
|
11142
11209
|
opts: FunctionCallOptions = {}) → `Promise<T>`
|
|
11143
|
-
Call a PUBLIC app server function inline (surface `'public'`). App-scoped: `POST /public/collection/:c/app/:appId/functions/:name`. // App calling its own function (appId from initializeApi({ appId })): const { value } = await SL.functions.call<{ value: number }>(collectionId, 'pressCounter') // Or address another app explicitly: await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'my-counter-app' })
|
|
11210
|
+
Call a PUBLIC app server function inline (surface `'public'`). App-scoped: `POST /fn/public/collection/:c/app/:appId/functions/:name`. // App calling its own function (appId from initializeApi({ appId })): const { value } = await SL.functions.call<{ value: number }>(collectionId, 'pressCounter') // Or address another app explicitly: await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'my-counter-app' })
|
|
11144
11211
|
|
|
11145
|
-
**callAdmin**(collectionId: string,
|
|
11146
|
-
name: string,
|
|
11147
|
-
body: Record<string, any> = {},
|
|
11212
|
+
**callAdmin**(collectionId: string,
|
|
11213
|
+
name: string,
|
|
11214
|
+
body: Record<string, any> = {},
|
|
11148
11215
|
opts: FunctionCallOptions = {}) → `Promise<T>`
|
|
11149
|
-
Call an ADMIN app server function (surface `'admin'`; requires an admin session). App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
|
|
11216
|
+
Call an ADMIN app server function (surface `'admin'`; requires an admin session). App-scoped: `POST /fn/admin/collection/:c/app/:appId/functions/:name`.
|
|
11150
11217
|
|
|
11151
11218
|
**list**(collectionId: string, opts: FunctionCallOptions = {}) → `Promise<FunctionListResponse>`
|
|
11152
11219
|
List the public functions available for a collection (discovery). Scoped to one app when an appId is given (or set as the SDK app context): `GET /public/collection/:c[/app/:appId]/functions`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Headless providers: declaring an app's content for other sites
|
|
2
|
+
|
|
3
|
+
A **headless provider** is an app whose content other sites and apps may use: an FAQ app, a media gallery, content pages, a blog. The site renders the content its own way. The business keeps editing it in the provider's own admin, inside SmartLinks. A site never builds an editor for provider content.
|
|
4
|
+
|
|
5
|
+
To become a provider, an app declares two blocks in `app.manifest.json`:
|
|
6
|
+
- **`data`**: its data types, meaning how each is stored, its fields, examples and read recipes;
|
|
7
|
+
- **`headless`**: the opt-in, meaning its purpose, categories, which types a site renders, where the owner edits it, and rendering guidance.
|
|
8
|
+
|
|
9
|
+
Site builders (the Forge agent included) read these declarations to decide "install the FAQ app and use its content" instead of inventing their own.
|
|
10
|
+
|
|
11
|
+
## The `data` block
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
"data": {
|
|
15
|
+
"schemaVersion": "1.0.0", // semver of THIS contract; a major = a breaking change
|
|
16
|
+
"types": {
|
|
17
|
+
"<app>.<thing>": { // lowercase, dot-separated, e.g. "faq.item"
|
|
18
|
+
"description": "What one item is, in a sentence.",
|
|
19
|
+
"storage": { "kind": "record", "recordType": "<the recordType the app writes>" },
|
|
20
|
+
"visibility": "public", // the visibility items normally have: public | owner | admin
|
|
21
|
+
"anchors": ["product"], // optional: what items can be attached to
|
|
22
|
+
"fields": {
|
|
23
|
+
"<key>": {
|
|
24
|
+
"type": "string", // see field types
|
|
25
|
+
"label": "Question",
|
|
26
|
+
"required": true,
|
|
27
|
+
"localized": true, // translated per language (text types only)
|
|
28
|
+
"public": true, // readable by anonymous visitors
|
|
29
|
+
"zone": "data" // data (default) | owner | admin
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"listing": { "sort": ["order"], "filters": ["category"] },
|
|
33
|
+
"examples": [ { "<key>": "realistic value" } ],
|
|
34
|
+
"read": { // optional: defaults to the standard calls for the storage
|
|
35
|
+
"list": "SL.app.records.list(collectionId, appId, { recordType: '...', limit: 50 })",
|
|
36
|
+
"get": "SL.app.records.get(collectionId, appId, recordId)",
|
|
37
|
+
"notes": "anything a site must know, e.g. 'sort by order, then title'"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Storage kinds:**
|
|
45
|
+
|
|
46
|
+
| `storage.kind` | The app keeps items in | Read with |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `record` (+ `recordType`) | App records: the usual home for content | `SL.app.records.list / get`, filtered by `recordType` |
|
|
49
|
+
| `case` | App cases: requests that get resolved | `SL.app.cases.list / get` |
|
|
50
|
+
| `thread` | App threads: discussions, Q&A, reviews | `SL.app.threads.list / get` |
|
|
51
|
+
| `config` (+ optional `key`) | App configuration: settings, small fixed lists | `SL.appConfiguration.getConfig({ collectionId, appId })` |
|
|
52
|
+
|
|
53
|
+
**What the read calls return.** This is fixed by the storage kind, so a provider never needs to describe it, and site builders get it from the checker and `forge-cms describe`:
|
|
54
|
+
|
|
55
|
+
- **`record`, `case`, `thread`:** `list` returns `{ data: Item[], pagination: { total, limit, offset, hasMore } }`.
|
|
56
|
+
- The items are in **`response.data`**, never `response.items` or `response.records`.
|
|
57
|
+
- Each item's declared fields are in **`item.data`** (e.g. `record.data.question`). `id`, `status`, `productId` and the dates are top-level.
|
|
58
|
+
- Page with `offset` / `limit` while `pagination.hasMore`. `get` returns one item.
|
|
59
|
+
- **`config`:** the configuration object. Items are in `config.<key>` when the type names a `key`.
|
|
60
|
+
|
|
61
|
+
Products, contacts and proofs are platform data. Reference them with `ref` fields (`"to": "product"`); never redeclare them.
|
|
62
|
+
|
|
63
|
+
**Field types:** `string`, `text`, `richtext` (HTML), `markdown`, `number`, `boolean`, `date` (YYYY-MM-DD), `datetime`, `enum` (+ `options`), `url`, `image` and `file` (a URL or `{ url, ... }`), `ref` and `ref[]` (+ `to`: a declared type id, or `product` / `contact` / `proof`), `string[]`, `json` (avoid where a typed field fits).
|
|
64
|
+
|
|
65
|
+
**Zones and what's public.** App records, cases and threads have three JSONB zones: `data`, `owner` and `admin`. Only `data` can be public, and only when the item's visibility is `public`. Mark each field a visitor may read with `"public": true`. Internal fields (notes, moderation flags) are `zone: "admin"` and never public.
|
|
66
|
+
|
|
67
|
+
**Examples** are the item's `data` JSON exactly as the app writes it, with realistic content, not lorem ipsum.
|
|
68
|
+
|
|
69
|
+
## The `headless` block
|
|
70
|
+
|
|
71
|
+
```jsonc
|
|
72
|
+
"headless": {
|
|
73
|
+
"purpose": "What content this holds and when a site should use it instead of building its own.",
|
|
74
|
+
"categories": ["faq"], // faq | media | pages | articles | catalog | events | locations
|
|
75
|
+
// | people | reviews | documents | forms | other
|
|
76
|
+
"primaryTypes": ["faq.item"], // the types a site renders; others are supporting (e.g. categories)
|
|
77
|
+
"editedIn": { "label": "FAQ → Questions", "adminPath": "#/questions" },
|
|
78
|
+
"render": {
|
|
79
|
+
"guidance": "Group by category; questions expand to show answers.",
|
|
80
|
+
"seo": ["faqPage"] // SL.seo.schema helpers a site should use
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A primary type must be readable by visitors: public visibility, at least one public field, and at least one example.
|
|
86
|
+
|
|
87
|
+
## Checking a declaration
|
|
88
|
+
|
|
89
|
+
- **In Forge:** `forge-headless check` validates it against the app's real public data on its test collection. `forge-headless register` validates it and records it in Forge, which makes the app a provider that site builds can find.
|
|
90
|
+
- **Anywhere else:**
|
|
91
|
+
- `npx smartlinks-headless [appDir] [--collection <id> --app <appId>]`;
|
|
92
|
+
- or `SL.headless.validate(manifest, { samples })` in code.
|
|
93
|
+
|
|
94
|
+
Errors block. Warnings are worth fixing, and the most useful one is *real data has "x" but it isn't declared*.
|
|
95
|
+
|
|
96
|
+
## Adding a headless mode to an existing app (procedure)
|
|
97
|
+
|
|
98
|
+
1. **Find what the app stores.** Search the source for its writes and reads:
|
|
99
|
+
- `SL.app.records.create / upsert / bulkUpsert` (note each `recordType`);
|
|
100
|
+
- `SL.app.cases`, `SL.app.threads`;
|
|
101
|
+
- `appConfiguration.setConfig` / data items.
|
|
102
|
+
|
|
103
|
+
Each distinct `recordType` (or config key) the business edits is a candidate type.
|
|
104
|
+
2. **Find the fields.** Read the admin forms and the TypeScript types for what each item holds. Note which zone each field is written to.
|
|
105
|
+
3. **Decide what's public.** Content a visitor sees is public. Notes, flags and anything personal are not, and move to `zone: "admin"` if the app keeps them in `data`.
|
|
106
|
+
4. **Write the `data` block:** a type per content thing, its storage, fields, listing, and one or two realistic examples taken from the app's real data.
|
|
107
|
+
5. **Write the `headless` block:** purpose, categories, primary types, where it's edited (the admin screen's name and route), and render and SEO guidance.
|
|
108
|
+
6. **Check against real data** (`forge-headless check`). Declare any real fields you missed, or confirm they're internal.
|
|
109
|
+
7. **Register** (`forge-headless register`) once it's valid.
|
|
110
|
+
8. **Keep the data contract stable.** Adding fields or types is a minor bump of `schemaVersion`; renaming or removing them is a major one.
|
|
111
|
+
|
|
112
|
+
Don't change how the app stores data just to declare it. The declaration describes what already exists. If something must change (say, an internal field sitting in the public `data` zone), make that a separate, deliberate change.
|