@storystick/next 0.0.0-stage → 0.2.0-rc.0

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.
Files changed (121) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +31 -0
  3. package/README.md +522 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/cache-tags.d.ts +2 -0
  6. package/dist/cache-tags.js +12 -0
  7. package/dist/clean.d.ts +1 -0
  8. package/dist/clean.js +1 -0
  9. package/dist/code-version.d.ts +29 -0
  10. package/dist/code-version.js +48 -0
  11. package/dist/file.d.ts +1 -0
  12. package/dist/file.js +1 -0
  13. package/dist/i18n-seo.d.ts +1 -0
  14. package/dist/i18n-seo.js +1 -0
  15. package/dist/image.d.ts +11 -0
  16. package/dist/image.js +22 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +3 -0
  19. package/dist/index.react-server.d.ts +3 -0
  20. package/dist/index.react-server.js +12 -0
  21. package/dist/link.d.ts +10 -0
  22. package/dist/link.js +8 -0
  23. package/dist/preview/index.d.ts +29 -0
  24. package/dist/preview/index.js +24 -0
  25. package/dist/preview/internal.d.ts +61 -0
  26. package/dist/preview/internal.js +97 -0
  27. package/dist/preview/preview-request-identity.d.ts +4 -0
  28. package/dist/preview/preview-request-identity.js +63 -0
  29. package/dist/preview/provenance-config.d.ts +42 -0
  30. package/dist/preview/provenance-config.js +132 -0
  31. package/dist/preview/provenance-frame-builder.d.ts +6 -0
  32. package/dist/preview/provenance-frame-builder.js +10 -0
  33. package/dist/preview/provenance-response.d.ts +5 -0
  34. package/dist/preview/provenance-response.js +9 -0
  35. package/dist/preview/provenance-runtime-adapter.d.ts +9 -0
  36. package/dist/preview/provenance-runtime-adapter.js +42 -0
  37. package/dist/preview/render-frame-provenance.d.ts +23 -0
  38. package/dist/preview/render-frame-provenance.js +47 -0
  39. package/dist/preview/route-authoring.d.ts +3 -0
  40. package/dist/preview/route-authoring.js +80 -0
  41. package/dist/preview/route-context.d.ts +3 -0
  42. package/dist/preview/route-context.js +38 -0
  43. package/dist/preview/route-disable.d.ts +7 -0
  44. package/dist/preview/route-disable.js +63 -0
  45. package/dist/preview/route-document-provenance.d.ts +23 -0
  46. package/dist/preview/route-document-provenance.js +46 -0
  47. package/dist/preview/route-errors.d.ts +1 -0
  48. package/dist/preview/route-errors.js +1 -0
  49. package/dist/preview/route-handler.d.ts +36 -0
  50. package/dist/preview/route-handler.js +139 -0
  51. package/dist/preview/route-initial.d.ts +9 -0
  52. package/dist/preview/route-initial.js +141 -0
  53. package/dist/preview/route-provenance-runtime.d.ts +34 -0
  54. package/dist/preview/route-provenance-runtime.js +90 -0
  55. package/dist/preview/route-token.d.ts +6 -0
  56. package/dist/preview/route-token.js +46 -0
  57. package/dist/preview/server-preview-internals.d.ts +3 -0
  58. package/dist/preview/server-preview-internals.js +3 -0
  59. package/dist/preview/slice-locale.d.ts +5 -0
  60. package/dist/preview/slice-locale.js +15 -0
  61. package/dist/preview-error-surface.d.ts +7 -0
  62. package/dist/preview-error-surface.js +24 -0
  63. package/dist/preview-exit.d.ts +7 -0
  64. package/dist/preview-exit.js +71 -0
  65. package/dist/preview-metadata.d.ts +15 -0
  66. package/dist/preview-metadata.js +13 -0
  67. package/dist/preview-mode-latch.d.ts +133 -0
  68. package/dist/preview-mode-latch.js +231 -0
  69. package/dist/preview-nav-location.d.ts +12 -0
  70. package/dist/preview-nav-location.js +149 -0
  71. package/dist/preview-nav.d.ts +94 -0
  72. package/dist/preview-nav.js +523 -0
  73. package/dist/preview-refresh-coordinator.d.ts +26 -0
  74. package/dist/preview-refresh-coordinator.js +93 -0
  75. package/dist/preview-render-error.d.ts +11 -0
  76. package/dist/preview-render-error.js +36 -0
  77. package/dist/preview-render-recovery.d.ts +9 -0
  78. package/dist/preview-render-recovery.js +42 -0
  79. package/dist/preview-session-recovery.d.ts +1 -0
  80. package/dist/preview-session-recovery.js +1 -0
  81. package/dist/preview-standalone-host.d.ts +3 -0
  82. package/dist/preview-standalone-host.js +55 -0
  83. package/dist/preview-text.d.ts +2 -0
  84. package/dist/preview-text.js +1 -0
  85. package/dist/preview-view-bootstrap.d.ts +5 -0
  86. package/dist/preview-view-bootstrap.js +13 -0
  87. package/dist/public-delivery.d.ts +6 -0
  88. package/dist/public-delivery.js +7 -0
  89. package/dist/query-fetch-options.d.ts +7 -0
  90. package/dist/query-fetch-options.js +193 -0
  91. package/dist/query.d.ts +2 -0
  92. package/dist/query.js +1 -0
  93. package/dist/rendering.d.ts +33 -0
  94. package/dist/rendering.js +42 -0
  95. package/dist/revalidate.d.ts +12 -0
  96. package/dist/revalidate.js +33 -0
  97. package/dist/rich-text.d.ts +2 -0
  98. package/dist/rich-text.js +1 -0
  99. package/dist/server-runtime.d.ts +208 -0
  100. package/dist/server-runtime.js +1584 -0
  101. package/dist/server.d.ts +8 -0
  102. package/dist/server.js +6 -0
  103. package/dist/slice-zone.d.ts +2 -0
  104. package/dist/slice-zone.js +1 -0
  105. package/dist/storystick-component-types.d.ts +26 -0
  106. package/dist/storystick-component-types.js +1 -0
  107. package/dist/storystick-component.d.ts +10 -0
  108. package/dist/storystick-component.js +45 -0
  109. package/dist/storystick-error-recovery.d.ts +12 -0
  110. package/dist/storystick-error-recovery.js +11 -0
  111. package/dist/storystick-preview-runtime.d.ts +30 -0
  112. package/dist/storystick-preview-runtime.js +561 -0
  113. package/dist/storystick.d.ts +24 -0
  114. package/dist/storystick.js +23 -0
  115. package/dist/testing.d.ts +9 -0
  116. package/dist/testing.js +40 -0
  117. package/dist/video.d.ts +1 -0
  118. package/dist/video.js +1 -0
  119. package/package.json +128 -3
  120. package/recipes/README.md +91 -0
  121. package/recipes/resource-mirror.ts +49 -0
package/LICENSE ADDED
@@ -0,0 +1,112 @@
1
+ # Functional Source License, Version 1.1, MIT Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-MIT
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Tanguy Freycon
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the MIT license that is effective on the second anniversary of the date we make
91
+ the Software available. On or after that date, you may use the Software under
92
+ the MIT license, in which case the following will apply:
93
+
94
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
95
+ this software and associated documentation files (the "Software"), to deal in
96
+ the Software without restriction, including without limitation the rights to
97
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
98
+ of the Software, and to permit persons to whom the Software is furnished to do
99
+ so, subject to the following conditions:
100
+
101
+ The above copyright notice and this permission notice shall be included in all
102
+ copies or substantial portions of the Software.
103
+
104
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
105
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
106
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
107
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
108
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
109
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
110
+ SOFTWARE.
111
+
112
+ This Agreement shall be governed by the laws of France.
package/PUBLIC-API.md ADDED
@@ -0,0 +1,31 @@
1
+ # @storystick/next API contract
2
+
3
+ Install this candidate explicitly with `@storystick/next@0.2.0-rc.0` after registry bootstrap and publication. This repository alone does not make the candidate or beta channel available on npm.
4
+
5
+ Version 0.2.0-rc.0, cohort `sdk-0.2.0-rc.0`. ESM distribution; no CommonJS or universal Edge compatibility is promised.
6
+
7
+ This package is a product integration facade. Reserved contracts are required between SDK packages and must be upgraded with the same cohort. Their public export paths do not promise independent stability.
8
+
9
+ | Entry | Status |
10
+ | --- | --- |
11
+ | `.` | Supported beta entry |
12
+ | `./preview-text` | Supported beta entry |
13
+ | `./image` | Supported beta entry |
14
+ | `./file` | Supported beta entry |
15
+ | `./video` | Supported beta entry |
16
+ | `./link` | Supported beta entry |
17
+ | `./query` | Supported beta entry |
18
+ | `./rich-text` | Supported beta entry |
19
+ | `./slice-zone` | Supported beta entry |
20
+ | `./revalidate` | Supported beta entry |
21
+ | `./preview-nav` | Reserved SDK contract |
22
+ | `./preview-nav-location` | Reserved SDK contract |
23
+ | `./preview` | Supported beta entry |
24
+ | `./testing` | Reserved SDK contract |
25
+ | `./server` | Supported beta entry |
26
+
27
+ The qualified toolchain is Node 24.19.0 and pnpm 9.15.9. Next uses App Router 15.5.28, Sharp 0.35.5 and React/React DOM 19.2.8; Astro uses 7.4.4 with the Node adapter 11.1.9. Browser delivery is verified by bundling; hosted Preview activation and cross-browser behavior require the staging manual check. Source/CLI are Node tools, server entries belong on the server, and browser entries must never receive a server API key.
28
+
29
+ Use a generated Query V2 definition. A missing document/singleton returns null, an empty collection returns an empty array, and fetch throws on blocked/degraded execution. Execute exposes the complete result envelope. RSC metadata must remain plain JSON data; request authority must not be stored in shared module state.
30
+
31
+ The repository is private. Public package availability does not make the monorepo public, and npm provenance from that repository is unavailable. Product and pilot support: [Storystick](https://storystick.app).
package/README.md CHANGED
@@ -1,3 +1,523 @@
1
- # Temporary Holding Version
1
+ # @storystick/next
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Minimal content reads and Preview for the Next.js 15 App Router, with React 19.
4
+
5
+ ```bash
6
+ pnpm add @storystick/next@beta
7
+ ```
8
+
9
+ ## Configure once
10
+
11
+ ```ts
12
+ // lib/site.ts
13
+ import { createSite } from '@storystick/next/server'
14
+ import * as definition from '@/generated/storystick/query'
15
+
16
+ export const site = createSite({
17
+ definition,
18
+ })
19
+ ```
20
+
21
+ Generate the definition with the Storystick CLI. Credentials come from the
22
+ project connection environment variables. Routable Story types are inferred
23
+ from the generated definition, the site URL comes from
24
+ `STORYSTICK_SITE_URL`, and the project primary locale is available through
25
+ `await site.locale()`. Override these only when an application deliberately
26
+ uses a different policy. The site object contains immutable configuration; it
27
+ never stores a current user's session.
28
+
29
+ ## Read where you render
30
+
31
+ ```tsx
32
+ import { previewTextProps } from '@storystick/next'
33
+ import { site } from '@/lib/site'
34
+
35
+ export async function Header() {
36
+ const client = await site.client()
37
+ const settings = await client.query('siteSettings').selectAll().fetch()
38
+ return settings && <header {...previewTextProps(settings.fields.siteName)} />
39
+ }
40
+ ```
41
+
42
+ This example assumes a `siteSettings` singleton with a `siteName` field. Keep
43
+ this component in a native layout, page, group or slot. It does not depend on
44
+ a CMS page initializing context. Put `<Storystick />` from `@storystick/next`
45
+ once in a shared layout. Export `GET` and `POST` from `site.handlers` in
46
+ `app/api/storystick/[...action]/route.ts`.
47
+
48
+ In detached Preview, the SDK owns the editing panel outside the site's layout
49
+ tree. Replacing a layout in the same client navigation preserves the panel and
50
+ its editing connection. Removing `<Storystick />` entirely, leaving Preview, or
51
+ changing project/view ends that lifetime. A full page reload creates a new
52
+ connection; an editor-initiated navigation can restore the panel at its confirmed
53
+ destination. Editor navigation waits for saving and context acceptance, while
54
+ page overlays wait for the corresponding site render. No additional provider or
55
+ SDK option is needed.
56
+
57
+ For a controlled schema migration, give each candidate deployment a distinct
58
+ `STORYSTICK_APP_BUILD_ID=app:<build-id>` and set
59
+ `STORYSTICK_APP_BUILD_DIGEST` to the build artifact's lowercase SHA-256. The
60
+ opt-in `/api/storystick/migration-readback` endpoint then reports this build's
61
+ project, generated query-schema fingerprint and the schema version actually
62
+ read through its server SDK. The ordinary health route stays lightweight.
63
+ These values are deployment claims plus an observed endpoint read, not a
64
+ cryptographic attestation of every running instance or a replacement for
65
+ checking B in Studio Preview before authority transfer.
66
+
67
+ Queries use Query V2 directly: `select`, `where`, `orderBy`, `page` and `by`.
68
+ `.fetch()` returns exact data and throws `QueryExecutionError` for blocked or
69
+ degraded execution. Missing singletons/documents return `null`; empty pages
70
+ return `[]`. `.execute()` returns the result envelope, including pagination
71
+ and diagnostics, when an application needs those details.
72
+
73
+ For a selection shared by several components, use the generated identity helper
74
+ instead of recreating a query abstraction:
75
+
76
+ ```ts
77
+ import { defineSelection } from '@/generated/storystick/query'
78
+
79
+ export const postCard = defineSelection('blogPost', {
80
+ title: true,
81
+ slug: true,
82
+ coverImage: true,
83
+ })
84
+ ```
85
+
86
+ `client.query('blogPost').select(postCard)` keeps the exact selected result
87
+ type. The helper emits no query syntax and adds no runtime work.
88
+
89
+ Configured server reads carry public edit addresses with their values in authorized
90
+ Preview. Ordinary async components, including later JSX descendants, need no
91
+ render wrapper or query registration. A global projected-generation subscription
92
+ refreshes the native Next tree, including queries that previously returned null
93
+ or an empty array. Changes are grouped in a fixed 50ms window; another refresh
94
+ waits for the current React transition. Stale projections do not trigger reads.
95
+ This updates more of the server tree than mounted-query targeting.
96
+
97
+ Keep stable React keys and native layout placement to retain client state.
98
+ Preview strings contain invisible provenance: use a cleaned copy for string
99
+ comparisons, lengths and other computation, and `previewTextProps` for native
100
+ text elements (see below). Derived values have no guessed edit target. Images,
101
+ links, rich text and slices retain the existing renderer helpers; custom
102
+ non-text surfaces can use explicit edit attributes.
103
+
104
+ ## Optional CMS pages
105
+
106
+ ```tsx
107
+ import { notFound, redirect, permanentRedirect } from 'next/navigation'
108
+ import { site } from '@/lib/site'
109
+ import { PageContent } from '@/components/page-content'
110
+
111
+ type Props = {
112
+ params: Promise<{ pathname?: string[] }>
113
+ searchParams: Promise<Record<string, string | string[] | undefined>>
114
+ }
115
+
116
+ export default async function Page(props: Props) {
117
+ const route = await site.resolvePage(props)
118
+ if (route.type === 'redirect') {
119
+ const navigate = route.permanent ? permanentRedirect : redirect
120
+ navigate(route.redirectTo)
121
+ }
122
+ if (route.type !== 'document') notFound()
123
+ return <PageContent document={route.document} />
124
+ }
125
+
126
+ export async function generateMetadata(props: Props) {
127
+ const route = await site.resolvePage(props)
128
+ return site.metadata(route, route.type === 'document'
129
+ ? { title: route.document.fields.title } : {})
130
+ }
131
+ export const generateStaticParams = site.params
132
+ ```
133
+
134
+ `PageContent` is application code. `resolvePage` accepts native catch-all props
135
+ or explicit `{ pathname, searchParams }`. Native-props redirects retain query
136
+ parameters. Public pages do not consume dynamic search parameters unless needed
137
+ for a redirect. `site.metadata` cleans metadata and sets localized alternates,
138
+ canonical URLs and Preview noindex. Sitemaps use `site.sitemapIds()` and
139
+ `site.sitemap()`. Use native `error.tsx` for render failures. If an error boundary
140
+ can replace the layout containing `<Storystick />`, mount
141
+ `<StorystickErrorRecovery error={error} reset={reset} />` beside its fallback UI:
142
+
143
+ ```tsx
144
+ 'use client'
145
+ import { StorystickErrorRecovery } from '@storystick/next'
146
+
147
+ export default function ErrorPage({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
148
+ return <main role="alert">
149
+ <StorystickErrorRecovery error={error} reset={reset} />
150
+ <p>This page could not be displayed.</p>
151
+ <button onClick={reset}>Try again</button>
152
+ </main>
153
+ }
154
+ ```
155
+
156
+ This keeps Studio navigation available after a render failure, including moves
157
+ between a route-less draft and the homepage. Passing `error` preserves the
158
+ distinction between transient content-service failures (which Studio can recover) and customer rendering bugs (which are not
159
+ automatically retried), including in production where Next redacts error details.
160
+ Ordinary public visits do not load the Preview runtime. Startup failures that prevent the error component itself from
161
+ mounting still require a full reload after the underlying problem is fixed.
162
+
163
+ A native route needs neither a CMS document nor a catch-all. It keeps its own
164
+ metadata responsibility; `storystickPreviewRobots({ enabled })` is available
165
+ when handling metadata outside the CMS route helper. `site.page`,
166
+ `site.component`, `StorystickDocument` and `StorystickEditable` are removed.
167
+
168
+ See the [published language routes and resource mirror recipes](recipes/README.md)
169
+ for `site.alternates()`, translated paths, `publicOrigins` / `domainLocale`, and an
170
+ explicit French interface with an English fallback publication.
171
+
172
+ ## Layouts, locales and Preview
173
+
174
+ `site.client()` uses the project's primary locale; `site.client({ locale })`
175
+ selects another explicitly. Slice reads inherit their source document locale.
176
+ For multilingual `<html lang>`, put the layout at a native segment that owns
177
+ the language parameter. A root layout does not receive descendant parameters.
178
+ Headers and footers can remain in their layouts and retain local client state.
179
+ They remain editable on native routes without CMS page documents. Selecting
180
+ a field changes the Studio editor target while retaining the actual view.
181
+
182
+ The standard Preview integration shares one authorized reading perspective
183
+ per browser/project/origin. Each view retains its own navigation and edit
184
+ target. Variant/release selection changes the shared perspective; individual
185
+ URLs are not its authority. No middleware, proxy, generated webpack alias,
186
+ customer-link rewrite or provenance flag is required.
187
+
188
+ Routing, read observations and JSON metadata live in framework-neutral
189
+ packages. This package supplies the Next request and router adapter; it does
190
+ not imply that Astro, Vue or Svelte adapters have been released.
191
+
192
+ The root entry exposes the ordinary rendering and client surfaces:
193
+
194
+ ```tsx
195
+ import {
196
+ previewTextProps,
197
+ RichText,
198
+ SliceZone,
199
+ StorystickImage,
200
+ StorystickLink,
201
+ } from '@storystick/next'
202
+ ```
203
+
204
+ Direct subpaths remain available as explicit bundle boundaries, but the starter
205
+ and ordinary application code need only `@storystick/next`.
206
+
207
+ ## Render text fields
208
+
209
+ SDK reads return native strings, including their provenance during authorized
210
+ Preview. Move that metadata out of the text node when rendering a field:
211
+
212
+ ```tsx
213
+ import { previewTextProps } from '@storystick/next'
214
+
215
+ <h1 {...previewTextProps(fields.title)} />
216
+ ```
217
+
218
+ `previewTextProps` returns plain `children` and a `data-ss-p` attribute for one
219
+ complete field. It adds no wrapper, provider or client lifecycle, and its props
220
+ can cross a Server Component boundary. The runtime validates the provenance
221
+ before making the existing element an edit target. Plain production values
222
+ produce only `children`.
223
+
224
+ In client components that only need this helper, use
225
+ `import { previewTextProps } from '@storystick/next/preview-text'` to keep the
226
+ import limited to the pure text adapter. The root export remains available
227
+ alongside the SDK's other rendering primitives.
228
+
229
+ Use the original field before formatting or concatenation. A composite value
230
+ is cleaned without assigning the element to a guessed field. Render fields
231
+ on separate elements for separate targets; for transformed text, clean the
232
+ display value and use explicit field bindings when an edit target is needed.
233
+ Rendering a provenance-bearing string directly in JSX does not guarantee
234
+ visually identical text across browsers and typography settings.
235
+
236
+ ## Clean values before computation and attributes
237
+
238
+ String comparison is the first case to protect because a failed comparison
239
+ leaves no detectable browser signal. Compare a cleaned copy, and pass the
240
+ original field to `previewTextProps` to retain its edit target:
241
+
242
+ ```tsx
243
+ import { previewTextProps, stripPreviewProvenance } from '@storystick/next'
244
+
245
+ function ComparisonCell({ value }: { value: string }) {
246
+ const comparable = stripPreviewProvenance(value)
247
+ if (comparable === '✓') return <CheckIcon />
248
+ return <span {...previewTextProps(value)} />
249
+ }
250
+ ```
251
+
252
+ Use `stripPreviewProvenance(value)` before comparisons, custom attributes,
253
+ `placeholder`, `aria-*`, custom metadata, JSON-LD, or another structured
254
+ payload. The helper deep-copies JSON-shaped input, removes preview coded-text
255
+ payloads and the reserved `_ssMarker` / `__ssPreviewProvenance` keys, and never
256
+ mutates the source. It is client-compatible from `@storystick/next` and is also
257
+ re-exported by `@storystick/next/server` for server-only code.
258
+
259
+ Cleaning a field before passing it to `previewTextProps` removes its edit
260
+ target. Keep the original value for that helper and clean separate copies for
261
+ computation and other sinks.
262
+
263
+ ## Render image fields
264
+
265
+ The zero-config component reads the request-owned asset sidecar registered by
266
+ `resolvePage`, so an ordinary render only passes the field value:
267
+
268
+ ```tsx
269
+ import { StorystickImage } from '@storystick/next'
270
+
271
+ return (
272
+ <StorystickImage
273
+ image={fields.image}
274
+ loading="eager"
275
+ fetchPriority="high"
276
+ />
277
+ )
278
+ ```
279
+
280
+ Images are lazy and responsive by default, using a bounded `srcset` and
281
+ `sizes="auto, 100vw"`. Eager images use `100vw`; set `responsive={false}` for
282
+ a single source or pass `options.sizes` when the layout has a more precise
283
+ contract. An explicit `source` remains authoritative for rendering outside a bound
284
+ request: `options={{ source: assets }}`. Transformations, `widths` and `sizes`
285
+ all live in this single options object. An explicit top-level `alt` overrides
286
+ the field; `alt=""` remains decorative.
287
+
288
+ A valid version LQIP is consumed automatically and removed after the final
289
+ image decodes. Use `placeholder="none"` to opt out. Without an LQIP the
290
+ component remains a server-rendered native `<img>` with no client lifecycle.
291
+ When `sizes` is present without `widths`, Storystick emits its bounded standard
292
+ responsive scale capped to the intrinsic asset width.
293
+
294
+ Native events and React 19 refs are supported inside your Client Components,
295
+ including when the image uses an LQIP. Functions still cannot cross a Server
296
+ Component boundary: create handlers in a Client Component, or pass the plain
297
+ JSON from `resolveImage().props` to your own client renderer.
298
+
299
+ In development a non-renderable image logs a warning and leaves an inspectable
300
+ `[data-storystick-image-status]` element at its render position. In production
301
+ it renders `null` by default. Pass `fallback` for app-owned degradation in both
302
+ environments; a render function receives the discriminated resolver failure:
303
+
304
+ ```tsx
305
+ <StorystickImage
306
+ image={fields.coverImage}
307
+ fallback={({ status }) => <CoverPlaceholder reason={status} />}
308
+ />
309
+ ```
310
+
311
+ `getNextImageProps()` remains available for explicit `next/image` interop. It
312
+ is not the default path: Storystick already serves canonical provider-native
313
+ transform URLs and avoids remote-pattern configuration and double optimization.
314
+
315
+ ### Optional `next/image` loader
316
+
317
+ Storystick's default `<img>` path already serves provider-native canonical
318
+ URLs. If an app needs a `next/image` loader, keep it pure: rewrite only the
319
+ last transform-key segment, round the requested width up through the grammar,
320
+ and map Next's numeric quality onto the three finite cache tiers.
321
+
322
+ ```ts
323
+ import type { ImageLoaderProps } from 'next/image'
324
+ import {
325
+ buildImageTransformKey,
326
+ parseImageTransformKey,
327
+ roundUpImageDimensionBucket,
328
+ type CanonicalImageTransform,
329
+ type ImageTransformQuality,
330
+ } from '@storystick/asset-grammar'
331
+
332
+ function qualityTier(quality = 75): ImageTransformQuality {
333
+ if (quality <= 55) return 'low'
334
+ if (quality <= 77) return 'medium'
335
+ return 'high'
336
+ }
337
+
338
+ export function storystickImageLoader({
339
+ src,
340
+ width,
341
+ quality,
342
+ }: ImageLoaderProps): string {
343
+ const bucket = roundUpImageDimensionBucket(width)
344
+ if (bucket === null) throw new Error('Unsupported Storystick image width')
345
+
346
+ const url = new URL(src)
347
+ const path = url.pathname.split('/')
348
+ const encodedKey = path.pop()
349
+ if (!encodedKey) throw new Error('Missing Storystick transform key')
350
+
351
+ const parsed = parseImageTransformKey(decodeURIComponent(encodedKey))
352
+ const transform: CanonicalImageTransform =
353
+ parsed.kind === 'transform'
354
+ ? parsed.transform
355
+ : { schemaVersion: 1 }
356
+ const nextKey = buildImageTransformKey({
357
+ ...transform,
358
+ resize: {
359
+ width: bucket,
360
+ fit: transform.resize?.fit ?? 'scale-down',
361
+ },
362
+ quality: qualityTier(quality),
363
+ })
364
+
365
+ path.push(encodeURIComponent(nextKey))
366
+ url.pathname = path.join('/')
367
+ return url.toString()
368
+ }
369
+ ```
370
+
371
+ Install `@storystick/asset-grammar` directly when using this recipe. Bucket
372
+ rounding fails closed above the largest supported width instead of creating an
373
+ unbounded transformation key.
374
+
375
+ ## Advanced server integration
376
+
377
+ `createServerClient` and `resolvePage` remain available from
378
+ `@storystick/next/server`. They require
379
+ explicit schema/site information that `createSite` binds once. Prefer direct
380
+ Query V2 reads to route-wide data declarations when slices have different
381
+ filters or selections.
382
+
383
+ There is no `ss` namespace. Use `site.params()`, `site.sitemap()` and
384
+ `site.sitemapIds()` for the configured site. Read singletons with
385
+ `(await site.client()).query('settings').selectAll().fetch()`. Custom static
386
+ parameters use paginated Query V2 selections and your own mapping, rather
387
+ than a predefined `slug` field or segment name.
388
+
389
+ Locale information is explicit: `route.localeContext` comes from
390
+ `site.resolvePage()`, and `site.client({ locale })` configures an independent
391
+ reader. The pure `prefixHref(href, locale, settings)` helper from
392
+ `prefixHref`, re-exported by `@storystick/next`, builds localized links. It does not
393
+ require another component to resolve a page first. Native layouts can pass
394
+ this information through ordinary props or application context.
395
+
396
+ `resolvePreviewDraft({ sessionId, locale? })` and
397
+ `isPreviewDraftActive(sessionId)` are advanced draft-target helpers for a
398
+ native page that already owns a view selector. They do not infer the request
399
+ URL from private headers. Layout content does not need either helper.
400
+
401
+ ## HTTP routes and deployment
402
+
403
+ `site.handlers` wraps `createRouteHandler` from `@storystick/next/preview`.
404
+ It handles activation, access renewal, view context commands, exit, health,
405
+ Preview runtime delivery and signed revalidation. `createSite({ routes })`
406
+ accepts the same handler options; direct handler construction remains possible.
407
+
408
+ Activation verifies the origin and capability, enables Draft Mode, and stores a
409
+ signed HttpOnly admission for the project and customer origin. This admission
410
+ **authorizes access; it does not select a perspective**. Each opening from Studio
411
+ creates a navigation view attached to the authenticated project's current
412
+ perspective. The URL's `_ss_session` identifies the view for navigation and
413
+ selection; ordinary links do not need to carry it. Reopening or returning to
414
+ Studio observes the shared perspective without replaying an old launch.
415
+ Document write targets are still derived from the selected perspective, document,
416
+ locale and release membership.
417
+
418
+ ### Navigation and perspectives
419
+
420
+ Use ordinary `next/link` or `<a>` links, and `StorystickLink` for CMS links.
421
+ The existing `<Storystick />` mount and Preview handlers own the integration:
422
+ no Preview navigation provider, middleware, session prop or per-link helper is required.
423
+ The SDK never rewrites customer DOM.
424
+
425
+ A human authentication session shares one perspective per project and customer
426
+ origin. Studio and detached previews observe the same revision; their current
427
+ page, locale and field selection remain local. Layouts and pages read that
428
+ perspective through the existing admission. Public requests retain static/ISR
429
+ rendering; admitted Preview reads stay outside public caches.
430
+
431
+ Choose **Published** in the perspective selector to read active publications
432
+ while keeping the toolbar. Published uses the current frontend build with
433
+ published content, without draft substitution or optimistic edits. A document or
434
+ language without a publication remains unavailable. Choose Main, a variant or a
435
+ release to resume editing. Changing perspective never publishes content.
436
+
437
+ Perspective commands use compare-and-set revisions and idempotent receipts.
438
+ A conflicting command is reported instead of silently replayed. Other surfaces
439
+ adopt the observed perspective after preserving their pending edits in their
440
+ original scope. Closing a panel does not end the shared context.
441
+
442
+ Production Preview requires HTTPS and an allowed customer origin. Grants are
443
+ addressed by project and normalized browser origin (including port), with the
444
+ configured base path as the cookie path. Reopen existing previews after this
445
+ upgrade so that their grants attach to the shared authority. Next Draft Mode is
446
+ an infrastructure flag, not authorization for private content. Published keeps
447
+ this admission active; it does not call the disable handler. Reopen from Studio
448
+ after deploying a new application build.
449
+
450
+ For simultaneous production Previews of separate Next builds, use distinct
451
+ hostnames. Next's own `__prerender_bypass` cookie is shared by hostname, uses
452
+ `Path=/`, and is bound to a build; another application on a different port or
453
+ base path can replace it. Storystick preserves Next's production static/ISR gate
454
+ and does not rename or emulate this framework cookie. Development and explicit
455
+ local lanes read the addressed Storystick grant directly.
456
+
457
+ For a custom native page outside the CMS route helper, metadata and native error handling
458
+ remain application-owned. For non-DOM transformations of rendered content,
459
+ keep the explicit cleaning boundary described above.
460
+
461
+ Signed publish webhooks use `STORYSTICK_WEBHOOK_SECRET`. Keep that value and
462
+ `STORYSTICK_API_KEY` server-only. Reverse proxies must preserve the customer
463
+ origin through trusted forwarded host/protocol headers. No proxy is required
464
+ by the SDK itself, and server action origin settings are not a replacement
465
+ for configuring the proxy correctly.
466
+
467
+ Licensed under [FSL-1.1-MIT](./LICENSE).
468
+
469
+ ### Rendering independent content
470
+
471
+ Values returned by `site.client()` and `site.resolvePage()` already own their
472
+ rendering dependencies. Use `<SliceZone slices={document.fields.sections}
473
+ components={components} />` and `<RichText content={author.fields.bio} />` without
474
+ technical context. Optional `context={{ document, parent }}` is application data
475
+ only. Native layouts and async components remain ordinary Next components;
476
+ no document wrapper or additional Provider is needed.
477
+
478
+ Richtext is a JSON object on reads. Reserved content metadata survives JSON/RSC
479
+ transport and must be preserved when passing rendering values. Component
480
+ registries contain functions: import the registry in its rendering environment,
481
+ rather than passing server functions through a client boundary.
482
+
483
+ Async slice functions inherit the locale carried by their content, including
484
+ nested slices and rich-text embeds. Awaited helpers inside that function share
485
+ the same locale. Independent components rendered later use the site's default,
486
+ or `site.client({ locale })`; no layout-global locale is assumed. Application
487
+ `context` is never inspected for SDK locale selection.
488
+
489
+ Automatic cache tags are bounded and share their encoding with invalidation.
490
+ Custom tags retain Next's 256-character limit, with at most 127 distinct custom
491
+ tags so every read can keep its project invalidation tag. Excess automatic tags
492
+ are omitted only when project invalidation still covers the read.
493
+
494
+ Public Query V2 delivery can use `STORYSTICK_DELIVERY_ENDPOINT`. Public framework
495
+ reads now default to `no-store`; explicit Next Data Cache requires a finite
496
+ positive `next.revalidate`. Publication handlers accept the shared v2 ready event,
497
+ and `signWebhookBody` is asynchronous. For renderer-owned receipts, deployment
498
+ manifests, recovery and qualified modes, see the repository's
499
+ [publication contract](../../services/public-content-gateway/PUBLICATION.md).
500
+
501
+ For ordinary static publication, declare `createSite({ definition,
502
+ publicRendering: 'static' })` and supply a new `VERCEL_DEPLOYMENT_ID` or
503
+ `STORYSTICK_DEPLOYMENT_ID` for **every compilation**, including CMS-only rebuilds
504
+ of the same Git commit. Freeze it in the compiled artifact, for example using
505
+ Next's `env` configuration. This mode keeps Next's native build-time caching
506
+ and scopes content, locale and route-inventory fetches to that deployment, so a
507
+ restored `.next/cache` cannot serve a previous build's content. Do not use the Git
508
+ SHA as this identity or reuse an ID for a new compilation. Pages and sitemap
509
+ must be activated together. Preview and explicit fresh reads remain `no-store`;
510
+ do not use `dynamic = 'force-static'` on pages that support Draft Mode. Static
511
+ publication still needs an actual rebuild and rendered proof, and does not
512
+ qualify managed schema migration.
513
+
514
+ The repository checks this against the actual marketing app with
515
+ `node scripts/marketing-build-qualification.mjs --cache-rebuild`: retained fetch
516
+ cache, changed content/locales/routes/sitemap, then a content-origin outage that
517
+ must fail the next build. See the [Next 15 cache behavior](https://nextjs.org/docs/15/app/guides/caching).
518
+
519
+ ## Candidate distribution contract
520
+
521
+ See [PUBLIC-API.md](./PUBLIC-API.md) for supported entries, reserved SDK contracts,
522
+ the qualified runtime matrix and the candidate cohort. Upgrade Storystick
523
+ packages together.
@@ -0,0 +1 @@
1
+ 7879ab7fae7e3c028138e1b55edc83b6b5bdc2edd6b6cdfcdc0560dfd44f88ae
@@ -0,0 +1,2 @@
1
+ /** Internal tags share one bounded representation at read and invalidation. */
2
+ export declare function storystickCacheTag(tag: string): string;