@proveanything/smartlinks 2.0.57 → 2.0.58

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.
@@ -70,7 +70,8 @@ export declare namespace integrations {
70
70
  }>;
71
71
  /**
72
72
  * Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` →
73
- * ConnectorTargetSchema, `sample` → ConnectorRecords, `transform` (a dry run) → ConnectorTransformResult.
73
+ * ConnectorTargetSchema, `sample` → ConnectorRecords, and dry runs of `shape` ({ records }) →
74
+ * ConnectorShapeResult and `serialize` ({ documents }) → ConnectorSerializeResult.
74
75
  * The server adds the installation; pass `flowId` to layer that flow's settings.
75
76
  * POST /integrations/connectors/:appId/steps/:step
76
77
  */
@@ -152,7 +152,8 @@ export var integrations;
152
152
  integrations.saveConnectorInstallation = saveConnectorInstallation;
153
153
  /**
154
154
  * Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` →
155
- * ConnectorTargetSchema, `sample` → ConnectorRecords, `transform` (a dry run) → ConnectorTransformResult.
155
+ * ConnectorTargetSchema, `sample` → ConnectorRecords, and dry runs of `shape` ({ records }) →
156
+ * ConnectorShapeResult and `serialize` ({ documents }) → ConnectorSerializeResult.
156
157
  * The server adds the installation; pass `flowId` to layer that flow's settings.
157
158
  * POST /integrations/connectors/:appId/steps/:step
158
159
  */
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.57 | Generated: 2026-10-13T11:42:19.247Z
3
+ Version: 2.0.58 | Generated: 2026-10-13T14:08:01.791Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -65,7 +65,7 @@ For detailed guides on specific features:
65
65
  - **[Analytics Metadata Conventions](analytics-metadata-conventions.md)** - Standard recommended keys and conventions for analytics metadata fields
66
66
  - **[Loyalty: Points, Members & Earning Rules](loyalty.md)** - Loyalty schemes, automatic point earning via interaction rules, member balances, transaction history, and manual adjustments
67
67
  - **[Integrations](integrations.md)** - Inbound/outbound integration flows + the sealed-secret store; triggers (manual/event/schedule), field mappings, and the Syndigo/Event Hub outbound path
68
- - **[Connectors](connectors.md)** - Connector apps: a manifest `connector` block + server functions (testConnection/describeTarget/sample/transform/send/fetch) that the integration engine and console call for one external provider
68
+ - **[Connectors](connectors.md)** - Connector apps (adapters for one external provider): a manifest `connector` block + server functions (testConnection/describeTarget/sample/fetch/shape/serialize/send); core does the mapping
69
69
  - **[AI Tools & Skills](ai-tools-and-skills.md)** - The AI capability catalog: skills apps invoke by name (e.g. research.brand), the tools the AI reaches for (web fetch/screenshot/brand assets/image gen), the agent loop, and how apps discover them
70
70
  - **[Deep Link Discovery](deep-link-discovery.md)** - Registering and discovering navigable app states for portal menus and AI orchestration
71
71
  - **[AI Guide Template](ai-guide-template.md)** - A sample for an app on how to build an AI setup guide
@@ -2908,7 +2908,8 @@ interface AppConnectorDeclaration {
2908
2908
  testConnection?: string
2909
2909
  describeTarget?: string
2910
2910
  sample?: string
2911
- transform?: string
2911
+ shape?: string
2912
+ serialize?: string
2912
2913
  send?: string
2913
2914
  fetch?: string
2914
2915
  }
@@ -2963,18 +2964,34 @@ interface ConnectorRecords {
2963
2964
  }
2964
2965
  ```
2965
2966
 
2966
- **ConnectorTransformInput** (interface)
2967
+ **ConnectorShapeInput** (interface)
2967
2968
  ```typescript
