@proveanything/smartlinks 2.0.36 → 2.0.38
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/ai.d.ts +2 -2
- package/dist/api/ai.js +2 -2
- package/dist/context.js +15 -0
- package/dist/docs/API_SUMMARY.md +76 -3
- package/dist/docs/headless-providers.md +104 -0
- package/dist/docs/server-functions.md +1 -1
- package/dist/docs/site-seo.md +67 -0
- package/dist/headless.d.ts +31 -0
- package/dist/headless.js +225 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/openapi.yaml +184 -5
- 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/ai.d.ts +12 -15
- package/dist/types/appManifest.d.ts +11 -0
- package/dist/types/appManifest.js +0 -1
- package/dist/types/collection.d.ts +1 -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 +76 -3
- package/docs/headless-providers.md +104 -0
- package/docs/server-functions.md +1 -1
- package/docs/site-seo.md +67 -0
- package/openapi.yaml +184 -5
- package/package.json +4 -2
- package/scripts/headless-check.mjs +87 -0
|
@@ -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.38 | Generated: 2026-10-05T12:10:36.598Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -918,8 +918,20 @@ interface AiUsageReport {
|
|
|
918
918
|
groupBy: string[]
|
|
919
919
|
from?: string | null
|
|
920
920
|
to?: string | null
|
|
921
|
-
totals:
|
|
922
|
-
groups: Array<Record<string, any> &
|
|
921
|
+
totals: AiUsageTotals
|
|
922
|
+
groups: Array<Record<string, any> & AiUsageTotals>
|
|
923
|
+
}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
**AiUsageTotals** (interface)
|
|
927
|
+
```typescript
|
|
928
|
+
interface AiUsageTotals {
|
|
929
|
+
promptTokens: number
|
|
930
|
+
outputTokens: number
|
|
931
|
+
cachedTokens: number
|
|
932
|
+
totalTokens: number
|
|
933
|
+
requests: number
|
|
934
|
+
images: number
|
|
923
935
|
}
|
|
924
936
|
```
|
|
925
937
|
|
|
@@ -2736,6 +2748,8 @@ interface AppManifest {
|
|
|
2736
2748
|
publicViews?: PublicView[];
|
|
2737
2749
|
executor?: AppManifestExecutor;
|
|
2738
2750
|
functions?: AppManifestFunctions;
|
|
2751
|
+
data?: AppDataDeclaration;
|
|
2752
|
+
headless?: AppHeadlessDeclaration;
|
|
2739
2753
|
[key: string]: any;
|
|
2740
2754
|
}
|
|
2741
2755
|
```
|
|
@@ -6447,6 +6461,65 @@ interface FacetValueGetParams {
|
|
|
6447
6461
|
|
|
6448
6462
|
**FacetValueDefinition** = `FacetValue`
|
|
6449
6463
|
|
|
6464
|
+
### headless
|
|
6465
|
+
|
|
6466
|
+
**AppDataField** (interface)
|
|
6467
|
+
```typescript
|
|
6468
|
+
interface AppDataField {
|
|
6469
|
+
type: AppDataFieldType;
|
|
6470
|
+
label?: string;
|
|
6471
|
+
description?: string;
|
|
6472
|
+
required?: boolean;
|
|
6473
|
+
localized?: boolean;
|
|
6474
|
+
zone?: 'data' | 'owner' | 'admin';
|
|
6475
|
+
public?: boolean;
|
|
6476
|
+
to?: string;
|
|
6477
|
+
options?: string[];
|
|
6478
|
+
}
|
|
6479
|
+
```
|
|
6480
|
+
|
|
6481
|
+
**AppDataType** (interface)
|
|
6482
|
+
```typescript
|
|
6483
|
+
interface AppDataType {
|
|
6484
|
+
description: string;
|
|
6485
|
+
storage: AppDataStorage;
|
|
6486
|
+
visibility?: 'public' | 'owner' | 'admin';
|
|
6487
|
+
anchors?: Array<'product' | 'variant' | 'batch' | 'proof' | 'contact'>;
|
|
6488
|
+
fields: Record<string, AppDataField>;
|
|
6489
|
+
listing?: { sort?: string[]; filters?: string[] };
|
|
6490
|
+
examples?: Array<Record<string, unknown>>;
|
|
6491
|
+
read?: { list?: string; get?: string; notes?: string };
|
|
6492
|
+
since?: string;
|
|
6493
|
+
}
|
|
6494
|
+
```
|
|
6495
|
+
|
|
6496
|
+
**AppDataDeclaration** (interface)
|
|
6497
|
+
```typescript
|
|
6498
|
+
interface AppDataDeclaration {
|
|
6499
|
+
schemaVersion: string;
|
|
6500
|
+
types: Record<string, AppDataType>;
|
|
6501
|
+
}
|
|
6502
|
+
```
|
|
6503
|
+
|
|
6504
|
+
**AppHeadlessDeclaration** (interface)
|
|
6505
|
+
```typescript
|
|
6506
|
+
interface AppHeadlessDeclaration {
|
|
6507
|
+
purpose: string;
|
|
6508
|
+
categories: HeadlessCategory[];
|
|
6509
|
+
primaryTypes: string[];
|
|
6510
|
+
editedIn: { label: string; adminPath?: string; notes?: string };
|
|
6511
|
+
render?: { guidance?: string; seo?: HeadlessSeoHelper[] };
|
|
6512
|
+
}
|
|
6513
|
+
```
|
|
6514
|
+
|
|
6515
|
+
**AppDataFieldType** = ``
|
|
6516
|
+
|
|
6517
|
+
**AppDataStorage** = ``
|
|
6518
|
+
|
|
6519
|
+
**HeadlessCategory** = ``
|
|
6520
|
+
|
|
6521
|
+
**HeadlessSeoHelper** = `'faqPage' | 'product' | 'article' | 'breadcrumbs' | 'organization' | 'localBusiness'`
|
|
6522
|
+
|
|
6450
6523
|
### iframeResponder
|
|
6451
6524
|
|
|
6452
6525
|
**CachedData** (interface)
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
Products, contacts and proofs are platform data. Reference them with `ref` fields (`"to": "product"`); never redeclare them.
|
|
54
|
+
|
|
55
|
+
**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).
|
|
56
|
+
|
|
57
|
+
**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.
|
|
58
|
+
|
|
59
|
+
**Examples** are the item's `data` JSON exactly as the app writes it, with realistic content, not lorem ipsum.
|
|
60
|
+
|
|
61
|
+
## The `headless` block
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
"headless": {
|
|
65
|
+
"purpose": "What content this holds and when a site should use it instead of building its own.",
|
|
66
|
+
"categories": ["faq"], // faq | media | pages | articles | catalog | events | locations
|
|
67
|
+
// | people | reviews | documents | forms | other
|
|
68
|
+
"primaryTypes": ["faq.item"], // the types a site renders; others are supporting (e.g. categories)
|
|
69
|
+
"editedIn": { "label": "FAQ → Questions", "adminPath": "#/questions" },
|
|
70
|
+
"render": {
|
|
71
|
+
"guidance": "Group by category; questions expand to show answers.",
|
|
72
|
+
"seo": ["faqPage"] // SL.seo.schema helpers a site should use
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A primary type must be readable by visitors: public visibility, at least one public field, and at least one example.
|
|
78
|
+
|
|
79
|
+
## Checking a declaration
|
|
80
|
+
|
|
81
|
+
- **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.
|
|
82
|
+
- **Anywhere else:**
|
|
83
|
+
- `npx smartlinks-headless [appDir] [--collection <id> --app <appId>]`;
|
|
84
|
+
- or `SL.headless.validate(manifest, { samples })` in code.
|
|
85
|
+
|
|
86
|
+
Errors block. Warnings are worth fixing, and the most useful one is *real data has "x" but it isn't declared*.
|
|
87
|
+
|
|
88
|
+
## Adding a headless mode to an existing app (procedure)
|
|
89
|
+
|
|
90
|
+
1. **Find what the app stores.** Search the source for its writes and reads:
|
|
91
|
+
- `SL.app.records.create / upsert / bulkUpsert` (note each `recordType`);
|
|
92
|
+
- `SL.app.cases`, `SL.app.threads`;
|
|
93
|
+
- `appConfiguration.setConfig` / data items.
|
|
94
|
+
|
|
95
|
+
Each distinct `recordType` (or config key) the business edits is a candidate type.
|
|
96
|
+
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.
|
|
97
|
+
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`.
|
|
98
|
+
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.
|
|
99
|
+
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.
|
|
100
|
+
6. **Check against real data** (`forge-headless check`). Declare any real fields you missed, or confirm they're internal.
|
|
101
|
+
7. **Register** (`forge-headless register`) once it's valid.
|
|
102
|
+
8. **Keep the data contract stable.** Adding fields or types is a minor bump of `schemaVersion`; renaming or removing them is a major one.
|
|
103
|
+
|
|
104
|
+
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.
|
package/docs/server-functions.md
CHANGED
|
@@ -275,7 +275,7 @@ await SL.functions.call(collectionId, 'pressCounter', {}, { channel: null }) /
|
|
|
275
275
|
```
|
|
276
276
|
|
|
277
277
|
**Public address on the collection's own site (webhooks, integrations).** Every collection has a
|
|
278
|
-
site host — `<name>.smartlinks.host` once a name is claimed, else `c-<
|
|
278
|
+
site host — `<name>.smartlinks.host` once a name is claimed, else `c-<collectionId>.smartlinks.host` —
|
|
279
279
|
returned as `collection.siteHost`. App functions are reachable there:
|
|
280
280
|
|
|
281
281
|
```
|
package/docs/site-seo.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Websites: search engines and AI crawlers (SEO + GEO)
|
|
2
|
+
|
|
3
|
+
For apps served as websites (an app site at `<name>.smartlinks.host` or a custom domain). Hub sites
|
|
4
|
+
get all of this from Hub.
|
|
5
|
+
|
|
6
|
+
## What the platform does for you
|
|
7
|
+
|
|
8
|
+
- **`/robots.txt`, `/sitemap.xml`, `/llms.txt`** are generated on the site's own address. Don't ship
|
|
9
|
+
`robots.txt` or `sitemap.xml`: one build serves many sites, and sitemap URLs must be absolute on
|
|
10
|
+
the site's host, which the build can't know.
|
|
11
|
+
- **Declare your routes** in `public/sitemap-paths.txt`, one path per line (`/`, `/menu`,
|
|
12
|
+
`/book`). They go into the sitemap and `llms.txt` on the right host. Pre-rendered pages
|
|
13
|
+
(`about/index.html`) are found automatically.
|
|
14
|
+
- **Canonical address.** A site can answer on its automatic address, a chosen name and a custom
|
|
15
|
+
domain. Every page gets `Link: <https://{canonical}{path}>; rel="canonical"`, so search engines
|
|
16
|
+
consolidate on one: the custom domain, else the chosen name, else the automatic address. Don't set
|
|
17
|
+
your own canonical unless a page has a different canonical page.
|
|
18
|
+
- **Previews stay out of search.** A sandbox collection's sites, and any test build (dev, alpha,
|
|
19
|
+
beta), are served with `X-Robots-Tag: noindex` and a `robots.txt` that disallows everything,
|
|
20
|
+
whatever the build ships.
|
|
21
|
+
|
|
22
|
+
## What you do: `SL.seo` and `SL.site`
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import * as SL from '@proveanything/smartlinks'
|
|
26
|
+
|
|
27
|
+
// Per route, from its data — on every route change:
|
|
28
|
+
SL.seo.head({
|
|
29
|
+
title: `${product.name} — ${brand}`,
|
|
30
|
+
description: product.description, // ~150 characters, says what the page is
|
|
31
|
+
image: product.heroImage?.url, // absolute; used for link previews
|
|
32
|
+
type: 'product', // 'website' | 'article' | 'product'
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Structured data (schema.org JSON-LD). The id names the block, so calling again replaces it:
|
|
36
|
+
SL.seo.jsonLd('page', SL.seo.schema.product(product, { url: location.href, brand }))
|
|
37
|
+
SL.seo.jsonLd('faq', SL.seo.schema.faqPage(faqs.map((f) => ({ question: f.q, answer: f.a }))))
|
|
38
|
+
SL.seo.jsonLd('org', SL.seo.schema.organization(collection))
|
|
39
|
+
|
|
40
|
+
// When the page's data has rendered:
|
|
41
|
+
SL.site.ready()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Builder | For |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `schema.product(product, { url, brand, offer, rating })` | product pages (`offer` only when it's really sold) |
|
|
47
|
+
| `schema.faqPage([{ question, answer }])` | FAQs: rich results, and the answer-shaped content AI search cites |
|
|
48
|
+
| `schema.organization(collection)` | the brand behind the site (home page) |
|
|
49
|
+
| `schema.localBusiness(collection, { type, address, telephone, openingHours })` | a business with a place (`type: 'Florist'`, `'Restaurant'`…) |
|
|
50
|
+
| `schema.breadcrumbs([{ name, url }])` | nested pages |
|
|
51
|
+
| `schema.article({ headline, datePublished, … })` | posts, guides, news |
|
|
52
|
+
|
|
53
|
+
`seo.head` removes tags a previous route set and the next one doesn't. Both are no-ops without a
|
|
54
|
+
`document` (SSR, tests).
|
|
55
|
+
|
|
56
|
+
`site.ready()` tells the platform's page renderer the page is complete, so it can snapshot the
|
|
57
|
+
content for crawlers that don't run JavaScript. Without it, the renderer waits for the network to go
|
|
58
|
+
quiet.
|
|
59
|
+
|
|
60
|
+
## Content that gets found
|
|
61
|
+
|
|
62
|
+
- **Content in the page, not behind interaction.** FAQ answers in `<details>` are fine; answers
|
|
63
|
+
fetched only when a question is clicked aren't seen.
|
|
64
|
+
- **One `<h1>` per page**, a logical heading order, `alt` text on content images.
|
|
65
|
+
- **Answer-shaped writing.** A short summary near the top, question-style headings where they fit.
|
|
66
|
+
This is what AI assistants quote.
|
|
67
|
+
- **Real links** (`<a href>`) between pages, never hash routes.
|
package/openapi.yaml
CHANGED
|
@@ -1347,7 +1347,7 @@ paths:
|
|
|
1347
1347
|
get:
|
|
1348
1348
|
tags:
|
|
1349
1349
|
- sessions
|
|
1350
|
-
summary: AI usage
|
|
1350
|
+
summary: AI usage for a collection (daily totals), grouped by any of model/appId/feature/mode/surface/provider/day over an optional date window (YYYY-MM-DD).
|
|
1351
1351
|
operationId: sessions_usage
|
|
1352
1352
|
security:
|
|
1353
1353
|
- bearerAuth: []
|
|
@@ -16327,17 +16327,37 @@ components:
|
|
|
16327
16327
|
to:
|
|
16328
16328
|
type: string
|
|
16329
16329
|
totals:
|
|
16330
|
-
|
|
16331
|
-
additionalProperties: true
|
|
16330
|
+
$ref: "#/components/schemas/AiUsageTotals"
|
|
16332
16331
|
groups:
|
|
16333
16332
|
type: array
|
|
16334
16333
|
items:
|
|
16335
|
-
|
|
16336
|
-
additionalProperties: true
|
|
16334
|
+
$ref: "#/components/schemas/AiUsageTotals"
|
|
16337
16335
|
required:
|
|
16338
16336
|
- groupBy
|
|
16339
16337
|
- totals
|
|
16340
16338
|
- groups
|
|
16339
|
+
AiUsageTotals:
|
|
16340
|
+
type: object
|
|
16341
|
+
properties:
|
|
16342
|
+
promptTokens:
|
|
16343
|
+
type: number
|
|
16344
|
+
outputTokens:
|
|
16345
|
+
type: number
|
|
16346
|
+
cachedTokens:
|
|
16347
|
+
type: number
|
|
16348
|
+
totalTokens:
|
|
16349
|
+
type: number
|
|
16350
|
+
requests:
|
|
16351
|
+
type: number
|
|
16352
|
+
images:
|
|
16353
|
+
type: number
|
|
16354
|
+
required:
|
|
16355
|
+
- promptTokens
|
|
16356
|
+
- outputTokens
|
|
16357
|
+
- cachedTokens
|
|
16358
|
+
- totalTokens
|
|
16359
|
+
- requests
|
|
16360
|
+
- images
|
|
16341
16361
|
VoiceSessionRequest:
|
|
16342
16362
|
type: object
|
|
16343
16363
|
properties:
|
|
@@ -19439,6 +19459,10 @@ components:
|
|
|
19439
19459
|
$ref: "#/components/schemas/AppManifestExecutor"
|
|
19440
19460
|
functions:
|
|
19441
19461
|
$ref: "#/components/schemas/AppManifestFunctions"
|
|
19462
|
+
data:
|
|
19463
|
+
$ref: "#/components/schemas/AppDataDeclaration"
|
|
19464
|
+
headless:
|
|
19465
|
+
$ref: "#/components/schemas/AppHeadlessDeclaration"
|
|
19442
19466
|
required:
|
|
19443
19467
|
- name
|
|
19444
19468
|
- version
|
|
@@ -25135,6 +25159,161 @@ components:
|
|
|
25135
25159
|
type: boolean
|
|
25136
25160
|
FacetValueDefinition:
|
|
25137
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
|
|
25138
25317
|
CachedData:
|
|
25139
25318
|
type: object
|
|
25140
25319
|
properties:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proveanything/smartlinks",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.38",
|
|
4
4
|
"description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"bin": {
|
|
31
31
|
"smartlinks-register-release": "scripts/register-release.mjs",
|
|
32
32
|
"smartlinks-publish": "scripts/publish.mjs",
|
|
33
|
-
"smartlinks-doctor": "scripts/doctor.mjs"
|
|
33
|
+
"smartlinks-doctor": "scripts/doctor.mjs",
|
|
34
|
+
"smartlinks-headless": "scripts/headless-check.mjs"
|
|
34
35
|
},
|
|
35
36
|
"files": [
|
|
36
37
|
"dist/",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"scripts/register-release.mjs",
|
|
39
40
|
"scripts/publish.mjs",
|
|
40
41
|
"scripts/doctor.mjs",
|
|
42
|
+
"scripts/headless-check.mjs",
|
|
41
43
|
"scripts/lib/",
|
|
42
44
|
"openapi.yaml",
|
|
43
45
|
"README.md"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// smartlinks-headless — check an app's headless-provider declaration (manifest `data` + `headless`).
|
|
4
|
+
//
|
|
5
|
+
// smartlinks-headless [appDir] # the declaration on its own
|
|
6
|
+
// smartlinks-headless [appDir] --collection <id> --app <appId> # also against real public data
|
|
7
|
+
// smartlinks-headless ... --json # machine-readable result
|
|
8
|
+
//
|
|
9
|
+
// With --collection/--app it samples each record type's PUBLIC items from that collection and reports
|
|
10
|
+
// fields the real data has that the declaration doesn't (and values that don't fit their type).
|
|
11
|
+
// API base: --api <url>, else SMARTLINKS_API_BASE, else https://smartlinks.app/api/v1.
|
|
12
|
+
// Spec: docs/headless-providers.md. Exit code: 0 = valid, 1 = errors.
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
16
|
+
import { resolve, dirname, join } from 'node:path'
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const { validate } = await import(pathToFileURL(resolve(here, '../dist/headless.js')).href)
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2)
|
|
23
|
+
const flag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined }
|
|
24
|
+
const json = args.includes('--json')
|
|
25
|
+
const positional = args.filter((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--') && args[i - 1] !== '--json'))
|
|
26
|
+
const appDir = resolve(positional[0] || process.cwd())
|
|
27
|
+
const collectionId = flag('collection')
|
|
28
|
+
const appId = flag('app')
|
|
29
|
+
const apiBase = (flag('api') || process.env.SMARTLINKS_API_BASE || 'https://smartlinks.app/api/v1').replace(/\/+$/, '')
|
|
30
|
+
|
|
31
|
+
const C = process.stdout.isTTY && !json
|
|
32
|
+
? { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', dim: '\x1b[2m', bold: '\x1b[1m', reset: '\x1b[0m' }
|
|
33
|
+
: { red: '', green: '', yellow: '', dim: '', bold: '', reset: '' }
|
|
34
|
+
|
|
35
|
+
function fail(message) {
|
|
36
|
+
if (json) console.log(JSON.stringify({ ok: false, errors: [{ path: '', message }], warnings: [], recipes: {} }))
|
|
37
|
+
else console.error(`${C.red}smartlinks-headless: ${message}${C.reset}`)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const manifestPath = ['public/app.manifest.json', 'app.manifest.json', 'dist/app.manifest.json'].map((p) => join(appDir, p)).find(existsSync)
|
|
42
|
+
if (!manifestPath) fail(`no app.manifest.json under ${appDir} (looked in public/, ., dist/)`)
|
|
43
|
+
let manifest
|
|
44
|
+
try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch (e) { fail(`can't parse ${manifestPath}: ${e.message}`) }
|
|
45
|
+
|
|
46
|
+
// Real public items per record type, when a collection is given.
|
|
47
|
+
const samples = {}
|
|
48
|
+
const sampled = []
|
|
49
|
+
if (collectionId && appId && manifest.data && manifest.data.types) {
|
|
50
|
+
for (const [typeId, type] of Object.entries(manifest.data.types)) {
|
|
51
|
+
if (!type || !type.storage || type.storage.kind !== 'record' || !type.storage.recordType) continue
|
|
52
|
+
const url = `${apiBase}/public/collection/${encodeURIComponent(collectionId)}/app/${encodeURIComponent(appId)}/records?recordType=${encodeURIComponent(type.storage.recordType)}&limit=25`
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(url, { headers: { accept: 'application/json' } })
|
|
55
|
+
if (!res.ok) { sampled.push(`${typeId}: HTTP ${res.status}`); continue }
|
|
56
|
+
const body = await res.json()
|
|
57
|
+
const items = (Array.isArray(body) ? body : body.data || body.items || []).map((r) => (r && r.data) || {})
|
|
58
|
+
samples[typeId] = items
|
|
59
|
+
sampled.push(`${typeId}: ${items.length} public item${items.length === 1 ? '' : 's'}`)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
sampled.push(`${typeId}: ${e.message}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else if (collectionId || appId) {
|
|
65
|
+
fail('--collection and --app go together')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = validate(manifest, { samples })
|
|
69
|
+
|
|
70
|
+
if (json) {
|
|
71
|
+
console.log(JSON.stringify({ ...result, manifest: manifestPath, sampled }, null, 2))
|
|
72
|
+
process.exit(result.ok ? 0 : 1)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`${C.bold}smartlinks-headless${C.reset} ${C.dim}${manifestPath}${C.reset}`)
|
|
76
|
+
if (!manifest.headless) console.log(`${C.dim}No "headless" block: checking the data declaration only.${C.reset}`)
|
|
77
|
+
if (sampled.length) console.log(`${C.dim}Sampled from ${collectionId}: ${sampled.join('; ')}${C.reset}`)
|
|
78
|
+
for (const e of result.errors) console.log(`${C.red}✗ ${e.path}${C.reset} ${e.message}`)
|
|
79
|
+
for (const w of result.warnings) console.log(`${C.yellow}! ${w.path}${C.reset} ${w.message}`)
|
|
80
|
+
if (Object.keys(result.recipes).length) {
|
|
81
|
+
console.log(`\n${C.bold}Read recipes${C.reset}`)
|
|
82
|
+
for (const [t, r] of Object.entries(result.recipes)) console.log(` ${t}\n list: ${r.list}${r.get ? `\n get: ${r.get}` : ''}`)
|
|
83
|
+
}
|
|
84
|
+
console.log(result.ok
|
|
85
|
+
? `\n${C.green}✓ valid${result.warnings.length ? ` (${result.warnings.length} warning${result.warnings.length === 1 ? '' : 's'})` : ''}${C.reset}`
|
|
86
|
+
: `\n${C.red}${result.errors.length} error${result.errors.length === 1 ? '' : 's'}${C.reset}`)
|
|
87
|
+
process.exit(result.ok ? 0 : 1)
|