smoodly 0.0.51 → 0.0.52

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 (285) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth-config.d.ts +6 -5
  3. package/dist/admin/auth.d.ts +3 -11
  4. package/dist/admin/auth.js +1 -30
  5. package/dist/admin/browser/fetch-op.js +3 -2
  6. package/dist/admin/browser/main.js +6 -2
  7. package/dist/admin/client-ops.d.ts +5 -4
  8. package/dist/admin/client-ops.js +14 -11
  9. package/dist/admin/delivery-mode.d.ts +1 -1
  10. package/dist/admin/delivery-mode.js +2 -2
  11. package/dist/admin/drift.d.ts +0 -2
  12. package/dist/admin/drift.js +1 -1
  13. package/dist/admin/editor/EditorView.d.ts +0 -3
  14. package/dist/admin/editor/EditorView.js +11 -16
  15. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  16. package/dist/admin/editor/FloatingToolbar.js +1 -1
  17. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  18. package/dist/admin/editor/SeoSettings.js +2 -2
  19. package/dist/admin/editor/canvas-address.d.ts +8 -0
  20. package/dist/admin/editor/canvas-address.js +18 -2
  21. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  22. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  23. package/dist/admin/editor/inline-dom.d.ts +0 -4
  24. package/dist/admin/editor/inline-dom.js +1 -1
  25. package/dist/admin/editor/preview-grant.d.ts +6 -0
  26. package/dist/admin/editor/preview-grant.js +25 -0
  27. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  28. package/dist/admin/files.generated.js +1 -1
  29. package/dist/admin/fixed-nodes.d.ts +0 -6
  30. package/dist/admin/fixed-nodes.js +1 -81
  31. package/dist/admin/forms/CropEditor.d.ts +4 -2
  32. package/dist/admin/forms/CropEditor.js +58 -5
  33. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  34. package/dist/admin/forms/ImagePreview.js +9 -3
  35. package/dist/admin/forms/MediaGrid.js +2 -1
  36. package/dist/admin/forms/MediaPicker.js +9 -6
  37. package/dist/admin/forms/MediaWidget.js +11 -9
  38. package/dist/admin/forms/assets-context.d.ts +7 -3
  39. package/dist/admin/forms/assets-context.js +5 -3
  40. package/dist/admin/forms/crop-editor.d.ts +24 -2
  41. package/dist/admin/forms/crop-editor.js +36 -2
  42. package/dist/admin/forms/date-picker.d.ts +0 -1
  43. package/dist/admin/forms/date-picker.js +1 -1
  44. package/dist/admin/forms/tabs.d.ts +0 -1
  45. package/dist/admin/forms/tabs.js +1 -1
  46. package/dist/admin/forms/upload.d.ts +18 -11
  47. package/dist/admin/forms/upload.js +62 -24
  48. package/dist/admin/global-items.d.ts +0 -3
  49. package/dist/admin/global-items.js +0 -54
  50. package/dist/admin/handler.d.ts +22 -28
  51. package/dist/admin/handler.js +146 -47
  52. package/dist/admin/health.d.ts +0 -4
  53. package/dist/admin/index.d.ts +5 -8
  54. package/dist/admin/index.js +3 -6
  55. package/dist/admin/ops.d.ts +36 -22
  56. package/dist/admin/page-data.d.ts +14 -6
  57. package/dist/admin/page-data.js +1 -1
  58. package/dist/admin/session-flow.d.ts +43 -0
  59. package/dist/admin/session-flow.js +94 -0
  60. package/dist/admin/shell/AdminApp.d.ts +8 -3
  61. package/dist/admin/shell/AdminApp.js +37 -58
  62. package/dist/admin/shell/ContentHealthPage.js +1 -3
  63. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  64. package/dist/admin/shell/GlobalFields.js +1 -1
  65. package/dist/admin/shell/IconRail.js +5 -4
  66. package/dist/admin/shell/MediaScreen.js +9 -6
  67. package/dist/admin/shell/MembersPage.d.ts +1 -2
  68. package/dist/admin/shell/MembersPage.js +19 -90
  69. package/dist/admin/shell/SettingsView.d.ts +1 -2
  70. package/dist/admin/shell/SettingsView.js +2 -2
  71. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  72. package/dist/admin/shell/SignedOutView.js +9 -0
  73. package/dist/admin/shell/console-links.d.ts +5 -0
  74. package/dist/admin/shell/console-links.js +11 -0
  75. package/dist/admin/shell/members-list.d.ts +1 -14
  76. package/dist/admin/shell/members-list.js +2 -23
  77. package/dist/admin/shell/pages-tree.d.ts +0 -2
  78. package/dist/admin/shell/pages-tree.js +1 -1
  79. package/dist/admin/shell/presence-socket.d.ts +10 -0
  80. package/dist/admin/shell/presence-socket.js +191 -0
  81. package/dist/admin/shell/presence.d.ts +6 -18
  82. package/dist/admin/shell/presence.js +4 -11
  83. package/dist/admin/shell/session.d.ts +20 -12
  84. package/dist/admin/shell/session.js +113 -47
  85. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  86. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  87. package/dist/admin/ui/primitives.d.ts +4 -14
  88. package/dist/admin/ui/primitives.js +5 -8
  89. package/dist/admin/ui/theme.js +13 -2
  90. package/dist/admin/validate.js +10 -1
  91. package/dist/api-client.d.ts +26 -0
  92. package/dist/api-client.js +107 -0
  93. package/dist/assemble.d.ts +4 -41
  94. package/dist/assemble.js +4 -302
  95. package/dist/asset-store.d.ts +28 -30
  96. package/dist/asset-store.js +26 -121
  97. package/dist/astro/components/Head.astro +4 -1
  98. package/dist/astro/components/Image.astro +12 -17
  99. package/dist/astro/components/Video.astro +5 -2
  100. package/dist/astro/head.d.ts +3 -0
  101. package/dist/astro/head.js +24 -0
  102. package/dist/astro/index.js +54 -25
  103. package/dist/astro/locale-paths.d.ts +5 -2
  104. package/dist/astro/locale-paths.js +10 -2
  105. package/dist/astro/routes/admin.js +9 -7
  106. package/dist/astro/runtime.d.ts +2 -1
  107. package/dist/astro/runtime.js +4 -3
  108. package/dist/astro/server.d.ts +7 -11
  109. package/dist/astro/server.js +22 -28
  110. package/dist/astro/shell.d.ts +2 -2
  111. package/dist/astro/static-media-node.d.ts +4 -0
  112. package/dist/astro/static-media-node.js +25 -0
  113. package/dist/astro/static-media.d.ts +17 -0
  114. package/dist/astro/static-media.js +55 -0
  115. package/dist/astro/static.d.ts +5 -10
  116. package/dist/astro/static.js +13 -29
  117. package/dist/canvas-marker.d.ts +14 -0
  118. package/dist/canvas-marker.js +36 -0
  119. package/dist/cli/api.d.ts +14 -0
  120. package/dist/cli/api.js +48 -0
  121. package/dist/cli/app-env.d.ts +2 -0
  122. package/dist/cli/app-env.js +20 -0
  123. package/dist/cli/export.d.ts +16 -2
  124. package/dist/cli/export.js +52 -35
  125. package/dist/cli/import.d.ts +7 -11
  126. package/dist/cli/import.js +50 -54
  127. package/dist/cli/load-config.d.ts +0 -1
  128. package/dist/cli/load-config.js +1 -1
  129. package/dist/cli/main.d.ts +4 -12
  130. package/dist/cli/main.js +26 -72
  131. package/dist/cli/push.d.ts +6 -0
  132. package/dist/cli/push.js +31 -0
  133. package/dist/collections.d.ts +0 -1
  134. package/dist/collections.js +1 -1
  135. package/dist/config.d.ts +20 -5
  136. package/dist/config.js +18 -1
  137. package/dist/content/content-api.d.ts +22 -23
  138. package/dist/content/content-api.js +46 -56
  139. package/dist/content/mime.d.ts +1 -0
  140. package/dist/content/mime.js +6 -0
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/deploy-store.js +1 -0
  143. package/dist/entry-store.d.ts +3 -89
  144. package/dist/entry-store.js +2 -550
  145. package/dist/env.d.ts +27 -20
  146. package/dist/env.js +38 -50
  147. package/dist/fields.d.ts +11 -3
  148. package/dist/fields.js +33 -2
  149. package/dist/global-id.d.ts +2 -0
  150. package/dist/global-id.js +4 -2
  151. package/dist/image/SmoodlyImage.d.ts +9 -5
  152. package/dist/image/SmoodlyImage.js +29 -15
  153. package/dist/image/SmoodlyVideo.d.ts +1 -1
  154. package/dist/image/SmoodlyVideo.js +3 -2
  155. package/dist/image/index.d.ts +2 -3
  156. package/dist/image/index.js +0 -2
  157. package/dist/image/markup.d.ts +53 -0
  158. package/dist/image/markup.js +111 -0
  159. package/dist/index.d.ts +12 -22
  160. package/dist/index.js +7 -20
  161. package/dist/media-cut.d.ts +44 -0
  162. package/dist/media-cut.js +70 -0
  163. package/dist/media.d.ts +57 -3
  164. package/dist/media.js +125 -15
  165. package/dist/member-store.d.ts +3 -22
  166. package/dist/member-store.js +1 -53
  167. package/dist/next/admin-route.d.ts +1 -1
  168. package/dist/next/admin-route.js +5 -3
  169. package/dist/next/meta.js +2 -3
  170. package/dist/next/page-renderer.d.ts +2 -2
  171. package/dist/next/page-renderer.js +22 -11
  172. package/dist/next/queries.d.ts +2 -2
  173. package/dist/path-index.d.ts +0 -24
  174. package/dist/path-index.js +0 -98
  175. package/dist/push.d.ts +30 -0
  176. package/dist/push.js +52 -0
  177. package/dist/read-counter.d.ts +2 -0
  178. package/dist/read-counter.js +6 -0
  179. package/dist/reads.d.ts +7 -7
  180. package/dist/reads.js +3 -0
  181. package/dist/registry-json.d.ts +11 -0
  182. package/dist/registry-json.js +81 -0
  183. package/dist/render.d.ts +0 -1
  184. package/dist/render.js +1 -1
  185. package/dist/resolve.d.ts +12 -6
  186. package/dist/resolve.js +11 -8
  187. package/dist/revalidate.d.ts +8 -1
  188. package/dist/revalidate.js +8 -1
  189. package/dist/richtext/index.d.ts +1 -1
  190. package/dist/section-wrapper.d.ts +1 -1
  191. package/dist/section-wrapper.js +0 -9
  192. package/dist/signed-message.d.ts +26 -0
  193. package/dist/signed-message.js +103 -0
  194. package/dist/site.d.ts +59 -18
  195. package/dist/site.js +21 -252
  196. package/dist/space-transfer.d.ts +5 -2
  197. package/dist/space-transfer.js +17 -5
  198. package/dist/store.d.ts +2 -80
  199. package/dist/store.js +3 -489
  200. package/dist/toolset.d.ts +2 -1
  201. package/dist/toolset.js +1 -1
  202. package/dist/version.d.ts +1 -1
  203. package/dist/version.js +1 -1
  204. package/package.json +4 -15
  205. package/dist/admin/ops-impl.d.ts +0 -51
  206. package/dist/admin/ops-impl.js +0 -1403
  207. package/dist/admin/shell/InviteModal.d.ts +0 -6
  208. package/dist/admin/shell/InviteModal.js +0 -28
  209. package/dist/admin/shell/LoginView.d.ts +0 -5
  210. package/dist/admin/shell/LoginView.js +0 -80
  211. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  212. package/dist/admin/shell/SchemaNotice.js +0 -4
  213. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  214. package/dist/admin/shell/SetPasswordView.js +0 -41
  215. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  216. package/dist/admin/shell/presence-realtime.js +0 -32
  217. package/dist/admin/supabase-auth.d.ts +0 -7
  218. package/dist/admin/supabase-auth.js +0 -30
  219. package/dist/astro/deploy-deps.d.ts +0 -10
  220. package/dist/astro/deploy-deps.js +0 -8
  221. package/dist/astro/image.d.ts +0 -27
  222. package/dist/astro/image.js +0 -29
  223. package/dist/bind.d.ts +0 -19
  224. package/dist/bind.js +0 -34
  225. package/dist/cli/add-admin.d.ts +0 -11
  226. package/dist/cli/add-admin.js +0 -25
  227. package/dist/cli/load-collections.d.ts +0 -2
  228. package/dist/cli/load-collections.js +0 -27
  229. package/dist/cli/migrate.d.ts +0 -43
  230. package/dist/cli/migrate.js +0 -138
  231. package/dist/cli/rebuild.d.ts +0 -23
  232. package/dist/cli/rebuild.js +0 -22
  233. package/dist/cli/space-io.d.ts +0 -41
  234. package/dist/cli/space-io.js +0 -84
  235. package/dist/client.d.ts +0 -11
  236. package/dist/client.js +0 -54
  237. package/dist/current-space.d.ts +0 -4
  238. package/dist/current-space.js +0 -9
  239. package/dist/document-stamp.d.ts +0 -7
  240. package/dist/document-stamp.js +0 -48
  241. package/dist/image/loaders.d.ts +0 -15
  242. package/dist/image/loaders.js +0 -5
  243. package/dist/image/next-config.d.ts +0 -18
  244. package/dist/image/next-config.js +0 -32
  245. package/dist/image/remote-pattern.d.ts +0 -1
  246. package/dist/image/remote-pattern.js +0 -5
  247. package/dist/migrations.d.ts +0 -37
  248. package/dist/migrations.js +0 -73
  249. package/dist/project.d.ts +0 -88
  250. package/dist/project.js +0 -231
  251. package/dist/published.d.ts +0 -142
  252. package/dist/published.js +0 -164
  253. package/dist/ref-index.d.ts +0 -19
  254. package/dist/ref-index.js +0 -0
  255. package/dist/schema-read.d.ts +0 -23
  256. package/dist/schema-read.js +0 -50
  257. package/dist/schema-version.d.ts +0 -19
  258. package/dist/schema-version.js +0 -34
  259. package/dist/sql-space.d.ts +0 -44
  260. package/dist/sql-space.js +0 -289
  261. package/dist/sql.d.ts +0 -15
  262. package/dist/sql.js +0 -556
  263. package/dist/supabase-asset-store.d.ts +0 -21
  264. package/dist/supabase-asset-store.js +0 -170
  265. package/dist/supabase-entry-store.d.ts +0 -92
  266. package/dist/supabase-entry-store.js +0 -824
  267. package/dist/supabase-member-store.d.ts +0 -21
  268. package/dist/supabase-member-store.js +0 -96
  269. package/dist/supabase-path-index.d.ts +0 -30
  270. package/dist/supabase-path-index.js +0 -138
  271. package/dist/supabase-published-store.d.ts +0 -56
  272. package/dist/supabase-published-store.js +0 -314
  273. package/dist/supabase-store.d.ts +0 -103
  274. package/dist/supabase-store.js +0 -678
  275. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  276. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  277. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  278. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  279. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  280. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  281. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  282. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  283. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  284. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  285. package/migrations/20261009120000_smoodly_v11.sql +0 -49
@@ -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,25 @@ 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 () => {
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 API client at build, from its config file (the one `options.config` names) and the env the setup hook loaded. */
83
+ const buildApi = async () => {
80
84
  const loaded = options.config ? await loadConfigFile(join(root, options.config), root) : await loadConfig(root);
81
- return { loaded, ...createSupabaseStores(createSmoodlyClient(resolveSmoodlyEnv(process.env)), loaded) };
85
+ return { loaded, api: createSmoodlyApi({ ...apiEnv(process.env), config: loaded }) };
86
+ };
87
+ /** A build read that fails names the service's address and stops the build before any page renders (a static build
88
+ * with the Worker down would otherwise fail page by page). */
89
+ const fromApi = async (read) => {
90
+ try {
91
+ return await read();
92
+ }
93
+ catch (e) {
94
+ const address = process.env.SMOODLY_API_URL || DEFAULT_API_URL;
95
+ 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.`);
96
+ }
82
97
  };
83
98
  return {
84
99
  name: "smoodly",
@@ -86,16 +101,14 @@ export default function smoodly(options = {}) {
86
101
  "astro:config:setup": async ({ config, command, updateConfig, injectRoute, addMiddleware, logger }) => {
87
102
  root = fileURLToPath(config.root);
88
103
  output = config.output === "static" ? "static" : "server";
104
+ i18n = config.i18n;
89
105
  // Root-relative with a leading slash, as Vite's import.meta.glob reads it; a source dir that is the root itself is "".
90
106
  const rel = relative(root, fileURLToPath(config.srcDir)).split("\\").join("/");
91
107
  const srcDir = rel === "" ? "" : "/" + rel;
92
108
  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.");
109
+ // No image service config: <Image> is plain markup over the cuts from Smoodly's media host (spec 2026-10-18 media
110
+ // on R2 and Images §7), so no remote pattern and no /_image.
97
111
  updateConfig({
98
- image: { remotePatterns: mediaUrl ? [mediaRemotePattern(mediaUrl)] : [] },
99
112
  vite: { plugins: [virtualModules(root, options.config ?? "smoodly.config.ts", srcDir, output)] },
100
113
  });
101
114
  for (const route of routes(adminPath, output))
@@ -122,10 +135,10 @@ export default function smoodly(options = {}) {
122
135
  // Static output (spec 2026-10-09 static mode §3): the redirect rows and the prefix-always root become the
123
136
  // host's redirects, answered without a function. Read at build only; `astro dev` serves them on demand.
124
137
  if (output === "static" && command === "build") {
125
- const { loaded, pages } = await buildStores();
126
- const routing = astroRouting(config.i18n, loaded.locales);
138
+ const { loaded, api } = await buildApi();
139
+ const routing = astroRouting(config.i18n, loaded.locales, loaded.routing);
127
140
  const href = localeHref(loaded.locales, routing);
128
- const redirects = { ...(await redirectEntries({ pages, config: loaded, href })), ...rootRedirect(routing, href, loaded.locales) };
141
+ const redirects = { ...(await fromApi(() => redirectEntries({ delivery: api, config: loaded, href }))), ...rootRedirect(routing, href, loaded.locales) };
129
142
  updateConfig({ redirects: hostRedirects(config.redirects, redirects) });
130
143
  logger.info(`smoodly: static output — ${Object.keys(redirects).length} redirects handed to the host.`);
131
144
  }
@@ -133,15 +146,33 @@ export default function smoodly(options = {}) {
133
146
  // The marker's watermark (spec 2026-10-09 static mode §5) is read BEFORE any page renders: a publish that lands
134
147
  // while the build runs is then newer than it and counts as waiting, which it is — the build may not hold it. A
135
148
  // 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 () => {
149
+ // The schema push (spec 2026-10-16 the control plane §4) comes first, in either output: the space then holds the
150
+ // registry this build renders with. A failed push never fails the build; the site runs against the schema the space has.
151
+ "astro:build:start": async ({ logger }) => {
152
+ const loaded = options.config ? await loadConfigFile(join(root, options.config), root) : await loadConfig(root);
153
+ // The runtime's startup check, at build in either output: a deploy that builds must not refuse every request.
154
+ astroRouting(i18n, loaded.locales, loaded.routing);
155
+ try {
156
+ const env = apiEnv(process.env);
157
+ const { version, changed } = await pushSchema(env, pushBody(loaded, process.env, output === "static" ? "static" : "live"));
158
+ logger.info(changed ? `smoodly: pushed schema version ${version} to ${env.spaceId}.` : `smoodly: schema unchanged (version ${version}).`);
159
+ }
160
+ catch (e) {
161
+ logger.warn(`smoodly: the schema push failed — ${e instanceof Error ? e.message : String(e)}; the site runs against the schema the space has.`);
162
+ }
137
163
  if (output !== "static")
138
164
  return;
139
- watermark = await (await buildStores()).published.newestPublishedAt();
165
+ // The media every prerendered page registers (static-media.ts), collected in this process for the build's end.
166
+ staticMedia = collectStaticMedia();
167
+ const { api } = await buildApi();
168
+ watermark = await fromApi(() => api.newestPublishedAt());
140
169
  },
141
170
  // The build marker: beside the pages, the only trace of Smoodly in a static build.
142
171
  "astro:build:done": async ({ dir, logger }) => {
143
172
  if (output !== "static")
144
173
  return;
174
+ // Every image the pages show, into the output beside them (spec 2026-10-18 media on R2 and Images §8).
175
+ logger.info(`smoodly: wrote ${await downloadStaticMedia(staticMedia.values(), fileURLToPath(dir))} media files into ${STATIC_MEDIA_DIR}.`);
145
176
  const marker = markerJson({ builtAt: Date.now(), publishedThrough: watermark });
146
177
  const file = join(fileURLToPath(dir), MARKER_PATH);
147
178
  await mkdir(dirname(file), { recursive: true });
@@ -162,10 +193,8 @@ export function createContent(config, options = {}) {
162
193
  loadSmoodlyEnv(options.root ?? process.cwd(), process.env.NODE_ENV === "production" ? "production" : "development");
163
194
  env = process.env;
164
195
  }
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 };
196
+ const href = localeHref(config.locales, astroRouting(options.i18n, config.locales, config.routing));
197
+ // Over Smoodly's API with the secret key (spec 2026-10-14 §1): the service runs the operations, so a publish from a
198
+ // script writes the delivery rows and expires the site's tags as the admin's does.
199
+ return { content: createSmoodlyContent({ config, href, transport: apiTransport(apiEnv(env)) }), href };
171
200
  }
@@ -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";
@@ -11,14 +11,15 @@ 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";
@@ -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";
@@ -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;
@@ -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>;
@@ -0,0 +1,55 @@
1
+ export const STATIC_MEDIA_DIR = "/_smoodly/media";
2
+ const hex = (bytes) => [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
3
+ /** `/_smoodly/media/<sha256(sourceUrl + format), first 16 hex>.<ext>`: the same address and format, the same file. */
4
+ export async function staticMediaPath(sourceUrl, format, ext) {
5
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${sourceUrl}\n${format}`));
6
+ return `${STATIC_MEDIA_DIR}/${hex(digest).slice(0, 16)}.${ext}`;
7
+ }
8
+ /** An original's extension from its address (`…/a1.jpg` → `jpg`), else `bin`. */
9
+ const extOf = (url) => /\.([a-z0-9]{1,8})$/i.exec(new URL(url).pathname)?.[1]?.toLowerCase() ?? "bin";
10
+ const candidates = (srcset) => srcset.split(", ").map((c) => { const [url, width] = c.trim().split(/\s+/); return { url: url, ...(width ? { width } : {}) }; });
11
+ /** The markup with every media address replaced by a file of the output, each file handed to `register`. */
12
+ export async function rewriteForStatic(m, register) {
13
+ const file = async (url, format) => {
14
+ const path = await staticMediaPath(url, format, format === "original" ? extOf(url) : format);
15
+ register({ url, format, path });
16
+ return path;
17
+ };
18
+ const inFormat = async (list, format) => (await Promise.all(list.map(async (c) => `${await file(c.url, format)}${c.width ? ` ${c.width}` : ""}`))).join(", ");
19
+ // A cut lists widths; an original is one address with no descriptor.
20
+ const cutOf = (srcset) => (srcset && candidates(srcset).every((c) => c.width) ? candidates(srcset) : null);
21
+ const sources = [];
22
+ const both = async (base, list) => {
23
+ sources.push({ ...base, type: "image/avif", srcset: await inFormat(list, "avif") });
24
+ sources.push({ ...base, type: "image/webp", srcset: await inFormat(list, "webp") });
25
+ };
26
+ for (const { srcset, type: _type, ...base } of m.sources) {
27
+ const list = cutOf(srcset);
28
+ if (list)
29
+ await both(base, list);
30
+ else
31
+ sources.push({ ...base, srcset: await inFormat(candidates(srcset), "original") });
32
+ }
33
+ const main = cutOf(m.img.srcset);
34
+ const { srcset: _srcset, sizes, ...img } = m.img;
35
+ if (main) {
36
+ await both({ ...(sizes ? { sizes } : {}), width: img.width, height: img.height }, main);
37
+ return { ...m, sources, img: { ...img, src: await file(m.img.src, "webp") } };
38
+ }
39
+ return { ...m, sources, img: { ...img, ...(sizes ? { sizes } : {}), src: await file(m.img.src, "original") } };
40
+ }
41
+ /** Where a prerendered page hands its files: installed by the integration for one static build. */
42
+ const COLLECTOR = Symbol.for("smoodly.staticMedia");
43
+ /** The build's collector (index.ts, astro:build:start): every file the pages register, once per path. */
44
+ export function collectStaticMedia() {
45
+ const entries = new Map();
46
+ globalThis[COLLECTOR] = (entry) => void entries.set(entry.path, entry);
47
+ return entries;
48
+ }
49
+ /** What Image.astro calls in a prerendered page of static output: the rewrite, each file handed to the build. */
50
+ export async function staticImage(m) {
51
+ const register = globalThis[COLLECTOR];
52
+ if (!register)
53
+ throw new Error("smoodly: a static page rendered outside the integration's `astro build` — no build collects its images.");
54
+ return rewriteForStatic(m, register);
55
+ }
@@ -1,15 +1,10 @@
1
1
  import type { ResolvedConfig } from "../config.ts";
2
- import type { PublishedStore } from "../published.ts";
3
- import type { PageStore } from "../store.ts";
2
+ import type { DeliveryReads } from "../site.ts";
4
3
  import { type LocaleRouting } from "./locale-paths.ts";
5
4
  type Href = (path: string, locale: string) => string;
6
- /** Every published page and entry address of every locale, as Astro's getStaticPaths entries. Pages come from
7
- * the delivery tree (a node with no path, hidden or under a code address, is not a page); entries from the paths
8
- * index's live rows, kept to the entries with a published delivery row — the Supabase delivery store lists entries
9
- * with their document only, no path (0.0.48 built no entry page for it), and the index is what visitors reach. */
5
+ /** Every published page and entry address of every locale, as Astro's getStaticPaths entries, each once. */
10
6
  export declare function staticPaths(deps: {
11
- published: PublishedStore;
12
- pages: PageStore;
7
+ delivery: DeliveryReads;
13
8
  config: ResolvedConfig;
14
9
  href: Href;
15
10
  }): Promise<{
@@ -18,9 +13,9 @@ export declare function staticPaths(deps: {
18
13
  };
19
14
  }[]>;
20
15
  /** The redirect rows as Astro's `redirects` config: old address → the target's current address, 308 (spec
21
- * 2026-09-26 redirects §7 at the host). A target with no live path in the locale is left out: a 404 as on demand. */
16
+ * 2026-09-26 redirects §7 at the host). The service leaves out a target with no live path in the locale. */
22
17
  export declare function redirectEntries(deps: {
23
- pages: PageStore;
18
+ delivery: DeliveryReads;
24
19
  config: ResolvedConfig;
25
20
  href: Href;
26
21
  }): Promise<Record<string, {