2968
- interface ConnectorTransformInput {
2969
+ interface ConnectorShapeInput {
2969
2970
  installation: ConnectorInstallation
2970
2971
  records: Array<Record<string, unknown>>
2971
- mapping: FieldMapping[]
2972
2972
  }
2973
2973
  ```
2974
2974
 
2975
- **ConnectorTransformResult** (interface)
2975
+ **ConnectorShapeResult** (interface)
2976
2976
  ```typescript
2977
- interface ConnectorTransformResult {
2977
+ interface ConnectorShapeResult {
2978
+ documents: Array<Record<string, unknown> | null>
2979
+ skipped?: Array<{ index: number; reason: string }>
2980
+ }
2981
+ ```
2982
+
2983
+ **ConnectorSerializeInput** (interface)
2984
+ ```typescript
2985
+ interface ConnectorSerializeInput {
2986
+ installation: ConnectorInstallation
2987
+ documents: Array<Record<string, unknown>>
2988
+ eventType?: string
2989
+ }
2990
+ ```
2991
+
2992
+ **ConnectorSerializeResult** (interface)
2993
+ ```typescript
2994
+ interface ConnectorSerializeResult {
2978
2995
  payloads: Array<unknown | null>
2979
2996
  skipped?: Array<{ index: number; reason: string }>
2980
2997
  }
@@ -7374,7 +7391,7 @@ interface RecordTypesResponse {
7374
7391
 
7375
7392
  **UpdateFlowInput** = `Partial<Omit<CreateFlowInput, 'direction'>> & {`
7376
7393
 
7377
- **ConnectorConsoleStep** = `'testConnection' | 'describeTarget' | 'sample' | 'transform'`
7394
+ **ConnectorConsoleStep** = `'testConnection' | 'describeTarget' | 'sample' | 'shape' | 'serialize'`
7378
7395
 
7379
7396
  **RunTrigger** = `'manual' | 'event' | 'schedule' | 'test'`
7380
7397
 
@@ -11702,7 +11719,7 @@ Save a connector's non-secret Connect-form values and/or settings. PUT /integrat
11702
11719
  appId: string,
11703
11720
  step: ConnectorConsoleStep,
11704
11721
  input: Record<string, unknown> & { flowId?: string } = {}) → `Promise<T>`
11705
- Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` → ConnectorTargetSchema, `sample` → ConnectorRecords, `transform` (a dry run) → ConnectorTransformResult. The server adds the installation; pass `flowId` to layer that flow's settings. POST /integrations/connectors/:appId/steps/:step
11722
+ Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` → ConnectorTargetSchema, `sample` → ConnectorRecords, and dry runs of `shape` ({ records }) → ConnectorShapeResult and `serialize` ({ documents }) → ConnectorSerializeResult. The server adds the installation; pass `flowId` to layer that flow's settings. POST /integrations/connectors/:appId/steps/:step
11706
11723
 
11707
11724
  ### interactions
11708
11725
 
@@ -440,7 +440,7 @@ this is just the manifest shape.
440
440
 
441
441
  Marks the app as an integration **connector** for one external provider: the integrations console
442
442
  lists it and draws its connect form and settings, and the integration engine calls its server
443
- functions (`testConnection`, `describeTarget`, `sample`, `transform`, `send`, `fetch`). The full
443
+ functions (`testConnection`, `describeTarget`, `sample`, `fetch`, `shape`, `serialize`, `send`). It adapts the provider's format; core does each customer's mapping. The full
444
444
  contract is in the **[Connectors guide](connectors.md)**.
445
445
 
446
446
  ---
@@ -9,16 +9,22 @@ Three layers, each with one job:
9
9
 
10
10
  | Layer | Holds |
11
11
  |---|---|
12
- | **Engine** (the API) | [Integration flows](integrations.md), runs and their logs, triggers (events, schedule, Run now), the secret store, the deterministic field mapper, generic transports (HTTP, Event Hub). No provider code. |
12
+ | **Engine** (the API) | [Integration flows](integrations.md), runs and their logs, triggers (events, schedule, Run now), the secret store, **the mapping** (each customer's field mappings and rules), generic transports (HTTP, Event Hub). No provider code. |
13
13
  | **Integrations console** (a core app) | The one admin screen: installed connectors, connect forms, settings, the mapper with AI assist, flows, runs. |
14
- | **Connector apps** | One per provider: its connect form, settings, target schema, transform, send/fetch, and any provider-specific panels. |
14
+ | **Connector apps** | One per provider: an **adapter**. Talks to the provider and converts between its format and a clean, provider-neutral document. |
15
+
16
+ **A connector adapts; core maps.** A connector never holds a customer's mapping. It turns the
17
+ provider's records into provider-neutral documents (`shape`), or provider-ready payloads out of
18
+ documents core has already mapped (`serialize`). Every customer's mapping, routing and rules live in
19
+ core, so they work the same for every provider and can be replayed and traced. This is the line
20
+ SnapLogic (endpoint Snaps vs the Mapper), MuleSoft and Airbyte all draw.
15
21
 
16
22
  A flow that only posts JSON to a webhook or an Event Hub needs no connector; it uses the engine's
17
23
  generic transports (see [integrations.md](integrations.md)).
18
24
 
19
25
  **No AI at run time.** The AI helps an admin *author* a mapping in the console (from the connector's
20
26
  target schema plus real sample records); a person approves it, and it is saved as data and runs
21
- deterministically. A connector's `transform` must be deterministic too.
27
+ deterministically. A connector's `shape` and `serialize` must be deterministic too.
22
28
 
23
29
  ---
24
30
 
@@ -33,8 +39,10 @@ deterministically. A connector's `transform` must be deterministic too.
33
39
  "entities": ["product"],
34
40
  "connection": {
35
41
  "fields": [
36
- { "name": "appId", "label": "App ID", "secret": "oneworldsync-app-id", "required": true },
37
- { "name": "secretKey", "label": "Secret key", "secret": "oneworldsync-secret-key", "required": true },
42
+ // Optional: SmartLinks' own credentials are the default; a customer may enter theirs.
43
+ { "name": "appId", "label": "App ID", "secret": "oneworldsync-app-id" },
44
+ { "name": "secretKey", "label": "Secret key", "secret": "oneworldsync-secret-key" },
45
+ { "name": "gln", "label": "GLN", "required": true },
38
46
  { "name": "environment", "label": "Environment", "type": "select",
39
47
  "options": [ { "value": "preprod", "label": "Pre-production" }, { "value": "production", "label": "Production" } ] }
40
48
  ]
@@ -44,12 +52,14 @@ deterministically. A connector's `transform` must be deterministic too.
44
52
  "properties": { "targetMarket": { "type": "string", "default": "GB" } }
45
53
  },
46
54
  "presets": [
47
- { "id": "import-products", "title": "Import products", "direction": "inbound", "entity": "product" }
55
+ { "id": "import-products", "title": "Import products", "direction": "inbound", "entity": "product",
56
+ "flow": { "config": { "fieldMappings": [ /* a suggested mapping the customer can edit */ ] } } }
48
57
  ],
49
58
  "functions": { // names of this app's server functions
50
59
  "testConnection": "testConnection",
51
60
  "describeTarget": "describeTarget",
52
61
  "sample": "sample",
62
+ "shape": "shape",
53
63
  "fetch": "fetch"
54
64
  },
55
65
  "panels": [ { "id": "explorer", "title": "Explorer", "component": "Explorer" } ],
@@ -74,9 +84,9 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
74
84
  | `title` | Provider display name; defaults to the app name. |
75
85
  | `directions` | `inbound` (provider → SmartLinks), `outbound` (SmartLinks → provider), or both. |
76
86
  | `entities` | SmartLinks entities it maps, e.g. `["product"]`. |
77
- | `connection.fields[]` | The console's **Connect** form. A field with `secret` is stored as this app's secret of that name (write-only; never returned to the browser). Other fields are saved in the installation's connection settings. |
87
+ | `connection.fields[]` | The console's **Connect** form. A field with `secret` is stored as this app's secret of that name on the collection (write-only; never returned to the browser). Other fields are saved in the installation's connection settings. |
78
88
  | `settings` | JSON Schema for installation settings; the console renders it as a form. |
79
- | `presets[]` | Ready-made flows ("Products → provider") with flow defaults. |
89
+ | `presets[]` | Ready-made flows ("Products → provider"). A preset's `flow.config.fieldMappings` is a **suggested** mapping: the connector's knowledge of the provider, which the customer accepts or edits in core. |
80
90
  | `functions` | Which of this app's server functions implement each step (below). Fill in only what applies. |
81
91
  | `panels[]` | Provider-specific screens the console mounts (an API explorer, a status page). |
82
92
  | `aiGuide` | A guide the mapping assistant reads: the provider's quirks, required fields, good defaults. |
@@ -87,28 +97,37 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
87
97
 
88
98
  Each step names one of the app's server functions. Declare them as **http POST, `visibility:
89
99
  "admin"`, `authority: "collection"`**: the engine calls them with no signed-in user (a product edit, a
90
- schedule), and the console calls the same functions while an admin configures the flow
91
- (`functions.callAdmin(collectionId, name, input, { appId })`). The input arrives as `event.body`;
92
- return the result as JSON. Capabilities still cap what each one can do: declare the provider's hosts
93
- (`network:<host>`) and the secrets it reads (`secrets:<name>`).
100
+ schedule), and the console calls the same functions while an admin configures the flow. The input
101
+ arrives as `event.body`; return the result as JSON. Capabilities still cap what each one can do:
102
+ declare the provider's hosts (`network:<host>`) and the secrets it reads (`secrets:<name>`).
94
103
 
95
104
  | Step | Called by | Input (`event.body`) | Returns |
96
105
  |---|---|---|---|
97
106
  | `testConnection` | Connect form, **Test** | `{ installation }` | `ConnectorTestResult` `{ ok, message }` |
98
- | `describeTarget` | Mapper opens | `{ installation }` | `ConnectorTargetSchema` `{ fields: [{ path, label, type, required, enum }] }`: the provider's fields *for this customer* |
107
+ | `describeTarget` | Mapper opens | `{ installation }` | `ConnectorTargetSchema` `{ fields: [{ path, label, type, required, enum }] }`: the provider's fields *for this customer*. Outbound mappings target these paths. |
99
108
  | `sample` | Mapper, AI assist, dry run | `{ installation, limit }` | `ConnectorRecords` `{ records }`: real provider records |
100
- | `transform` | Every run | `ConnectorTransformInput` `{ installation, records, mapping }` | `ConnectorTransformResult` `{ payloads, skipped }`: one payload per record, same order; `null` skips one |
101
- | `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
102
109
  | `fetch` | Every inbound run | `ConnectorFetchInput` `{ installation, cursor, limit }` | `ConnectorRecords` `{ records, nextCursor }`: one page per call |
110
+ | `shape` | Every inbound run (and sample dry runs) | `ConnectorShapeInput` `{ installation, records }` | `ConnectorShapeResult` `{ documents, skipped }`: one provider-neutral document per record, same order; `null` skips one |
111
+ | `serialize` | Every outbound run | `ConnectorSerializeInput` `{ installation, documents, eventType? }`: documents core mapped to `describeTarget` paths; a deletion arrives as `{ id, _deleted: true }` | `ConnectorSerializeResult` `{ payloads, skipped }`: one provider payload per document, same order; `null` skips one |
112
+ | `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
103
113
 
104
- What happens when a step is left out:
114
+ **How a run flows:**
115
+ - **Inbound:** `fetch` (paged) → `shape` → core maps the documents with the flow's mapping → upsert.
116
+ - **Outbound:** gather the SmartLinks records → core maps them to the provider's fields → `serialize`
117
+ → `send` (or a generic transport).
105
118
 
106
- - **No `transform`:** the engine's own mapper runs the flow's `fieldMappings` (direct, static, Liquid
107
- template).
108
- - **No `send`:** the flow's own transport: HTTP (`config.connection`), or Azure Event Hub with
119
+ **What happens when a step is left out:**
120
+ - **No `shape`:** core maps the provider records as they come.
121
+ - **No `serialize`:** the mapped documents are sent as they are.
122
+ - **No `send`:** the flow's own transport is used: HTTP (`config.connection`), or Azure Event Hub with
109
123
  `config.transport: 'eventhub'` and `config.eventHub`.
110
124
  - **No `describeTarget`:** the mapper offers free-text target paths.
111
125
 
126
+ **What `shape` should produce:** plain, predictable JSON a person can map without reading the
127
+ provider's documentation. Flatten the provider's envelopes, name things plainly, keep codes alongside
128
+ their meanings, and put repeated sections (ingredients, allergens, nutrients, images) in arrays. The
129
+ same provider record always gives the same document.
130
+
112
131
  ### `installation`
113
132
 
114
133
  Every step receives the installation it serves:
@@ -124,13 +143,15 @@ interface ConnectorInstallation {
124
143
  ```
125
144
 
126
145
  Secrets are never in it. Read them in the function with `ctx.secrets.get('<name>')`, which needs the
127
- matching `secrets:<name>` capability.
146
+ matching `secrets:<name>` capability. A secret the customer set on their collection wins; otherwise the
147
+ connector's own app-level secret of that name (set in Forge) is used, so a connector can ship with
148
+ SmartLinks' credentials as the default.
128
149
 
129
- ### Example: `fetch`
150
+ ### Example: `fetch` and `shape`
130
151
 
131
152
  ```ts
132
153
  // src/functions/index.ts (the app's functions bundle)
133
- import type { ConnectorFetchInput, ConnectorRecords } from '@proveanything/smartlinks'
154
+ import type { ConnectorFetchInput, ConnectorRecords, ConnectorShapeInput, ConnectorShapeResult } from '@proveanything/smartlinks'
134
155
 
135
156
  export async function fetch(ctx: any, event: any): Promise<ConnectorRecords> {
136
157
  const { installation, cursor, limit = 50 } = event.body as ConnectorFetchInput
@@ -140,6 +161,18 @@ export async function fetch(ctx: any, event: any): Promise<ConnectorRecords> {
140
161
  const page = await res.json()
141
162
  return { records: page.items, nextCursor: page.next ?? null }
142
163
  }
164
+
165
+ export async function shape(_ctx: any, event: any): Promise<ConnectorShapeResult> {
166
+ const { records } = event.body as ConnectorShapeInput
167
+ return {
168
+ documents: records.map((r: any) => ({
169
+ gtin: r.item?.gtin,
170
+ name: r.item?.tradeItemDescription?.[0]?.value,
171
+ brand: r.item?.brandName,
172
+ ingredients: (r.item?.ingredients ?? []).map((i: any) => ({ name: i.name, percent: i.percentage })),
173
+ })),
174
+ }
175
+ }
143
176
  ```
144
177
 
145
178
  ---
@@ -148,9 +181,9 @@ export async function fetch(ctx: any, event: any): Promise<ConnectorRecords> {
148
181
 
149
182
  | What | Where |
150
183
  |---|---|
151
- | Connect-form values (non-secret), installation settings | The connector app's config on the collection: `appConfiguration` for `appId`, under `connector.connection` and `connector.settings`. |
152
- | Credentials | The connector app's secrets, by the names its fields declare. |
153
- | One flow's use of the connector | The flow: `config.connector = { appId, preset?, settings? }` (per-flow settings override the installation's), plus `config.fieldMappings`. |
184
+ | Connect-form values (non-secret), installation settings | The connector app's config on the collection, in its admin-only zone (`admin.connector.connection`, `admin.connector.settings`). |
185
+ | Credentials | The connector app's secrets, by the names its fields declare (the customer's on their collection, else the connector's default). |
186
+ | One flow's use of the connector | The flow: `config.connector = { appId, preset?, settings? }` (per-flow settings override the installation's), plus `config.fieldMappings`, which core runs. |
154
187
 
155
188
  ```ts
156
189
  await integrations.createFlow(collectionId, {
@@ -162,7 +195,8 @@ await integrations.createFlow(collectionId, {
162
195
  targetEntity: 'product',
163
196
  config: {
164
197
  connector: { appId: 'oneworldsync', preset: 'import-products' },
165
- fieldMappings: [ { targetPath: 'name', sourcePath: 'item.tradeItemDescription', transformType: 'direct' } ],
198
+ // Maps the connector's shaped documents (not the raw provider records) onto the product.
199
+ fieldMappings: [ { targetPath: 'name', sourcePath: 'name', transformType: 'direct' } ],
166
200
  },
167
201
  })
168
202
  ```
@@ -181,7 +215,9 @@ await secrets.put(collectionId, 'provider-key', value, { appId }) /
181
215
  await integrations.saveConnectorInstallation(collectionId, appId, { connection: { environment: 'production' } })
182
216
  const test = await integrations.runConnectorStep(collectionId, appId, 'testConnection')
183
217
  const target = await integrations.runConnectorStep(collectionId, appId, 'describeTarget')
184
- const dryRun = await integrations.runConnectorStep(collectionId, appId, 'transform', { records, mapping })
218
+ const { records } = await integrations.runConnectorStep(collectionId, appId, 'sample', { limit: 5 })
219
+ const shaped = await integrations.runConnectorStep(collectionId, appId, 'shape', { records }) // dry run
220
+ const wire = await integrations.runConnectorStep(collectionId, appId, 'serialize', { documents }) // dry run
185
221
  ```
186
222
 
187
223
  `send` and `fetch` only run inside a flow. A flow run's summary carries `sent` (outbound) or
@@ -192,8 +228,9 @@ const dryRun = await integrations.runConnectorStep(collectionId, appId, 'transfo
192
228
  ## Panels
193
229
 
194
230
  A connector can contribute screens to the integrations console (an API explorer, say). Each
195
- `panels[]` entry names a component exported from the app's widgets bundle; the console mounts it host-side and passes it the
196
- collection and the installation. Panels are admin-only and call the connector's own functions.
231
+ `panels[]` entry names a component exported from the app's widgets bundle; the console mounts it
232
+ host-side and passes it the collection and the installation. Panels are admin-only and call the
233
+ connector's own functions.
197
234
 
198
235
  ---
199
236
 
@@ -1,68 +1,69 @@
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
- - **List every page** in `public/sitemap-paths.txt`, one `<path> [Title]` per line, in menu order
12
- (`/ Home`, `/menu Menu`, `/book Book a table`). They go into the sitemap and `llms.txt` (titled)
13
- on the right host, and Forge's preview uses the same list as its page menu. Keep it in step with
14
- the router. Pre-rendered pages (`about/index.html`) are found automatically.
15
- - **Canonical address.** A site can answer on its automatic address, a chosen name and a custom
16
- domain. Every page gets `Link: <https://{canonical}{path}>; rel="canonical"`, so search engines
17
- consolidate on one: the custom domain, else the chosen name, else the automatic address. Don't set
18
- your own canonical unless a page has a different canonical page.
19
- - **Previews stay out of search.** A sandbox collection's sites, and any test build (dev, alpha,
20
- beta), are served with `X-Robots-Tag: noindex` and a `robots.txt` that disallows everything,
21
- whatever the build ships.
22
-
23
- ## What you do: `SL.seo` and `SL.site`
24
-
25
- ```ts
26
- import * as SL from '@proveanything/smartlinks'
27
-
28
- // Per route, from its data — on every route change:
29
- SL.seo.head({
30
- title: `${product.name} — ${brand}`,
31
- description: product.description, // ~150 characters, says what the page is
32
- image: product.heroImage?.url, // absolute; used for link previews
33
- type: 'product', // 'website' | 'article' | 'product'
34
- })
35
-
36
- // Structured data (schema.org JSON-LD). The id names the block, so calling again replaces it:
37
- SL.seo.jsonLd('page', SL.seo.schema.product(product, { url: location.href, brand }))
38
- SL.seo.jsonLd('faq', SL.seo.schema.faqPage(faqs.map((f) => ({ question: f.q, answer: f.a }))))
39
- SL.seo.jsonLd('org', SL.seo.schema.organization(collection))
40
-
41
- // When the page's data has rendered:
42
- SL.site.ready()
43
- ```
44
-
45
- | Builder | For |
46
- |---|---|
47
- | `schema.product(product, { url, brand, offer, rating })` | product pages (`offer` only when it's really sold) |
48
- | `schema.faqPage([{ question, answer }])` | FAQs: rich results, and the answer-shaped content AI search cites |
49
- | `schema.organization(collection)` | the brand behind the site (home page) |
50
- | `schema.localBusiness(collection, { type, address, telephone, openingHours })` | a business with a place (`type: 'Florist'`, `'Restaurant'`…) |
51
- | `schema.breadcrumbs([{ name, url }])` | nested pages |
52
- | `schema.article({ headline, datePublished, … })` | posts, guides, news |
53
-
54
- `seo.head` removes tags a previous route set and the next one doesn't. Both are no-ops without a
55
- `document` (SSR, tests).
56
-
57
- `site.ready()` tells the platform's page renderer the page is complete, so it can snapshot the
58
- content for crawlers that don't run JavaScript. Without it, the renderer waits for the network to go
59
- quiet.
60
-
61
- ## Content that gets found
62
-
63
- - **Content in the page, not behind interaction.** FAQ answers in `<details>` are fine; answers
64
- fetched only when a question is clicked aren't seen.
65
- - **One `<h1>` per page**, a logical heading order, `alt` text on content images.
66
- - **Answer-shaped writing.** A short summary near the top, question-style headings where they fit.
67
- This is what AI assistants quote.
68
- - **Real links** (`<a href>`) between pages, never hash routes.
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
+ - **Every page is server-rendered.** A website runs on the SmartLinks site runtime (see
9
+ [websites.md](websites.md)): the first response for any URL is the finished HTML, with its title,
10
+ description, link-preview image, structured data and real status codes (404s are 404s). Search
11
+ engines, AI crawlers and link previews read it without running JavaScript.
12
+ - **`/robots.txt`, `/sitemap.xml`, `/llms.txt`** are generated on the site's own address. Don't ship
13
+ `robots.txt` or `sitemap.xml`: one build serves many sites, and sitemap URLs must be absolute on
14
+ the site's host, which the build can't know.
15
+ - **List every page** in `public/sitemap-paths.txt`, one `<path> [Title]` per line, in menu order
16
+ (`/ Home`, `/menu Menu`, `/book Book a table`). They go into the sitemap and `llms.txt` (titled)
17
+ on the right host, and Forge's preview uses the same list as its page menu. Keep it in step with
18
+ the router.
19
+ - **Canonical address.** A site can answer on its automatic address, a chosen name and a custom
20
+ domain. Every page gets `Link: <https://{canonical}{path}>; rel="canonical"`, so search engines
21
+ consolidate on one: the custom domain, else the chosen name, else the automatic address. Don't set
22
+ your own canonical unless a page has a different canonical page.
23
+ - **Previews stay out of search.** A sandbox collection's sites, and any test build (dev, alpha,
24
+ beta), are served with `X-Robots-Tag: noindex` and a `robots.txt` that disallows everything,
25
+ whatever the build ships.
26
+
27
+ ## What you do: `SL.seo` and `SL.site`
28
+
29
+ ```ts
30
+ import * as SL from '@proveanything/smartlinks'
31
+
32
+ // Per route, from its data — on every route change:
33
+ SL.seo.head({
34
+ title: `${product.name} — ${brand}`,
35
+ description: product.description, // ~150 characters, says what the page is
36
+ image: product.heroImage?.url, // absolute; used for link previews
37
+ type: 'product', // 'website' | 'article' | 'product'
38
+ })
39
+
40
+ // Structured data (schema.org JSON-LD). The id names the block, so calling again replaces it:
41
+ SL.seo.jsonLd('page', SL.seo.schema.product(product, { url: location.href, brand }))
42
+ SL.seo.jsonLd('faq', SL.seo.schema.faqPage(faqs.map((f) => ({ question: f.q, answer: f.a }))))
43
+ SL.seo.jsonLd('org', SL.seo.schema.organization(collection))
44
+
45
+ ```
46
+
47
+ | Builder | For |
48
+ |---|---|
49
+ | `schema.product(product, { url, brand, offer, rating })` | product pages (`offer` only when it's really sold) |
50
+ | `schema.faqPage([{ question, answer }])` | FAQs: rich results, and the answer-shaped content AI search cites |
51
+ | `schema.organization(collection)` | the brand behind the site (home page) |
52
+ | `schema.localBusiness(collection, { type, address, telephone, openingHours })` | a business with a place (`type: 'Florist'`, `'Restaurant'`…) |
53
+ | `schema.breadcrumbs([{ name, url }])` | nested pages |
54
+ | `schema.article({ headline, datePublished, … })` | posts, guides, news |
55
+
56
+ `seo.head` removes tags a previous route set and the next one doesn't. Both are no-ops without a
57
+ `document`. In a server-rendered site, prefer the router's own head support (TanStack Start's route
58
+ `head()`), so the tags are in the server's HTML; use `SL.seo.schema.*` to build the JSON-LD for it.
59
+
60
+ `SL.site.ready()` is no longer needed: pages arrive finished from the server.
61
+
62
+ ## Content that gets found
63
+
64
+ - **Content in the page, not behind interaction.** FAQ answers in `<details>` are fine; answers
65
+ fetched only when a question is clicked aren't seen.
66
+ - **One `<h1>` per page**, a logical heading order, `alt` text on content images.
67
+ - **Answer-shaped writing.** A short summary near the top, question-style headings where they fit.
68
+ This is what AI assistants quote.
69
+ - **Real links** (`<a href>`) between pages, never hash routes.
@@ -0,0 +1,91 @@
1
+ # Websites: server-rendered on the SmartLinks site runtime
2
+
3
+ A website built on SmartLinks is an app whose public side is a **server-rendered site** at its own
4
+ address: an automatic `*.smartlinks.host` name, a chosen name, or the business's own domain. Every
5
+ page arrives as finished HTML, so search engines, AI crawlers and link previews see the real
6
+ content, and visitors see it before any JavaScript runs.
7
+
8
+ ## How a request is served
9
+
10
+ ```
11
+ visitor / crawler / agent
12
+ └─► SmartLinks site router (edge)
13
+ ├─ /robots.txt, /sitemap.xml, /llms.txt made by the platform (site-seo.md)
14
+ ├─ a file in your build (has an extension) served from the build, cached at the edge
15
+ └─ anything else your site's server renders it
16
+ • runs in an isolated worker, close to the SmartLinks API
17
+ • may call the SmartLinks API and the hosts you declare, nothing else
18
+ • request header x-sl-site says which site it is (SL.site.fromRequest)
19
+ ```
20
+
21
+ The router also adds the canonical or noindex header, the security headers, and
22
+ `window.__SMARTLINKS_SITE__` to every HTML page (read in the browser with `SL.site.current()` or
23
+ `SL.readContext()`).
24
+
25
+ ## The build
26
+
27
+ The website template (TanStack Start) produces this; you don't write it by hand.
28
+
29
+ ```
30
+ dist/
31
+ ├── index.html? / assets/… the client build (hashed files: cached for a year)
32
+ ├── sitemap-paths.txt your pages, "<path> [Title]" per line (site-seo.md)
33
+ ├── _server/ the server bundle: one ES module entry + its chunks
34
+ │ └── index.mjs exports default { fetch(request, env, ctx) } (WinterTC)
35
+ └── app.manifest.json
36
+ ```
37
+
38
+ Manifest:
39
+
40
+ ```jsonc
41
+ "site": {
42
+ "server": {
43
+ "main": "_server/index.mjs", // the server entry, inside the build
44
+ "compatibilityFlags": ["nodejs_compat"] // allowed: nodejs_compat, nodejs_compat_v2, nodejs_als
45
+ },
46
+ "network": ["api.example-weather.com"] // other hosts the server may call (optional)
47
+ }
48
+ ```
49
+
50
+ On publish, the server folder becomes the site's server for that channel. Promote copies the same
51
+ bytes. `_server/` is never served to visitors.
52
+
53
+ ## The server's rules
54
+
55
+ - **Which site:** `const site = SL.site.fromRequest(request)` gives `{ host, collectionId, appId,
56
+ channel, canonicalHost }`. One build serves every site it's installed on, so always take the
57
+ collection from here, never from a constant.
58
+ - **Data:** call the SmartLinks API with the SDK as you would in the browser. The server reads what
59
+ an anonymous visitor could read on that site, and only that collection. It holds no keys and no
60
+ signed-in user.
61
+ - **Writes** (contact forms, entries, sign-ups) go to your app's **server functions**
62
+ (server-functions.md), called from the browser as today. Their capabilities, secrets and limits
63
+ apply.
64
+ - **Other hosts:** only those in `site.network`. Anything else is refused (`403 Blocked`).
65
+ - **Limits per request:** a CPU-time budget and a cap on outgoing calls. A page that needs more is
66
+ doing work that belongs in a server function or a scheduled job.
67
+ - **No secrets** in the server bundle and no Node built-ins beyond `nodejs_compat`.
68
+
69
+ ## Caching
70
+
71
+ The router keeps a rendered page at the edge when your response says it may:
72
+
73
+ | Your response | At the edge |
74
+ |---|---|
75
+ | `Cache-Control: public, s-maxage=300` (the template's default for pages) | kept up to 5 minutes |
76
+ | `private`, `no-store`, `no-cache`, or a `Set-Cookie` | never kept |
77
+ | no `Cache-Control` | never kept |
78
+
79
+ Cached pages are keyed by the release and the collection's **content generation**: when products,
80
+ public app records, app settings or the collection change, the generation moves and every page of
81
+ the collection's sites re-renders on its next visit (within about 30 seconds). Publishing a release
82
+ does the same. Tracking parameters (`utm_*`, `gclid`, `fbclid`…) don't create separate entries.
83
+
84
+ Keep pages the same for every visitor. Anything personal (a signed-in owner's products, a basket)
85
+ renders in the browser after load.
86
+
87
+ ## Locally
88
+
89
+ `npm run dev` runs the site with your dev collection (`.env`: `VITE_SL_COLLECTION_ID`), because
90
+ there's no router to say which site it is: `SL.site.fromRequest()` is null, so fall back to it.
91
+ Forge's preview runs the same server code the site runtime does.
package/dist/openapi.yaml CHANGED
@@ -19766,7 +19766,9 @@ components:
19766
19766
  type: string
19767
19767
  sample:
19768
19768
  type: string
19769
- transform:
19769
+ shape:
19770
+ type: string
19771
+ serialize:
19770
19772
  type: string
19771
19773
  send:
19772
19774
  type: string
@@ -19860,7 +19862,7 @@ components:
19860
19862
  type: string
19861
19863
  required:
19862
19864
  - records
19863
- ConnectorTransformInput:
19865
+ ConnectorShapeInput:
19864
19866
  type: object
19865
19867
  properties:
19866
19868
  installation:
@@ -19870,15 +19872,40 @@ components:
19870
19872
  items:
19871
19873
  type: object
19872
19874
  additionalProperties: true
19873
- mapping:
19875
+ required:
19876
+ - installation
19877
+ - records
19878
+ ConnectorShapeResult:
19879
+ type: object
19880
+ properties:
19881
+ documents:
19874
19882
  type: array
19875
19883
  items:
19876
- $ref: "#/components/schemas/FieldMapping"
19884
+ type: object
19885
+ additionalProperties: true
19886
+ skipped:
19887
+ type: array
19888
+ items:
19889
+ type: object
19890
+ additionalProperties: true
19891
+ required:
19892
+ - documents
19893
+ ConnectorSerializeInput:
19894
+ type: object
19895
+ properties:
19896
+ installation:
19897
+ $ref: "#/components/schemas/ConnectorInstallation"
19898
+ documents:
19899
+ type: array
19900
+ items:
19901
+ type: object
19902
+ additionalProperties: true
19903
+ eventType:
19904
+ type: string
19877
19905
  required:
19878
19906
  - installation
19879
- - records
19880
- - mapping
19881
- ConnectorTransformResult:
19907
+ - documents
19908
+ ConnectorSerializeResult:
19882
19909
  type: object
19883
19910
  properties:
19884
19911
  payloads:
@@ -26734,7 +26761,8 @@ components:
26734
26761
  - testConnection
26735
26762
  - describeTarget
26736
26763
  - sample
26737
- - transform
26764
+ - shape
26765
+ - serialize
26738
26766
  RunTrigger:
26739
26767
  type: string
26740
26768
  enum: