smoodly 0.0.51 → 0.0.53

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 (292) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth.d.ts +3 -11
  3. package/dist/admin/auth.js +1 -30
  4. package/dist/admin/client-ops.d.ts +5 -4
  5. package/dist/admin/client-ops.js +15 -12
  6. package/dist/admin/delivery-mode.d.ts +1 -1
  7. package/dist/admin/delivery-mode.js +2 -2
  8. package/dist/admin/drift.d.ts +0 -2
  9. package/dist/admin/drift.js +1 -1
  10. package/dist/admin/editor/EditorView.d.ts +0 -3
  11. package/dist/admin/editor/EditorView.js +11 -16
  12. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  13. package/dist/admin/editor/FloatingToolbar.js +1 -1
  14. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  15. package/dist/admin/editor/SeoSettings.js +2 -2
  16. package/dist/admin/editor/canvas-address.d.ts +8 -0
  17. package/dist/admin/editor/canvas-address.js +18 -2
  18. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  19. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  20. package/dist/admin/editor/inline-dom.d.ts +0 -4
  21. package/dist/admin/editor/inline-dom.js +1 -1
  22. package/dist/admin/editor/preview-grant.d.ts +6 -0
  23. package/dist/admin/editor/preview-grant.js +25 -0
  24. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  25. package/dist/admin/files.generated.js +1 -1
  26. package/dist/admin/fixed-nodes.d.ts +0 -6
  27. package/dist/admin/fixed-nodes.js +1 -81
  28. package/dist/admin/forms/CropEditor.d.ts +4 -2
  29. package/dist/admin/forms/CropEditor.js +58 -5
  30. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  31. package/dist/admin/forms/ImagePreview.js +9 -3
  32. package/dist/admin/forms/MediaGrid.js +2 -1
  33. package/dist/admin/forms/MediaPicker.js +9 -6
  34. package/dist/admin/forms/MediaWidget.js +11 -9
  35. package/dist/admin/forms/assets-context.d.ts +7 -3
  36. package/dist/admin/forms/assets-context.js +5 -3
  37. package/dist/admin/forms/crop-editor.d.ts +24 -2
  38. package/dist/admin/forms/crop-editor.js +36 -2
  39. package/dist/admin/forms/date-picker.d.ts +0 -1
  40. package/dist/admin/forms/date-picker.js +1 -1
  41. package/dist/admin/forms/tabs.d.ts +0 -1
  42. package/dist/admin/forms/tabs.js +1 -1
  43. package/dist/admin/forms/upload.d.ts +18 -11
  44. package/dist/admin/forms/upload.js +62 -24
  45. package/dist/admin/global-items.d.ts +0 -3
  46. package/dist/admin/global-items.js +0 -54
  47. package/dist/admin/handler.d.ts +22 -28
  48. package/dist/admin/handler.js +146 -47
  49. package/dist/admin/health.d.ts +0 -4
  50. package/dist/admin/index.d.ts +6 -9
  51. package/dist/admin/index.js +3 -6
  52. package/dist/admin/ops.d.ts +40 -32
  53. package/dist/admin/page-data.d.ts +14 -6
  54. package/dist/admin/page-data.js +1 -1
  55. package/dist/admin/session-flow.d.ts +65 -0
  56. package/dist/admin/session-flow.js +121 -0
  57. package/dist/admin/shell/AdminApp.d.ts +8 -3
  58. package/dist/admin/shell/AdminApp.js +37 -58
  59. package/dist/admin/shell/ContentHealthPage.js +1 -3
  60. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  61. package/dist/admin/shell/GlobalFields.js +1 -1
  62. package/dist/admin/shell/IconRail.js +10 -8
  63. package/dist/admin/shell/MediaScreen.js +9 -6
  64. package/dist/admin/shell/MembersPage.d.ts +1 -2
  65. package/dist/admin/shell/MembersPage.js +19 -90
  66. package/dist/admin/shell/SettingsView.d.ts +1 -2
  67. package/dist/admin/shell/SettingsView.js +2 -2
  68. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  69. package/dist/admin/shell/SignedOutView.js +9 -0
  70. package/dist/admin/shell/console-links.d.ts +5 -0
  71. package/dist/admin/shell/console-links.js +11 -0
  72. package/dist/admin/shell/members-list.d.ts +1 -14
  73. package/dist/admin/shell/members-list.js +2 -23
  74. package/dist/admin/shell/pages-tree.d.ts +0 -2
  75. package/dist/admin/shell/pages-tree.js +1 -1
  76. package/dist/admin/shell/presence-socket.d.ts +10 -0
  77. package/dist/admin/shell/presence-socket.js +191 -0
  78. package/dist/admin/shell/presence.d.ts +6 -18
  79. package/dist/admin/shell/presence.js +4 -11
  80. package/dist/admin/shell/session.d.ts +19 -12
  81. package/dist/admin/shell/session.js +114 -47
  82. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  83. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  84. package/dist/admin/ui/primitives.d.ts +4 -14
  85. package/dist/admin/ui/primitives.js +7 -9
  86. package/dist/admin/ui/theme.js +13 -2
  87. package/dist/admin/validate.js +10 -1
  88. package/dist/api-client.d.ts +26 -0
  89. package/dist/api-client.js +107 -0
  90. package/dist/assemble.d.ts +4 -41
  91. package/dist/assemble.js +5 -303
  92. package/dist/asset-store.d.ts +28 -30
  93. package/dist/asset-store.js +26 -121
  94. package/dist/astro/cache.d.ts +1 -1
  95. package/dist/astro/components/Head.astro +4 -1
  96. package/dist/astro/components/Image.astro +12 -17
  97. package/dist/astro/components/Video.astro +5 -2
  98. package/dist/astro/head.d.ts +3 -0
  99. package/dist/astro/head.js +24 -0
  100. package/dist/astro/index.js +61 -27
  101. package/dist/astro/locale-paths.d.ts +5 -2
  102. package/dist/astro/locale-paths.js +10 -2
  103. package/dist/astro/routes/admin.js +9 -7
  104. package/dist/astro/runtime.d.ts +3 -2
  105. package/dist/astro/runtime.js +5 -4
  106. package/dist/astro/server.d.ts +7 -11
  107. package/dist/astro/server.js +22 -28
  108. package/dist/astro/shell.d.ts +3 -3
  109. package/dist/astro/shell.js +1 -1
  110. package/dist/astro/static-media-node.d.ts +4 -0
  111. package/dist/astro/static-media-node.js +25 -0
  112. package/dist/astro/static-media.d.ts +17 -0
  113. package/dist/astro/static-media.js +55 -0
  114. package/dist/astro/static.d.ts +5 -10
  115. package/dist/astro/static.js +13 -29
  116. package/dist/canvas-marker.d.ts +14 -0
  117. package/dist/canvas-marker.js +36 -0
  118. package/dist/cli/api.d.ts +14 -0
  119. package/dist/cli/api.js +48 -0
  120. package/dist/cli/app-env.d.ts +2 -0
  121. package/dist/cli/app-env.js +20 -0
  122. package/dist/cli/export.d.ts +16 -2
  123. package/dist/cli/export.js +52 -35
  124. package/dist/cli/import.d.ts +7 -11
  125. package/dist/cli/import.js +50 -54
  126. package/dist/cli/load-config.d.ts +0 -1
  127. package/dist/cli/load-config.js +1 -1
  128. package/dist/cli/main.d.ts +4 -12
  129. package/dist/cli/main.js +26 -72
  130. package/dist/cli/push.d.ts +6 -0
  131. package/dist/cli/push.js +31 -0
  132. package/dist/collections.d.ts +0 -1
  133. package/dist/collections.js +1 -1
  134. package/dist/config.d.ts +20 -5
  135. package/dist/config.js +18 -1
  136. package/dist/content/content-api.d.ts +23 -23
  137. package/dist/content/content-api.js +48 -56
  138. package/dist/content/mime.d.ts +1 -0
  139. package/dist/content/mime.js +6 -0
  140. package/dist/css.js +1 -1
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/entry-store.d.ts +8 -94
  143. package/dist/entry-store.js +9 -557
  144. package/dist/env.d.ts +27 -20
  145. package/dist/env.js +38 -50
  146. package/dist/fields.d.ts +11 -3
  147. package/dist/fields.js +33 -2
  148. package/dist/global-id.d.ts +2 -0
  149. package/dist/global-id.js +4 -2
  150. package/dist/image/SmoodlyImage.d.ts +9 -5
  151. package/dist/image/SmoodlyImage.js +29 -15
  152. package/dist/image/SmoodlyVideo.d.ts +1 -1
  153. package/dist/image/SmoodlyVideo.js +3 -2
  154. package/dist/image/index.d.ts +2 -3
  155. package/dist/image/index.js +0 -2
  156. package/dist/image/markup.d.ts +53 -0
  157. package/dist/image/markup.js +111 -0
  158. package/dist/index.d.ts +13 -23
  159. package/dist/index.js +7 -20
  160. package/dist/media-cut.d.ts +44 -0
  161. package/dist/media-cut.js +70 -0
  162. package/dist/media.d.ts +58 -4
  163. package/dist/media.js +126 -16
  164. package/dist/member-store.d.ts +2 -50
  165. package/dist/member-store.js +0 -56
  166. package/dist/next/admin-route.d.ts +1 -1
  167. package/dist/next/admin-route.js +5 -3
  168. package/dist/next/meta.js +2 -3
  169. package/dist/next/page-renderer.d.ts +2 -2
  170. package/dist/next/page-renderer.js +22 -11
  171. package/dist/next/queries.d.ts +2 -2
  172. package/dist/path-index.d.ts +0 -24
  173. package/dist/path-index.js +0 -98
  174. package/dist/paths.d.ts +1 -1
  175. package/dist/paths.js +1 -1
  176. package/dist/push.d.ts +30 -0
  177. package/dist/push.js +52 -0
  178. package/dist/read-counter.d.ts +2 -0
  179. package/dist/read-counter.js +6 -0
  180. package/dist/reads.d.ts +7 -7
  181. package/dist/reads.js +3 -0
  182. package/dist/registry-json.d.ts +11 -0
  183. package/dist/registry-json.js +81 -0
  184. package/dist/render.d.ts +0 -1
  185. package/dist/render.js +1 -1
  186. package/dist/resolve.d.ts +12 -6
  187. package/dist/resolve.js +11 -8
  188. package/dist/revalidate.d.ts +8 -1
  189. package/dist/revalidate.js +8 -1
  190. package/dist/richtext/index.d.ts +1 -1
  191. package/dist/section-wrapper.d.ts +1 -1
  192. package/dist/section-wrapper.js +0 -9
  193. package/dist/signed-message.d.ts +26 -0
  194. package/dist/signed-message.js +103 -0
  195. package/dist/site.d.ts +59 -18
  196. package/dist/site.js +21 -252
  197. package/dist/space-transfer.d.ts +6 -3
  198. package/dist/space-transfer.js +18 -6
  199. package/dist/store.d.ts +2 -80
  200. package/dist/store.js +3 -489
  201. package/dist/toolset.d.ts +2 -1
  202. package/dist/toolset.js +1 -1
  203. package/dist/version.d.ts +1 -1
  204. package/dist/version.js +1 -1
  205. package/package.json +4 -15
  206. package/dist/admin/auth-config.d.ts +0 -6
  207. package/dist/admin/browser/fetch-op.d.ts +0 -10
  208. package/dist/admin/browser/fetch-op.js +0 -44
  209. package/dist/admin/browser/main.d.ts +0 -1
  210. package/dist/admin/browser/main.js +0 -23
  211. package/dist/admin/ops-impl.d.ts +0 -51
  212. package/dist/admin/ops-impl.js +0 -1403
  213. package/dist/admin/shell/InviteModal.d.ts +0 -6
  214. package/dist/admin/shell/InviteModal.js +0 -28
  215. package/dist/admin/shell/LoginView.d.ts +0 -5
  216. package/dist/admin/shell/LoginView.js +0 -80
  217. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  218. package/dist/admin/shell/SchemaNotice.js +0 -4
  219. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  220. package/dist/admin/shell/SetPasswordView.js +0 -41
  221. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  222. package/dist/admin/shell/presence-realtime.js +0 -32
  223. package/dist/admin/supabase-auth.d.ts +0 -7
  224. package/dist/admin/supabase-auth.js +0 -30
  225. package/dist/astro/deploy-deps.d.ts +0 -10
  226. package/dist/astro/deploy-deps.js +0 -8
  227. package/dist/astro/image.d.ts +0 -27
  228. package/dist/astro/image.js +0 -29
  229. package/dist/bind.d.ts +0 -19
  230. package/dist/bind.js +0 -34
  231. package/dist/cli/add-admin.d.ts +0 -11
  232. package/dist/cli/add-admin.js +0 -25
  233. package/dist/cli/load-collections.d.ts +0 -2
  234. package/dist/cli/load-collections.js +0 -27
  235. package/dist/cli/migrate.d.ts +0 -43
  236. package/dist/cli/migrate.js +0 -138
  237. package/dist/cli/rebuild.d.ts +0 -23
  238. package/dist/cli/rebuild.js +0 -22
  239. package/dist/cli/space-io.d.ts +0 -41
  240. package/dist/cli/space-io.js +0 -84
  241. package/dist/client.d.ts +0 -11
  242. package/dist/client.js +0 -54
  243. package/dist/current-space.d.ts +0 -4
  244. package/dist/current-space.js +0 -9
  245. package/dist/document-stamp.d.ts +0 -7
  246. package/dist/document-stamp.js +0 -48
  247. package/dist/image/loaders.d.ts +0 -15
  248. package/dist/image/loaders.js +0 -5
  249. package/dist/image/next-config.d.ts +0 -18
  250. package/dist/image/next-config.js +0 -32
  251. package/dist/image/remote-pattern.d.ts +0 -1
  252. package/dist/image/remote-pattern.js +0 -5
  253. package/dist/migrations.d.ts +0 -37
  254. package/dist/migrations.js +0 -73
  255. package/dist/project.d.ts +0 -88
  256. package/dist/project.js +0 -231
  257. package/dist/published.d.ts +0 -142
  258. package/dist/published.js +0 -164
  259. package/dist/ref-index.d.ts +0 -19
  260. package/dist/ref-index.js +0 -0
  261. package/dist/schema-read.d.ts +0 -23
  262. package/dist/schema-read.js +0 -50
  263. package/dist/schema-version.d.ts +0 -19
  264. package/dist/schema-version.js +0 -34
  265. package/dist/sql-space.d.ts +0 -44
  266. package/dist/sql-space.js +0 -289
  267. package/dist/sql.d.ts +0 -15
  268. package/dist/sql.js +0 -556
  269. package/dist/supabase-asset-store.d.ts +0 -21
  270. package/dist/supabase-asset-store.js +0 -170
  271. package/dist/supabase-entry-store.d.ts +0 -92
  272. package/dist/supabase-entry-store.js +0 -824
  273. package/dist/supabase-member-store.d.ts +0 -21
  274. package/dist/supabase-member-store.js +0 -96
  275. package/dist/supabase-path-index.d.ts +0 -30
  276. package/dist/supabase-path-index.js +0 -138
  277. package/dist/supabase-published-store.d.ts +0 -56
  278. package/dist/supabase-published-store.js +0 -314
  279. package/dist/supabase-store.d.ts +0 -103
  280. package/dist/supabase-store.js +0 -678
  281. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  282. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  283. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  284. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  285. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  286. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  287. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  288. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  289. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  290. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  291. package/migrations/20261009120000_smoodly_v11.sql +0 -49
  292. /package/dist/{admin/auth-config.js → deploy-store.js} +0 -0
@@ -4,7 +4,9 @@
4
4
  // link per locale the page is published in, the Open Graph values, and noindex. Next writes the same through
5
5
  // smoodlyMetadata. Absolute addresses from `site` in astro.config, else the request's origin. A layout places
6
6
  // it inside <head>; `fallback` titles a document Smoodly has no title for (an entry whose collection has no
7
- // seo). A layout that writes its own <title> leaves `fallback` out and reads `of.title` itself.
7
+ // seo). A layout that writes its own <title> leaves `fallback` out and reads `of.title` itself. A page that shows an
8
+ // image preconnects to the media host, with no crossorigin: an <img> is fetched with credentials and would not use an
9
+ // anonymous connection (spec 2026-10-18 media on R2 and Images §7).
8
10
  const { of: head, fallback } = Astro.props;
9
11
  const origin = (Astro.site ?? Astro.url).origin;
10
12
  const absolute = (path: string) => new URL(path, origin).href;
@@ -12,6 +14,7 @@ const title = head.title ?? fallback;
12
14
  const canonical = head.canonical ? absolute(head.canonical) : undefined;
13
15
  const image = head.image;
14
16
  ---
17
+ {head.mediaOrigin && <link rel="preconnect" href={head.mediaOrigin} />}
15
18
  {title && <title>{title}</title>}
16
19
  {head.description && <meta name="description" content={head.description} />}
17
20
  {canonical && <link rel="canonical" href={canonical} />}
@@ -1,20 +1,15 @@
1
1
  ---
2
- // <Image>: a box at the aspect (declared, chosen or natural), the picture inside placed by crop and hotspot,
3
- // dimensions set so nothing shifts (spec 2026-09-08 media §9; the React one is image/SmoodlyImage.tsx). The
4
- // bytes come through Astro's image service: `getImage` turns the media URL into the /_image endpoint with a
5
- // srcset of widths, so a 4 MB upload is served at the size the page asks for. The media host is allowed by the
6
- // integration (index.ts). An SVG, or `unoptimized`, is the original. Data attributes land on the box.
7
- import { getImage } from "astro:assets";
8
- import { imageRequest, placeImage } from "smoodly/astro/runtime";
2
+ // <Image>: the one image markup (src/image/markup.ts) written as HTML, attribute for attribute what React writes for
3
+ // <SmoodlyImage> (spec 2026-10-18 media on R2 and Images §7): a box, a plain <img> over the cut's widths, or a <picture>
4
+ // with a <source> per crop the view places in `media`. No Astro image service and no Smoodly JavaScript. Data
5
+ // attributes land on the box. In a prerendered page of static output the addresses become files of the site's own
6
+ // output, AVIF and WebP (static-media.ts, spec §8); the preview address, rendered on demand, keeps the media host's.
7
+ import { imageHtml, imageMarkup, staticImage } from "smoodly/astro/runtime";
8
+ import { output } from "smoodly:build";
9
9
 
10
- const { image, aspect, sizes, priority, loading, alt, unoptimized, class: className, style, ...data } = Astro.props;
11
- const placed = placeImage(image, { aspect });
12
- const request = imageRequest(image, { sizes, unoptimized });
13
- const transformed = request ? await getImage(request) : null;
14
- const src = transformed?.src ?? image.url;
15
- const srcset = transformed?.srcSet.attribute || undefined;
10
+ const { image, aspect, media, sizes, priority, loading, alt, quality, maxWidth, class: className, style, ...data } = Astro.props;
11
+ let markup = imageMarkup(image, { aspect, media, sizes, priority, loading, alt, quality, maxWidth });
12
+ if (output === "static" && Astro.isPrerendered) markup = await staticImage(markup);
13
+ const html = imageHtml(markup, { class: className, style: typeof style === "string" && style ? style : undefined, data });
16
14
  ---
17
- <span class={className} style={typeof style === "string" && style ? `${placed.box};${style}` : placed.box} {...data}>
18
- <img src={src} srcset={srcset} sizes={srcset ? sizes : undefined} alt={alt ?? image.alt} width={image.width} height={image.height}
19
- loading={priority ? "eager" : loading ?? "lazy"} fetchpriority={priority ? "high" : undefined} decoding="async" style={placed.frame} />
20
- </span>
15
+ <Fragment set:html={html} />
@@ -1,5 +1,8 @@
1
1
  ---
2
- // A <video> over the bucket's CDN with its poster and size, no transcoding (spec 2026-09-08 media §9).
2
+ // A <video> from the media host with its poster (a cut where there is one) and size, no transcoding (spec 2026-09-08
3
+ // media §9).
4
+ import { posterUrl } from "smoodly/astro/runtime";
5
+
3
6
  const { video, ...rest } = Astro.props;
4
7
  ---
5
- <video src={video.url} poster={video.poster?.url} width={video.width} height={video.height} playsinline {...rest}></video>
8
+ <video src={video.url} poster={posterUrl(video)} width={video.width} height={video.height} playsinline {...rest}></video>
@@ -9,5 +9,8 @@ export type PageHead = {
9
9
  canonical?: string;
10
10
  /** locale → the page's address there, for a multi-locale site; empty otherwise. */
11
11
  languages: Record<string, string>;
12
+ /** The media host of the first image the page shows, for a preconnect (spec 2026-10-18 media on R2 and Images §7):
13
+ * the connection opens while the HTML is parsed. */
14
+ mediaOrigin?: string;
12
15
  };
13
16
  export declare function headOf(hit: SiteHit, locale: string, href: (path: string, locale: string) => string, locales: LocaleSet): PageHead;
@@ -1,3 +1,23 @@
1
+ import { isResolvedImage } from "../richtext-href.js";
2
+ /** The origin of the first resolved image in a document (its cut's, else its original's), depth first. */
3
+ function firstImageOrigin(value, depth = 0) {
4
+ if (depth > 32 || value === null || typeof value !== "object")
5
+ return undefined;
6
+ if (isResolvedImage(value) && typeof value.mime === "string" && value.mime.startsWith("image/")) {
7
+ try {
8
+ return new URL(value.cut ?? value.url).origin;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ for (const v of Array.isArray(value) ? value : Object.values(value)) {
15
+ const found = firstImageOrigin(v, depth + 1);
16
+ if (found)
17
+ return found;
18
+ }
19
+ return undefined;
20
+ }
1
21
  export function headOf(hit, locale, href, locales) {
2
22
  // A page type without seo: true still titles the document with its page title; an entry without seo leaves the title to the layout.
3
23
  const seo = hit.seo ?? (hit.kind === "page" ? { title: hit.title } : {});
@@ -8,6 +28,9 @@ export function headOf(hit, locale, href, locales) {
8
28
  if (alt?.path)
9
29
  languages[l] = href(alt.path, l);
10
30
  }
31
+ // What the page shows: a page's tree and its own fields, an entry's fields; never the social image alone.
32
+ const shown = hit.kind === "page" ? [hit.tree, hit.fields] : [hit.entry.fields];
33
+ const mediaOrigin = firstImageOrigin(shown);
11
34
  return {
12
35
  ...(seo.title ? { title: seo.title } : {}),
13
36
  ...(seo.description ? { description: seo.description } : {}),
@@ -15,5 +38,6 @@ export function headOf(hit, locale, href, locales) {
15
38
  noindex: seo.noindex === true,
16
39
  ...(own ? { canonical: href(own, locale) } : {}),
17
40
  languages,
41
+ ...(mediaOrigin ? { mediaOrigin } : {}),
18
42
  };
19
43
  }
@@ -6,18 +6,18 @@ import { mkdir, writeFile } from "node:fs/promises";
6
6
  import { dirname, join, relative } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { DEFAULT_ADMIN_PATH } from "../admin-path.js";
9
- import { createSupabaseStores, siteOptions } from "../bind.js";
10
9
  import { loadConfig, loadConfigFile } from "../cli/load-config.js";
11
- import { createSmoodlyClient } from "../client.js";
12
- import { createSmoodlyContent } from "../content/content-api.js";
10
+ import { apiTransport, createSmoodlyContent } from "../content/content-api.js";
13
11
  import { markerJson, MARKER_PATH } from "../deploy-state.js";
14
- import { resolveSmoodlyEnv, siteUrl } from "../env.js";
15
- import { mediaRemotePattern } from "../image/remote-pattern.js";
12
+ import { DEFAULT_API_URL, apiEnv, siteUrl } from "../env.js";
13
+ import { createSmoodlyApi } from "../api-client.js";
14
+ import { pushBody, pushSchema } from "../push.js";
16
15
  import { loadSmoodlyEnv } from "./env-files.js";
17
16
  import { astroRouting, localeHref } from "./locale-paths.js";
18
17
  import { PREVIEW_BASE } from "./preview.js";
19
18
  import { hostRedirects, redirectEntries, rootRedirect } from "./static.js";
20
- import { createSmoodlySite } from "../site.js";
19
+ import { collectStaticMedia, STATIC_MEDIA_DIR } from "./static-media.js";
20
+ import { downloadStaticMedia } from "./static-media-node.js";
21
21
  // In the repository this file is TypeScript; in the published package tsc made it JavaScript, and the
22
22
  // .astro files were copied beside it unchanged.
23
23
  const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
@@ -75,10 +75,30 @@ export default function smoodly(options = {}) {
75
75
  let output = "server";
76
76
  let root = process.cwd();
77
77
  let watermark = null;
78
- /** The project's stores at build, from its config file (the one `options.config` names) and the env the setup hook loaded. */
79
- const buildStores = async () => {
80
- const loaded = options.config ? await loadConfigFile(join(root, options.config), root) : await loadConfig(root);
81
- return { loaded, ...createSupabaseStores(createSmoodlyClient(resolveSmoodlyEnv(process.env)), loaded) };
78
+ /** The files the prerendered pages registered (static output). */
79
+ let staticMedia = new Map();
80
+ /** astro.config's `i18n`, which the build holds the config's `routing` against. */
81
+ let i18n;
82
+ /** The project's config at build, from its config file (the one `options.config` names), loaded once and shared by
83
+ * every hook; and its API client, from the env the setup hook loaded, made once. Two steps, since the routing
84
+ * check wants the config before any env is needed. */
85
+ let builtConfig = null;
86
+ const buildConfig = () => (builtConfig ?? (builtConfig = options.config ? loadConfigFile(join(root, options.config), root) : loadConfig(root)));
87
+ let builtApi = null;
88
+ const buildApi = () => (builtApi ?? (builtApi = (async () => {
89
+ const loaded = await buildConfig();
90
+ return { loaded, api: createSmoodlyApi({ ...apiEnv(process.env), config: loaded }) };
91
+ })()));
92
+ /** A build read that fails names the service's address and stops the build before any page renders (a static build
93
+ * with the Worker down would otherwise fail page by page). */
94
+ const fromApi = async (read) => {
95
+ try {
96
+ return await read();
97
+ }
98
+ catch (e) {
99
+ const address = process.env.SMOODLY_API_URL || DEFAULT_API_URL;
100
+ throw new Error(`smoodly: the static build needs Smoodly's API — SMOODLY_API_URL (${address}) did not answer: ${e instanceof Error ? e.message : String(e)}. Start the service (npm run dev:service) or set the address.`);
101
+ }
82
102
  };
83
103
  return {
84
104
  name: "smoodly",
@@ -86,23 +106,21 @@ export default function smoodly(options = {}) {
86
106
  "astro:config:setup": async ({ config, command, updateConfig, injectRoute, addMiddleware, logger }) => {
87
107
  root = fileURLToPath(config.root);
88
108
  output = config.output === "static" ? "static" : "server";
109
+ i18n = config.i18n;
89
110
  // Root-relative with a leading slash, as Vite's import.meta.glob reads it; a source dir that is the root itself is "".
90
111
  const rel = relative(root, fileURLToPath(config.srcDir)).split("\\").join("/");
91
112
  const srcDir = rel === "" ? "" : "/" + rel;
92
113
  loadSmoodlyEnv(root, command === "dev" ? "development" : "production");
93
- // The media host, for Astro's image service: <Image> is served resized through /_image only from an allowed host.
94
- const mediaUrl = process.env.SMOODLY_URL;
95
- if (!mediaUrl)
96
- logger.warn("smoodly: SMOODLY_URL is not set at build — every image will fail: Astro's /_image serves only hosts in image.remotePatterns, and the media host is taken from SMOODLY_URL here. Set it where `astro build` runs.");
114
+ // No image service config: <Image> is plain markup over the cuts from Smoodly's media host (spec 2026-10-18 media
115
+ // on R2 and Images §7), so no remote pattern and no /_image.
97
116
  updateConfig({
98
- image: { remotePatterns: mediaUrl ? [mediaRemotePattern(mediaUrl)] : [] },
99
117
  vite: { plugins: [virtualModules(root, options.config ?? "smoodly.config.ts", srcDir, output)] },
100
118
  });
101
119
  for (const route of routes(adminPath, output))
102
120
  injectRoute(route);
103
121
  addMiddleware({ entrypoint: here(`./middleware${ext}`), order: "pre" });
104
122
  if (output === "server" && !config.cache?.provider) {
105
- logger.warn("smoodly: no cache provider in astro.config — every page renders from the database. Add `cache: { provider: memoryCache() }` (from \"astro/config\").");
123
+ logger.warn("smoodly: no cache provider in astro.config — every page renders from the service. Add `cache: { provider: memoryCache() }` (from \"astro/config\").");
106
124
  }
107
125
  // astro.config is evaluated before Astro loads .env, so `site: process.env.SITE_URL` there sees only the
108
126
  // shell's value; the env files were read above, and SITE_URL from them sets `site` here. A value
@@ -122,10 +140,10 @@ export default function smoodly(options = {}) {
122
140
  // Static output (spec 2026-10-09 static mode §3): the redirect rows and the prefix-always root become the
123
141
  // host's redirects, answered without a function. Read at build only; `astro dev` serves them on demand.
124
142
  if (output === "static" && command === "build") {
125
- const { loaded, pages } = await buildStores();
126
- const routing = astroRouting(config.i18n, loaded.locales);
143
+ const { loaded, api } = await buildApi();
144
+ const routing = astroRouting(config.i18n, loaded.locales, loaded.routing);
127
145
  const href = localeHref(loaded.locales, routing);
128
- const redirects = { ...(await redirectEntries({ pages, config: loaded, href })), ...rootRedirect(routing, href, loaded.locales) };
146
+ const redirects = { ...(await fromApi(() => redirectEntries({ delivery: api, config: loaded, href }))), ...rootRedirect(routing, href, loaded.locales) };
129
147
  updateConfig({ redirects: hostRedirects(config.redirects, redirects) });
130
148
  logger.info(`smoodly: static output — ${Object.keys(redirects).length} redirects handed to the host.`);
131
149
  }
@@ -133,15 +151,33 @@ export default function smoodly(options = {}) {
133
151
  // The marker's watermark (spec 2026-10-09 static mode §5) is read BEFORE any page renders: a publish that lands
134
152
  // while the build runs is then newer than it and counts as waiting, which it is — the build may not hold it. A
135
153
  // row the build itself rebuilds keeps its publishedAt (a heal is not a publish, project.ts), so it never counts.
136
- "astro:build:start": async () => {
154
+ // The schema push (spec 2026-10-16 the control plane §4) comes first, in either output: the space then holds the
155
+ // registry this build renders with. A failed push never fails the build; the site runs against the schema the space has.
156
+ "astro:build:start": async ({ logger }) => {
157
+ const loaded = await buildConfig();
158
+ // The runtime's startup check, at build in either output: a deploy that builds must not refuse every request.
159
+ astroRouting(i18n, loaded.locales, loaded.routing);
160
+ try {
161
+ const env = apiEnv(process.env);
162
+ const { version, changed } = await pushSchema(env, pushBody(loaded, process.env, output === "static" ? "static" : "live"));
163
+ logger.info(changed ? `smoodly: pushed schema version ${version} to ${env.spaceId}.` : `smoodly: schema unchanged (version ${version}).`);
164
+ }
165
+ catch (e) {
166
+ logger.warn(`smoodly: the schema push failed — ${e instanceof Error ? e.message : String(e)}; the site runs against the schema the space has.`);
167
+ }
137
168
  if (output !== "static")
138
169
  return;
139
- watermark = await (await buildStores()).published.newestPublishedAt();
170
+ // The media every prerendered page registers (static-media.ts), collected in this process for the build's end.
171
+ staticMedia = collectStaticMedia();
172
+ const { api } = await buildApi();
173
+ watermark = await fromApi(() => api.newestPublishedAt());
140
174
  },
141
175
  // The build marker: beside the pages, the only trace of Smoodly in a static build.
142
176
  "astro:build:done": async ({ dir, logger }) => {
143
177
  if (output !== "static")
144
178
  return;
179
+ // Every image the pages show, into the output beside them (spec 2026-10-18 media on R2 and Images §8).
180
+ logger.info(`smoodly: wrote ${await downloadStaticMedia(staticMedia.values(), fileURLToPath(dir))} media files into ${STATIC_MEDIA_DIR}.`);
145
181
  const marker = markerJson({ builtAt: Date.now(), publishedThrough: watermark });
146
182
  const file = join(fileURLToPath(dir), MARKER_PATH);
147
183
  await mkdir(dirname(file), { recursive: true });
@@ -162,10 +198,8 @@ export function createContent(config, options = {}) {
162
198
  loadSmoodlyEnv(options.root ?? process.cwd(), process.env.NODE_ENV === "production" ? "production" : "development");
163
199
  env = process.env;
164
200
  }
165
- const db = createSmoodlyClient(resolveSmoodlyEnv(env));
166
- const { pages, entries, assets, members, published } = createSupabaseStores(db, config);
167
- const href = localeHref(config.locales, astroRouting(options.i18n, config.locales));
168
- // The site's projector: a publish from a script writes the delivery rows as the admin's does.
169
- const { projector } = createSmoodlySite(siteOptions(config, () => ({ pages, entries, assets, published })));
170
- return { content: createSmoodlyContent({ config, pages, entries, assets, members, href, projector }), href };
201
+ const href = localeHref(config.locales, astroRouting(options.i18n, config.locales, config.routing));
202
+ // Over Smoodly's API with the secret key (spec 2026-10-14 §1): the service runs the operations, so a publish from a
203
+ // script writes the delivery rows and expires the site's tags as the admin's does.
204
+ return { content: createSmoodlyContent({ config, href, transport: apiTransport(apiEnv(env)) }), href };
171
205
  }
@@ -26,5 +26,8 @@ export declare function splitLocale(pathname: string, locales: LocaleSet, routin
26
26
  } | null;
27
27
  /** Locale-relative CMS path → site URL: what the operations and the `page` prop link with. */
28
28
  export declare const localeHref: (locales: LocaleSet, routing?: LocaleRouting) => (path: string, locale: string) => string;
29
- /** The rule astro.config's i18n asks for; its locales must be the config's, or links and reads would disagree. */
30
- export declare function astroRouting(i18n: AstroI18n | undefined, locales: LocaleSet): LocaleRouting;
29
+ /** The rule astro.config's i18n asks for; its locales must be the config's, and its prefixDefaultLocale the config's
30
+ * `routing` (which the pushed registry carries to the space's links), or links and reads would disagree. */
31
+ export declare function astroRouting(i18n: AstroI18n | undefined, locales: LocaleSet, configRouting?: {
32
+ prefixDefaultLocale: boolean;
33
+ }): LocaleRouting;
@@ -21,8 +21,16 @@ export function splitLocale(pathname, locales, routing = DEFAULT_ROUTING) {
21
21
  }
22
22
  /** Locale-relative CMS path → site URL: what the operations and the `page` prop link with. */
23
23
  export const localeHref = (locales, routing = DEFAULT_ROUTING) => (path, locale) => locale === locales.default && !routing.prefixDefault ? path : path === "/" ? `/${locale}` : `/${locale}${path}`;
24
- /** The rule astro.config's i18n asks for; its locales must be the config's, or links and reads would disagree. */
25
- export function astroRouting(i18n, locales) {
24
+ /** The rule astro.config's i18n asks for; its locales must be the config's, and its prefixDefaultLocale the config's
25
+ * `routing` (which the pushed registry carries to the space's links), or links and reads would disagree. */
26
+ export function astroRouting(i18n, locales, configRouting) {
27
+ const routing = readAstroRouting(i18n, locales);
28
+ if (configRouting && configRouting.prefixDefaultLocale !== routing.prefixDefault) {
29
+ throw new Error(`smoodly: astro.config's i18n.routing.prefixDefaultLocale is ${routing.prefixDefault} but smoodly.config's routing.prefixDefaultLocale is ${configRouting.prefixDefaultLocale} — make them agree.`);
30
+ }
31
+ return routing;
32
+ }
33
+ function readAstroRouting(i18n, locales) {
26
34
  if (!i18n)
27
35
  return DEFAULT_ROUTING;
28
36
  if (i18n.domains && Object.keys(i18n.domains).length > 0) {
@@ -1,26 +1,28 @@
1
1
  // The admin in an Astro project (spec 2026-10-02 astro cloud fit §4), injected by the integration at the
2
- // admin path: the page, the package's files and the operations endpoint are the framework-free handler's
3
- // (admin/handler.ts), made once per server. This file adds only what is Astro's: the tags an operation
4
- // expires go to the response cache together, and draft mode is the draft cookie, scoped to the preview
5
- // address. The request's own effects ride an AsyncLocalStorage, so one handler serves requests at once.
2
+ // admin path: the page, the package's files, the revalidate route and the preview route are the
3
+ // framework-free handler's (admin/handler.ts), made once per server. This file adds only what is Astro's:
4
+ // the tags the service's revalidate call names go to the response cache together, and draft mode is the
5
+ // draft cookie, scoped to the preview address. The request's own effects ride an AsyncLocalStorage, so one handler serves requests at once.
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { createAdminHandler } from "../../admin/handler.js";
8
8
  import { expire } from "../cache.js";
9
9
  import { DRAFT_COOKIE, PREVIEW_BASE, draftCookieValue } from "../preview.js";
10
- import { adminDeps, deployDeps, secret } from "../server.js";
10
+ import { output } from "smoodly:build";
11
+ import { adminDeps, secret } from "../server.js";
11
12
  export const prerender = false;
12
13
  const current = new AsyncLocalStorage();
13
14
  const effects = () => {
14
15
  const got = current.getStore();
15
16
  if (!got)
16
- throw new Error("smoodly: an admin operation ran outside its request.");
17
+ throw new Error("smoodly: an admin route ran outside its request.");
17
18
  return got;
18
19
  };
19
20
  const secure = (context) => context.url.protocol === "https:" || context.request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim() === "https";
20
21
  const handle = createAdminHandler(() => ({
21
22
  ...adminDeps(),
22
23
  previewBase: PREVIEW_BASE,
23
- deploy: deployDeps(),
24
+ // The Deploy tile's mode is the build's output; the deploy operations themselves run in the service.
25
+ delivery: output === "static" ? "static" : "live",
24
26
  effects: {
25
27
  revalidate: (tag) => void effects().expired.add(tag),
26
28
  draft: {
@@ -8,7 +8,8 @@ export { crashMessage } from "../crash-message.ts";
8
8
  export { styleAttribute } from "./style.ts";
9
9
  export { richtextChunks, type RichTextChunk } from "./richtext.ts";
10
10
  export { CUSTOM_NODE_TYPE } from "../custom/schema.ts";
11
- export { IMAGE_WIDTHS, imageRequest, imageWidths, placeImage } from "./image.ts";
11
+ export { imageHtml, imageMarkup, posterUrl } from "../image/markup.ts";
12
+ export { staticImage } from "./static-media.ts";
12
13
  export { previewPath, previewTarget } from "./preview.ts";
13
14
  export { href } from "./server.ts";
14
15
  export type { Shell, SmoodlyLocals } from "./shell.ts";
@@ -18,7 +19,7 @@ export type { PageHead } from "./head.ts";
18
19
  export type { ViewFiles } from "./views-check.ts";
19
20
  import { type Shell, type SmoodlyLocals } from "./shell.ts";
20
21
  /** The shell's decision with the project's bound site: what routes/page.astro calls, with `Astro.cache`.
21
- * A page read from the database gets its lifetime here, before its view renders. The first call compares
22
+ * A page read from the service gets its lifetime here, before its view renders. The first call compares
22
23
  * the project's view files to the registry and names every disagreement once. */
23
24
  export declare function shell(pathname: string, search: string, state: SmoodlyLocals, views: ViewFiles, cache?: unknown): Promise<Shell>;
24
25
  /** getStaticPaths for the static page shell (spec 2026-10-09 static mode §3): every published address of the bound site. */
@@ -11,20 +11,21 @@ export { crashMessage } from "../crash-message.js";
11
11
  export { styleAttribute } from "./style.js";
12
12
  export { richtextChunks } from "./richtext.js";
13
13
  export { CUSTOM_NODE_TYPE } from "../custom/schema.js";
14
- export { IMAGE_WIDTHS, imageRequest, imageWidths, placeImage } from "./image.js";
14
+ export { imageHtml, imageMarkup, posterUrl } from "../image/markup.js";
15
+ export { staticImage } from "./static-media.js";
15
16
  export { previewPath, previewTarget } from "./preview.js";
16
17
  export { href } from "./server.js";
17
18
  import { customLayout } from "./custom.js";
18
19
  import { registryInfo } from "./defaults.js";
19
20
  import { lifetime } from "./cache.js";
20
21
  import { astroReads, preload } from "./scope.js";
21
- import { config, href, pages, published, routing, site } from "./server.js";
22
+ import { config, delivery, href, routing, site } from "./server.js";
22
23
  import { staticPaths as listStatic } from "./static.js";
23
24
  import { missingViews } from "./views-check.js";
24
25
  import { resolveShell } from "./shell.js";
25
26
  let viewsChecked = false;
26
27
  /** The shell's decision with the project's bound site: what routes/page.astro calls, with `Astro.cache`.
27
- * A page read from the database gets its lifetime here, before its view renders. The first call compares
28
+ * A page read from the service gets its lifetime here, before its view renders. The first call compares
28
29
  * the project's view files to the registry and names every disagreement once. */
29
30
  export async function shell(pathname, search, state, views, cache) {
30
31
  if (!viewsChecked) {
@@ -40,7 +41,7 @@ export async function shell(pathname, search, state, views, cache) {
40
41
  return result;
41
42
  }
42
43
  /** getStaticPaths for the static page shell (spec 2026-10-09 static mode §3): every published address of the bound site. */
43
- export const staticPaths = () => listStatic({ published: published(), pages: pages(), config, href });
44
+ export const staticPaths = () => listStatic({ delivery: delivery(), config, href });
44
45
  /** The registry's titles and settings defaults, for the zone and the richtext renderer. */
45
46
  export const info = registryInfo(config.registry);
46
47
  /** The custom section's rows and layout css, at the project's (already resolved) stacking breakpoint. */
@@ -1,22 +1,18 @@
1
1
  import type { AdminHandlerDeps } from "../admin/handler.ts";
2
2
  import type { ResolvedConfig } from "../config.ts";
3
+ import type { ContentReads, DeliveryReads } from "../site.ts";
3
4
  export declare const config: ResolvedConfig;
4
5
  /** The address rule: astro.config's i18n when it is set, else the default locale at the root. */
5
6
  export declare const routing: import("./locale-paths.ts").LocaleRouting;
6
7
  /** Locale-relative CMS path → site URL, for the operations and the `page` prop. */
7
8
  export declare const href: (path: string, locale: string) => string;
8
- export declare const site: () => import("../site.ts").SmoodlySite;
9
- /** The delivery store: the static shell lists its addresses from it, and the deploy operations read it. */
10
- export declare const published: () => import("../supabase-published-store.ts").SupabasePublishedStore;
11
- /** The page store, for the paths index the static shell lists entry addresses from. */
12
- export declare const pages: () => import("../supabase-store.ts").SupabasePageStore;
13
- /** The deploy operations' deps in static output (spec 2026-10-09 static mode §7); undefined on a live site. */
14
- export declare const deployDeps: () => (import("../admin/ops-impl.ts").DeployDeps & {
15
- mode: "live" | "static";
16
- }) | undefined;
9
+ export declare const site: () => ContentReads;
10
+ /** The delivery reads of the static shell and the build: the addresses and the watermark, over the API. */
11
+ export declare const delivery: () => DeliveryReads;
17
12
  /** The draft cookie's key: the server-only key every Smoodly site already holds. */
18
13
  export declare const secret: () => string;
19
- type AdminDeps = Omit<AdminHandlerDeps, "effects" | "previewBase">;
20
- /** The admin's wiring. The publishable key is asked for here, not on a live page. */
14
+ type AdminDeps = Omit<AdminHandlerDeps, "effects" | "previewBase" | "delivery">;
15
+ /** The admin's wiring: the page, the revalidate route the service calls and the preview route the browser
16
+ * posts the grant to. Operations run in the service, which also tells the page where editors sign in. */
21
17
  export declare function adminDeps(): AdminDeps;
22
18
  export {};
@@ -1,52 +1,46 @@
1
- // The bound stores and site layer of an Astro project (spec 2026-10-02 astro cloud fit §1), built from its
2
- // smoodly.config.ts through the virtual module: what the Next starter's server.ts glue does, with no file
3
- // in the app. Built on first use, so a build that renders nothing needs no keys.
4
- import { i18n, site as configuredSite } from "astro:config/server";
5
- import { output } from "smoodly:build";
1
+ // The bound reads of an Astro project (spec 2026-10-02 astro cloud fit §1; spec 2026-10-14 §1), built from its
2
+ // smoodly.config.ts through the virtual module: what the Next starter's server.ts glue does, with no file in the
3
+ // app. Every read goes over Smoodly's content API; the site opens no store. Built on first use, so a build that
4
+ // renders nothing needs no keys.
5
+ import { i18n } from "astro:config/server";
6
6
  import { config as loaded } from "smoodly:config";
7
- import { deployDepsOf } from "./deploy-deps.js";
8
- import { supabaseAdminAuth } from "../admin/supabase-auth.js";
9
- import { createSupabaseStores, siteOptions } from "../bind.js";
10
- import { createSmoodlyClient } from "../client.js";
11
- import { presenceConfig, smoodlyEnv } from "../env.js";
12
- import { createSmoodlySite } from "../site.js";
7
+ import { output } from "smoodly:build";
8
+ import { createSmoodlyApi } from "../api-client.js";
9
+ import { pushBody } from "../push.js";
10
+ import { apiEnv, presenceConfig, smoodlyEnv } from "../env.js";
13
11
  import { astroRouting, localeHref } from "./locale-paths.js";
14
12
  export const config = loaded;
15
13
  if (!config?.registry) {
16
14
  throw new Error("smoodly: smoodly.config.ts must export `config`, the result of defineConfig(…).");
17
15
  }
18
16
  /** The address rule: astro.config's i18n when it is set, else the default locale at the root. */
19
- export const routing = astroRouting(i18n, config.locales);
17
+ export const routing = astroRouting(i18n, config.locales, config.routing);
20
18
  /** Locale-relative CMS path → site URL, for the operations and the `page` prop. */
21
19
  export const href = localeHref(config.locales, routing);
22
20
  function build() {
23
- const db = createSmoodlyClient(smoodlyEnv());
24
- const stores = createSupabaseStores(db, config);
25
- const site = createSmoodlySite(siteOptions(config, () => ({ pages: stores.pages, entries: stores.entries, assets: stores.assets, published: stores.published })));
26
- return { db, ...stores, site };
21
+ // The site's reads go over Smoodly's API (SMOODLY_API_URL, SMOODLY_SPACE_ID, SMOODLY_SECRET_KEY). In `astro dev` the
22
+ // client pushes the schema first, in the mode the build would (spec 2026-10-16 the control plane §4).
23
+ const dev = process.env.NODE_ENV === "development";
24
+ const api = createSmoodlyApi({ ...apiEnv(), config, push: dev ? pushBody(config, process.env, output === "static" ? "static" : "live") : false });
25
+ return { api };
27
26
  }
28
27
  let cached = null;
29
28
  const bound = () => (cached ?? (cached = build()));
30
- export const site = () => bound().site;
31
- /** The delivery store: the static shell lists its addresses from it, and the deploy operations read it. */
32
- export const published = () => bound().published;
33
- /** The page store, for the paths index the static shell lists entry addresses from. */
34
- export const pages = () => bound().pages;
35
- /** The deploy operations' deps in static output (spec 2026-10-09 static mode §7); undefined on a live site. */
36
- export const deployDeps = () => deployDepsOf({ output, site: configuredSite, env: process.env, store: published() });
29
+ export const site = () => bound().api;
30
+ /** The delivery reads of the static shell and the build: the addresses and the watermark, over the API. */
31
+ export const delivery = () => bound().api;
37
32
  /** The draft cookie's key: the server-only key every Smoodly site already holds. */
38
33
  export const secret = () => smoodlyEnv().secretKey;
39
34
  let admin = null;
40
- /** The admin's wiring. The publishable key is asked for here, not on a live page. */
35
+ /** The admin's wiring: the page, the revalidate route the service calls and the preview route the browser
36
+ * posts the grant to. Operations run in the service, which also tells the page where editors sign in. */
41
37
  export function adminDeps() {
42
38
  if (admin)
43
39
  return admin;
44
40
  const env = smoodlyEnv("admin");
45
- const { db, pages, entries, assets, members, site } = bound();
46
41
  admin = {
47
- config, pages, entries, assets, members, href, projector: site.projector,
48
- auth: supabaseAdminAuth({ authUrl: env.authUrl, authKey: env.authKey, db }),
49
- signIn: { url: env.authUrl, key: env.authKey },
42
+ // Where editors sign in comes from the service (GET /v1/auth, spec 2026-10-16 sign-in and the dashboard §4).
43
+ config, apiUrl: env.apiUrl, spaceId: env.spaceId,
50
44
  presence: () => presenceConfig(env),
51
45
  };
52
46
  return admin;
@@ -2,7 +2,7 @@ import type { ResolvedConfig } from "../config.ts";
2
2
  import { type ReadAdapter } from "../reads.ts";
3
3
  import { type EntryItem } from "../read-shapes.ts";
4
4
  import type { PageContext } from "../page-context.ts";
5
- import { type SmoodlySite } from "../site.ts";
5
+ import { type ContentReads } from "../site.ts";
6
6
  import { type PageHead } from "./head.ts";
7
7
  import { type LocaleRouting } from "./locale-paths.ts";
8
8
  import type { ZoneContent } from "./zone.ts";
@@ -14,7 +14,7 @@ export type SmoodlyLocals = {
14
14
  editor: boolean;
15
15
  /** History's stored version, inside a preview only. */
16
16
  version?: string;
17
- /** Set by the shell when it rendered a page read from the database: the response may be cached under its tags. */
17
+ /** Set by the shell when it rendered a page read from the service: the response may be cached under its tags. */
18
18
  cacheable: boolean;
19
19
  /** Set by the shell when it rendered a page at all: in a preview, only that response is a canvas. */
20
20
  shell: boolean;
@@ -29,7 +29,7 @@ declare global {
29
29
  }
30
30
  }
31
31
  export type ShellDeps = {
32
- site: SmoodlySite;
32
+ site: ContentReads;
33
33
  config: ResolvedConfig;
34
34
  href: (path: string, locale: string) => string;
35
35
  routing: LocaleRouting;
@@ -52,7 +52,7 @@ export async function resolveShell(deps, pathname, search, state, views) {
52
52
  console.warn(`smoodly: no src/page-types/${hit.template}.astro — the record at ${hit.path} is a 404.`);
53
53
  return NOT_FOUND;
54
54
  }
55
- // Only a page read from the database has a tag to expire it by.
55
+ // Only a page read from the service has a tag to expire it by.
56
56
  state.cacheable = !hit.record.id.startsWith("code:") && !state.draft;
57
57
  state.shell = true;
58
58
  const zoneNames = Object.keys(schema.zones ?? {});
@@ -0,0 +1,4 @@
1
+ import type { StaticEntry } from "./static-media.ts";
2
+ /** Every file, fetched eight at a time (a cut asked in its format by Accept) and written under `outDir`; a file that
3
+ * answers non-200 fails the build by its address. Answers how many were written. */
4
+ export declare function downloadStaticMedia(entries: Iterable<StaticEntry>, outDir: string, fetcher?: typeof fetch): Promise<number>;
@@ -0,0 +1,25 @@
1
+ // The Node half of static media (static-media.ts): the download the build runs at `astro:build:done` over what the
2
+ // prerendered pages registered. Loaded by the integration only: a live page never imports it.
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ const CONCURRENCY = 8;
6
+ const ACCEPT = { avif: "image/avif", webp: "image/webp", original: undefined };
7
+ /** Every file, fetched eight at a time (a cut asked in its format by Accept) and written under `outDir`; a file that
8
+ * answers non-200 fails the build by its address. Answers how many were written. */
9
+ export async function downloadStaticMedia(entries, outDir, fetcher = fetch) {
10
+ const queue = [...entries];
11
+ const total = queue.length;
12
+ const work = async () => {
13
+ for (let entry = queue.shift(); entry; entry = queue.shift()) {
14
+ const accept = ACCEPT[entry.format];
15
+ const res = await fetcher(entry.url, accept ? { headers: { accept } } : {});
16
+ if (res.status !== 200)
17
+ throw new Error(`smoodly: the static build could not download ${entry.url} (${res.status}) — every image a page shows is written into the output.`);
18
+ const target = join(outDir, entry.path);
19
+ await mkdir(dirname(target), { recursive: true });
20
+ await writeFile(target, new Uint8Array(await res.arrayBuffer()));
21
+ }
22
+ };
23
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, total) }, work));
24
+ return total;
25
+ }
@@ -0,0 +1,17 @@
1
+ import type { ImageMarkup } from "../image/markup.ts";
2
+ export type StaticFormat = "avif" | "webp" | "original";
3
+ /** One file a static build writes: where it comes from, in which format, and its path in the output. */
4
+ export type StaticEntry = {
5
+ url: string;
6
+ format: StaticFormat;
7
+ path: string;
8
+ };
9
+ export declare const STATIC_MEDIA_DIR = "/_smoodly/media";
10
+ /** `/_smoodly/media/<sha256(sourceUrl + format), first 16 hex>.<ext>`: the same address and format, the same file. */
11
+ export declare function staticMediaPath(sourceUrl: string, format: StaticFormat, ext: string): Promise<string>;
12
+ /** The markup with every media address replaced by a file of the output, each file handed to `register`. */
13
+ export declare function rewriteForStatic(m: ImageMarkup, register: (entry: StaticEntry) => void): Promise<ImageMarkup>;
14
+ /** The build's collector (index.ts, astro:build:start): every file the pages register, once per path. */
15
+ export declare function collectStaticMedia(): Map<string, StaticEntry>;
16
+ /** What Image.astro calls in a prerendered page of static output: the rewrite, each file handed to the build. */
17
+ export declare function staticImage(m: ImageMarkup): Promise<ImageMarkup>;