create-smoodly-app 0.0.33 → 0.0.35

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 (55) hide show
  1. package/dist/index.js +21 -6
  2. package/package.json +1 -1
  3. package/templates/blank/app/(site)/layout.tsx +3 -1
  4. package/templates/blank/app/api/preview/route.ts +3 -0
  5. package/templates/blank/package.json +1 -1
  6. package/templates/blank/scripts/seed.ts +1 -1
  7. package/templates/blank/smoodly/server.ts +7 -3
  8. package/templates/blank/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  9. package/templates/blank/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  10. package/templates/blank/supabase/migrations/{20260926150100_smoodly_views.sql → 20260929135623_smoodly_views.sql} +1 -1
  11. package/templates/blank-intl/app/[locale]/layout.tsx +3 -1
  12. package/templates/blank-intl/app/api/preview/route.ts +3 -0
  13. package/templates/blank-intl/package.json +1 -1
  14. package/templates/blank-intl/scripts/seed.ts +1 -1
  15. package/templates/blank-intl/smoodly/server.ts +7 -3
  16. package/templates/blank-intl/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  17. package/templates/blank-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  18. package/templates/blank-intl/supabase/migrations/{20260926150100_smoodly_views.sql → 20260929135623_smoodly_views.sql} +1 -1
  19. package/templates/blank-intl.tsconfig.json +0 -3
  20. package/templates/blank.tsconfig.json +0 -3
  21. package/templates/lorem/README.md +3 -2
  22. package/templates/lorem/app/(site)/layout.tsx +3 -1
  23. package/templates/lorem/app/api/preview/route.ts +3 -0
  24. package/templates/lorem/app/globals.css +9 -0
  25. package/templates/lorem/components/news.ts +14 -0
  26. package/templates/lorem/components/pages/News.tsx +48 -0
  27. package/templates/lorem/components/pages/NewsPost.tsx +28 -0
  28. package/templates/lorem/messages/en.json +2 -1
  29. package/templates/lorem/package.json +1 -1
  30. package/templates/lorem/scripts/seed.ts +45 -8
  31. package/templates/lorem/smoodly/collections.ts +21 -1
  32. package/templates/lorem/smoodly/config.ts +3 -1
  33. package/templates/lorem/smoodly/server.ts +7 -3
  34. package/templates/lorem/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  35. package/templates/lorem/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  36. package/templates/lorem/supabase/migrations/{20260926150100_smoodly_views.sql → 20260929135623_smoodly_views.sql} +22 -1
  37. package/templates/lorem-intl/README.md +3 -2
  38. package/templates/lorem-intl/app/[locale]/layout.tsx +3 -1
  39. package/templates/lorem-intl/app/api/preview/route.ts +3 -0
  40. package/templates/lorem-intl/app/globals.css +9 -0
  41. package/templates/lorem-intl/components/news.ts +14 -0
  42. package/templates/lorem-intl/components/pages/News.tsx +48 -0
  43. package/templates/lorem-intl/components/pages/NewsPost.tsx +28 -0
  44. package/templates/lorem-intl/messages/en.json +2 -1
  45. package/templates/lorem-intl/messages/fi.json +2 -1
  46. package/templates/lorem-intl/package.json +1 -1
  47. package/templates/lorem-intl/scripts/seed.ts +45 -8
  48. package/templates/lorem-intl/smoodly/collections.ts +21 -1
  49. package/templates/lorem-intl/smoodly/config.ts +3 -1
  50. package/templates/lorem-intl/smoodly/server.ts +7 -3
  51. package/templates/lorem-intl/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  52. package/templates/lorem-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  53. package/templates/lorem-intl/supabase/migrations/{20260926150100_smoodly_views.sql → 20260929135623_smoodly_views.sql} +22 -1
  54. package/templates/lorem-intl.tsconfig.json +0 -3
  55. package/templates/lorem.tsconfig.json +0 -3
package/dist/index.js CHANGED
@@ -163,6 +163,10 @@ function renderEnv(keys) {
163
163
  `SMOODLY_URL=${keys.url}`,
164
164
  `SMOODLY_SECRET_KEY=${keys.secretKey}`,
165
165
  `SMOODLY_PUBLISHABLE_KEY=${keys.publishableKey}`,
166
+ "",
167
+ "# The site's own address: absolute hreflang and social URLs. Set it to",
168
+ "# the production URL where you deploy.",
169
+ "SITE_URL=http://localhost:3000",
166
170
  ""
167
171
  ].join("\n");
168
172
  }
@@ -212,10 +216,16 @@ var ROUTING_LOCALES = ' locales: ["en", "fi"],';
212
216
  var ROUTING_DEFAULT = ' defaultLocale: "en",';
213
217
  var ARTICLES_PATH_LINE = ' path: { en: "posts", fi: "artikkelit" },';
214
218
  var ARTICLES_SLUG_LINE = ' slug: { en: "posts", fi: "artikkelit" },';
219
+ var NEWS_PATH_LINE = ' path: { en: "company/news", fi: "yritys/uutiset" },';
220
+ var NEWS_SLUG_LINE = ' slug: { en: "company/news", fi: "yritys/uutiset" },';
215
221
  function articlesSegments(locales) {
216
222
  const entries = locales.map((l) => `${/^[a-z]+$/.test(l) ? l : JSON.stringify(l)}: ${l === "fi" ? '"artikkelit"' : '"posts"'}`);
217
223
  return `{ ${entries.join(", ")} }`;
218
224
  }
225
+ function newsSegments(locales) {
226
+ const entries = locales.map((l) => `${/^[a-z]+$/.test(l) ? l : JSON.stringify(l)}: ${l === "fi" ? '"yritys/uutiset"' : '"company/news"'}`);
227
+ return `{ ${entries.join(", ")} }`;
228
+ }
219
229
  async function scaffold(opts) {
220
230
  await cp(opts.templateDir, opts.dir, { recursive: true });
221
231
  await rename(join(opts.dir, "_gitignore"), join(opts.dir, ".gitignore"));
@@ -235,13 +245,18 @@ async function scaffold(opts) {
235
245
  routing.replace(ROUTING_LOCALES, ` locales: [${opts.locales.map((l) => JSON.stringify(l)).join(", ")}],`).replace(ROUTING_DEFAULT, ` defaultLocale: ${JSON.stringify(opts.locales[0])},`)
236
246
  );
237
247
  if (opts.starter !== "blank") {
238
- const map = articlesSegments(opts.locales);
239
- for (const [rel, line] of [["smoodly/collections.ts", ARTICLES_PATH_LINE], ["components/pages/Articles.tsx", ARTICLES_SLUG_LINE]]) {
240
- const path = join(opts.dir, rel);
241
- const source = await readFile(path, "utf8");
242
- if (!source.includes(line)) throw new Error(`${rel} in the template has drifted; expected the line
248
+ const rewrites = [
249
+ { map: articlesSegments(opts.locales), from: '{ en: "posts", fi: "artikkelit" }', lines: [["smoodly/collections.ts", ARTICLES_PATH_LINE], ["components/pages/Articles.tsx", ARTICLES_SLUG_LINE]] },
250
+ { map: newsSegments(opts.locales), from: '{ en: "company/news", fi: "yritys/uutiset" }', lines: [["smoodly/collections.ts", NEWS_PATH_LINE], ["components/pages/News.tsx", NEWS_SLUG_LINE]] }
251
+ ];
252
+ for (const { map, from, lines } of rewrites) {
253
+ for (const [rel, line] of lines) {
254
+ const path = join(opts.dir, rel);
255
+ const source = await readFile(path, "utf8");
256
+ if (!source.includes(line)) throw new Error(`${rel} in the template has drifted; expected the line
243
257
  ${line}`);
244
- await writeFile(path, source.replace(line, line.replace('{ en: "posts", fi: "artikkelit" }', map)));
258
+ await writeFile(path, source.replace(line, line.replace(from, map)));
259
+ }
245
260
  }
246
261
  }
247
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smoodly-app",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "Create a Smoodly site: a fresh Next.js app with the admin mounted, a Supabase schema, an env file and a first editor.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -7,9 +7,11 @@ import "../globals.css";
7
7
  // a child of this one. The `site` global item (smoodly/globals.ts) gives
8
8
  // the document title and the footer line: read through `q`, cached under
9
9
  // its tag, null until published.
10
+ // metadataBase makes Smoodly's hreflang alternates and social image URLs
11
+ // absolute; SITE_URL is the deployed address (spec 2026-09-29 pre-install fixes §6).
10
12
  export async function generateMetadata() {
11
13
  const site = await q().getGlobal("site");
12
- return { title: site?.name ?? "Smoodly site" };
14
+ return { metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"), title: site?.name ?? "Smoodly site" };
13
15
  }
14
16
 
15
17
  export default async function RootLayout({ children }: { children: ReactNode }) {
@@ -0,0 +1,3 @@
1
+ // Draft mode on (with SMOODLY_PREVIEW_SECRET) or off (the preview pill's
2
+ // Exit). The logic lives in the package.
3
+ export { previewRoute as GET } from "smoodly/next";
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.33"
17
+ "smoodly": "0.0.35"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",
@@ -72,7 +72,7 @@ const tree = {
72
72
  },
73
73
  } as unknown as PageTree;
74
74
 
75
- const home = await pages.createPage({ locale: DEFAULT, slug: "home", title: "Home", template: "home" });
75
+ const home = await pages.createPage({ locale: DEFAULT, slug: "home", title: "Home", template: "home", fixed: "page:home" });
76
76
  await pages.saveDraft(home.id, DEFAULT, tree);
77
77
  await pages.publish(home.id, DEFAULT);
78
78
  for (const locale of OTHERS) {
@@ -32,7 +32,8 @@ function bound(): Bound {
32
32
  const env = smoodlyEnv();
33
33
  const db = createSmoodlyClient(env);
34
34
  // ONE path index for both stores: pages and entries share the URL space.
35
- const paths = new SupabasePathIndex(db);
35
+ // The index reads the code-owned addresses from the config (spec 2026-09-29 anchored paths §2).
36
+ const paths = new SupabasePathIndex(db, config.anchors);
36
37
  const locales = config.locales;
37
38
  const entries = new SupabaseEntryStore(db, collections, { paths, locales, globals, elements });
38
39
  const pages = new SupabasePageStore(db, {
@@ -40,6 +41,8 @@ function bound(): Bound {
40
41
  // The custom section's columns toolset, so a column's images count as usage (spec 2026-09-14 §6).
41
42
  custom: config.custom,
42
43
  homePageSlug: config.homePageSlug,
44
+ // Code-owned addresses by owner: a fixed page's or a collection's path (spec 2026-09-29 nested paths §3).
45
+ addresses: config.addresses,
43
46
  tree: config.pages?.tree,
44
47
  });
45
48
  // The media library: rows in `assets`, bytes in the public `media` bucket.
@@ -84,8 +87,9 @@ export function publicAuth(): AuthConfig {
84
87
  return { url: env.authUrl, key: env.authKey };
85
88
  }
86
89
 
87
- /** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5). */
88
- export function publicContent(): ContentConfig {
90
+ /** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5).
91
+ * The space is asked of the database once per process. */
92
+ export function publicContent(): Promise<ContentConfig> {
89
93
  return publicContentConfig(smoodlyEnv());
90
94
  }
91
95
 
@@ -0,0 +1,56 @@
1
+ -- Smoodly core schema, version 5 (docs/superpowers/specs/2026-09-29-nested-collection-paths-design.md,
2
+ -- docs/superpowers/specs/2026-09-29-anchored-paths-design.md).
3
+ -- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
4
+ -- Never edit a published file: the next change is the next file.
5
+ -- The delta over version 4: `pages.fixed` names the registration that owns a
6
+ -- code-owned record, unique per space; `paths.anchor` names the code address
7
+ -- a URL row hangs under, and the row keeps only the part editors made.
8
+ -- No backfill: a record without `fixed` is an editor's page, a row without
9
+ -- an anchor is the editors' tree.
10
+
11
+ set client_min_messages = warning;
12
+
13
+ alter table "pages" add column if not exists "fixed" text;
14
+
15
+ create unique index if not exists "pages_fixed_idx"
16
+ on "pages" ("space_id", "fixed") where "fixed" is not null;
17
+
18
+ alter table "paths" add column if not exists "anchor" text not null default '';
19
+ alter table "paths" drop constraint if exists "paths_pkey";
20
+ alter table "paths" add constraint "paths_pkey" primary key ("space_id", "locale", "anchor", "path");
21
+
22
+ create or replace function "smoodly_rewrite_paths"(p_targets jsonb, p_rows jsonb) returns void language sql set search_path = public as $$
23
+ delete from "paths" p
24
+ using jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid, keys jsonb)
25
+ where p."redirect" and p."locale" = r.locale
26
+ and exists (select 1 from jsonb_to_recordset(r.keys) as k(anchor text, path text) where k.anchor = p."anchor" and k.path = p."path")
27
+ and p."space_id" = "smoodly_current_space"();
28
+ update "paths" p set "redirect" = true
29
+ from jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
30
+ where not p."redirect" and p."kind" = t.kind and p."target_id" = t.id
31
+ and p."locale" = any (coalesce(t.redirect_locales, '{}'::text[]))
32
+ and not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid, keys jsonb)
33
+ cross join lateral jsonb_to_recordset(r.keys) as k(anchor text, path text)
34
+ where r.locale = p."locale" and k.anchor = p."anchor" and k.path = p."path")
35
+ and p."space_id" = "smoodly_current_space"();
36
+ delete from "paths" p
37
+ using jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
38
+ where p."kind" = t.kind and p."target_id" = t.id
39
+ and (not p."redirect" or not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid)
40
+ where r.kind = p."kind" and r.id = p."target_id" and r.locale = p."locale"))
41
+ and p."space_id" = "smoodly_current_space"();
42
+ insert into "paths" ("locale", "anchor", "path", "kind", "target_id")
43
+ select r.locale, r.anchor, r.path, r.kind, r.id
44
+ from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid);
45
+ $$;
46
+
47
+ revoke execute on function "smoodly_rewrite_paths"(jsonb, jsonb) from public, anon;
48
+
49
+ create or replace function "smoodly_resolve_path"(p_locale text, p_keys jsonb) returns setof "paths" language sql stable set search_path = public as $$
50
+ select p.* from "paths" p
51
+ join jsonb_to_recordset(p_keys) as k(anchor text, path text) on p."anchor" = k.anchor and p."path" = k.path
52
+ where p."locale" = p_locale;
53
+ $$;
54
+
55
+ insert into "smoodly_meta" ("key", "value") values ('schema_version', '5')
56
+ on conflict ("key") do update set "value" = excluded."value";
@@ -0,0 +1,200 @@
1
+ -- Smoodly core schema, version 6 (docs/superpowers/specs/2026-09-29-pre-install-fixes-design.md §3).
2
+ -- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
3
+ -- Never edit a published file: the next change is the next file.
4
+ -- The delta over version 5: one permissive policy per table and action (the
5
+ -- write key reads through smoodly_read; smoodly_write splits into insert,
6
+ -- update and delete where a public read exists; the media pair the same),
7
+ -- indexes on the foreign keys without one, and no anon call of the row cap.
8
+ -- Access is unchanged.
9
+
10
+ set client_min_messages = warning;
11
+
12
+ create index if not exists "page_locales_draft_version_idx" on "page_locales" ("draft_version_id");
13
+ create index if not exists "entry_locales_draft_version_idx" on "entry_locales" ("draft_version_id");
14
+ create index if not exists "page_locales_published_version_idx" on "page_locales" ("published_version_id");
15
+ create index if not exists "entry_locales_published_version_idx" on "entry_locales" ("published_version_id");
16
+ create index if not exists "pages_parent_id_idx" on "pages" ("parent_id");
17
+ create index if not exists "entries_parent_id_idx" on "entries" ("parent_id");
18
+ create index if not exists "assets_poster_idx" on "assets" ("poster_id");
19
+
20
+ drop policy if exists "smoodly_write" on "pages";
21
+
22
+ drop policy if exists "smoodly_insert" on "pages";
23
+ create policy "smoodly_insert" on "pages" for insert
24
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
25
+
26
+ drop policy if exists "smoodly_update" on "pages";
27
+ create policy "smoodly_update" on "pages" for update
28
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
29
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
30
+
31
+ drop policy if exists "smoodly_delete" on "pages";
32
+ create policy "smoodly_delete" on "pages" for delete
33
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
34
+
35
+ drop policy if exists "smoodly_read" on "pages";
36
+ create policy "smoodly_read" on "pages" for select
37
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and ((select "smoodly_key_kind"()) = 'write' or exists (select 1 from "page_locales" l where l."page_id" = "pages"."id" and l."status" = 'published')));
38
+
39
+ drop policy if exists "smoodly_write" on "page_locales";
40
+
41
+ drop policy if exists "smoodly_insert" on "page_locales";
42
+ create policy "smoodly_insert" on "page_locales" for insert
43
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
44
+
45
+ drop policy if exists "smoodly_update" on "page_locales";
46
+ create policy "smoodly_update" on "page_locales" for update
47
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
48
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
49
+
50
+ drop policy if exists "smoodly_delete" on "page_locales";
51
+ create policy "smoodly_delete" on "page_locales" for delete
52
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
53
+
54
+ drop policy if exists "smoodly_read" on "page_locales";
55
+ create policy "smoodly_read" on "page_locales" for select
56
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and ((select "smoodly_key_kind"()) = 'write' or "status" = 'published'));
57
+
58
+ drop policy if exists "smoodly_write" on "page_versions";
59
+
60
+ drop policy if exists "smoodly_insert" on "page_versions";
61
+ create policy "smoodly_insert" on "page_versions" for insert
62
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
63
+
64
+ drop policy if exists "smoodly_update" on "page_versions";
65
+ create policy "smoodly_update" on "page_versions" for update
66
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
67
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
68
+
69
+ drop policy if exists "smoodly_delete" on "page_versions";
70
+ create policy "smoodly_delete" on "page_versions" for delete
71
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
72
+
73
+ drop policy if exists "smoodly_read" on "page_versions";
74
+ create policy "smoodly_read" on "page_versions" for select
75
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and ((select "smoodly_key_kind"()) = 'write' or exists (select 1 from "page_locales" l where l."published_version_id" = "page_versions"."id")));
76
+
77
+ drop policy if exists "smoodly_write" on "entries";
78
+
79
+ drop policy if exists "smoodly_insert" on "entries";
80
+ create policy "smoodly_insert" on "entries" for insert
81
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
82
+
83
+ drop policy if exists "smoodly_update" on "entries";
84
+ create policy "smoodly_update" on "entries" for update
85
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
86
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
87
+
88
+ drop policy if exists "smoodly_delete" on "entries";
89
+ create policy "smoodly_delete" on "entries" for delete
90
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
91
+
92
+ drop policy if exists "smoodly_read" on "entries";
93
+ create policy "smoodly_read" on "entries" for select
94
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and ((select "smoodly_key_kind"()) = 'write' or exists (select 1 from "entry_locales" l where l."entry_id" = "entries"."id" and l."status" = 'published')));
95
+
96
+ drop policy if exists "smoodly_write" on "entry_locales";
97
+
98
+ drop policy if exists "smoodly_insert" on "entry_locales";
99
+ create policy "smoodly_insert" on "entry_locales" for insert
100
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
101
+
102
+ drop policy if exists "smoodly_update" on "entry_locales";
103
+ create policy "smoodly_update" on "entry_locales" for update
104
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
105
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
106
+
107
+ drop policy if exists "smoodly_delete" on "entry_locales";
108
+ create policy "smoodly_delete" on "entry_locales" for delete
109
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
110
+
111
+ drop policy if exists "smoodly_read" on "entry_locales";
112
+ create policy "smoodly_read" on "entry_locales" for select
113
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and ((select "smoodly_key_kind"()) = 'write' or "status" = 'published'));
114
+
115
+ drop policy if exists "smoodly_write" on "entry_versions";
116
+
117
+ drop policy if exists "smoodly_insert" on "entry_versions";
118
+ create policy "smoodly_insert" on "entry_versions" for insert
119
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
120
+
121
+ drop policy if exists "smoodly_update" on "entry_versions";
122
+ create policy "smoodly_update" on "entry_versions" for update
123
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
124
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
125
+
126
+ drop policy if exists "smoodly_delete" on "entry_versions";
127
+ create policy "smoodly_delete" on "entry_versions" for delete
128
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
129
+
130
+ drop policy if exists "smoodly_read" on "entry_versions";
131
+ create policy "smoodly_read" on "entry_versions" for select
132
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and ((select "smoodly_key_kind"()) = 'write' or exists (select 1 from "entry_locales" l where l."published_version_id" = "entry_versions"."id")));
133
+
134
+ drop policy if exists "smoodly_write" on "refs";
135
+ create policy "smoodly_write" on "refs" for all
136
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
137
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
138
+
139
+ drop policy if exists "smoodly_write" on "paths";
140
+
141
+ drop policy if exists "smoodly_insert" on "paths";
142
+ create policy "smoodly_insert" on "paths" for insert
143
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
144
+
145
+ drop policy if exists "smoodly_update" on "paths";
146
+ create policy "smoodly_update" on "paths" for update
147
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
148
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
149
+
150
+ drop policy if exists "smoodly_delete" on "paths";
151
+ create policy "smoodly_delete" on "paths" for delete
152
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
153
+
154
+ drop policy if exists "smoodly_read" on "paths";
155
+ create policy "smoodly_read" on "paths" for select
156
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and ((select "smoodly_key_kind"()) = 'write' or (("kind" = 'page' and exists (select 1 from "page_locales" l where l."page_id" = "paths"."target_id" and l."locale" = "paths"."locale" and l."status" = 'published')) or ("kind" = 'entry' and exists (select 1 from "entry_locales" l where l."entry_id" = "paths"."target_id" and l."locale" = "paths"."locale" and l."status" = 'published')))));
157
+
158
+ drop policy if exists "smoodly_write" on "assets";
159
+
160
+ drop policy if exists "smoodly_insert" on "assets";
161
+ create policy "smoodly_insert" on "assets" for insert
162
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
163
+
164
+ drop policy if exists "smoodly_update" on "assets";
165
+ create policy "smoodly_update" on "assets" for update
166
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
167
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
168
+
169
+ drop policy if exists "smoodly_delete" on "assets";
170
+ create policy "smoodly_delete" on "assets" for delete
171
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
172
+
173
+ drop policy if exists "smoodly_read" on "assets";
174
+ create policy "smoodly_read" on "assets" for select
175
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and ((select "smoodly_key_kind"()) = 'write' or "status" = 'ready'));
176
+
177
+ drop policy if exists "smoodly_write" on "members";
178
+ create policy "smoodly_write" on "members" for all
179
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
180
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
181
+
182
+ drop policy if exists "smoodly_media_write" on storage.objects;
183
+
184
+ drop policy if exists "smoodly_media_insert" on storage.objects;
185
+ create policy "smoodly_media_insert" on storage.objects for insert
186
+ with check ("bucket_id" = 'media' and (select public."smoodly_key_kind"()) = 'write' and not (select public."smoodly_space_key_revoked"()) and split_part("name", '/', 1) = (select public."smoodly_current_space"())::text);
187
+
188
+ drop policy if exists "smoodly_media_update" on storage.objects;
189
+ create policy "smoodly_media_update" on storage.objects for update
190
+ using ("bucket_id" = 'media' and (select public."smoodly_key_kind"()) = 'write' and not (select public."smoodly_space_key_revoked"()) and split_part("name", '/', 1) = (select public."smoodly_current_space"())::text)
191
+ with check ("bucket_id" = 'media' and (select public."smoodly_key_kind"()) = 'write' and not (select public."smoodly_space_key_revoked"()) and split_part("name", '/', 1) = (select public."smoodly_current_space"())::text);
192
+
193
+ drop policy if exists "smoodly_media_delete" on storage.objects;
194
+ create policy "smoodly_media_delete" on storage.objects for delete
195
+ using ("bucket_id" = 'media' and (select public."smoodly_key_kind"()) = 'write' and not (select public."smoodly_space_key_revoked"()) and split_part("name", '/', 1) = (select public."smoodly_current_space"())::text);
196
+
197
+ revoke execute on function "smoodly_space_row_cap"() from public, anon;
198
+
199
+ insert into "smoodly_meta" ("key", "value") values ('schema_version', '6')
200
+ on conflict ("key") do update set "value" = excluded."value";
@@ -1,5 +1,5 @@
1
1
  -- Smoodly: read views and order indexes for this app's collections, written by `npx smoodly migrate`
2
- -- (2026-09-26T15:01:00.000Z). One block per collection; the marker's hash is the block's SQL, compared on the next run.
2
+ -- (2026-09-29T13:56:23.000Z). One block per collection; the marker's hash is the block's SQL, compared on the next run.
3
3
  -- Never edit a written file: change the collection and run migrate again.
4
4
 
5
5
  -- smoodly:collection posts sha256:2e648b887b8e7146
@@ -14,10 +14,12 @@ import "../globals.css";
14
14
  // from its own `locales` map, landing on the same page in the other
15
15
  // language (spec 2026-09-15). They are the visitor's only language switch,
16
16
  // since locale detection is off (see i18n/routing.ts).
17
+ // metadataBase makes Smoodly's hreflang alternates and social image URLs
18
+ // absolute; SITE_URL is the deployed address (spec 2026-09-29 pre-install fixes §6).
17
19
  export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
18
20
  const { locale } = await params;
19
21
  const site = await q().getGlobal("site", { locale });
20
- return { title: site?.name ?? "Smoodly site" };
22
+ return { metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"), title: site?.name ?? "Smoodly site" };
21
23
  }
22
24
 
23
25
  export default async function LocaleLayout({ children, params }: { children: ReactNode; params: Promise<{ locale: string }> }) {
@@ -0,0 +1,3 @@
1
+ // Draft mode on (with SMOODLY_PREVIEW_SECRET) or off (the preview pill's
2
+ // Exit). The logic lives in the package.
3
+ export { previewRoute as GET } from "smoodly/next";
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.33",
17
+ "smoodly": "0.0.35",
18
18
  "next-intl": "^4.14.2"
19
19
  },
20
20
  "devDependencies": {
@@ -72,7 +72,7 @@ const tree = {
72
72
  },
73
73
  } as unknown as PageTree;
74
74
 
75
- const home = await pages.createPage({ locale: DEFAULT, slug: "home", title: "Home", template: "home" });
75
+ const home = await pages.createPage({ locale: DEFAULT, slug: "home", title: "Home", template: "home", fixed: "page:home" });
76
76
  await pages.saveDraft(home.id, DEFAULT, tree);
77
77
  await pages.publish(home.id, DEFAULT);
78
78
  for (const locale of OTHERS) {
@@ -33,7 +33,8 @@ function bound(): Bound {
33
33
  const env = smoodlyEnv();
34
34
  const db = createSmoodlyClient(env);
35
35
  // ONE path index for both stores: pages and entries share the URL space.
36
- const paths = new SupabasePathIndex(db);
36
+ // The index reads the code-owned addresses from the config (spec 2026-09-29 anchored paths §2).
37
+ const paths = new SupabasePathIndex(db, config.anchors);
37
38
  const locales = config.locales;
38
39
  const entries = new SupabaseEntryStore(db, collections, { paths, locales, globals, elements });
39
40
  const pages = new SupabasePageStore(db, {
@@ -41,6 +42,8 @@ function bound(): Bound {
41
42
  // The custom section's columns toolset, so a column's images count as usage (spec 2026-09-14 §6).
42
43
  custom: config.custom,
43
44
  homePageSlug: config.homePageSlug,
45
+ // Code-owned addresses by owner: a fixed page's or a collection's path (spec 2026-09-29 nested paths §3).
46
+ addresses: config.addresses,
44
47
  tree: config.pages?.tree,
45
48
  });
46
49
  // The media library: rows in `assets`, bytes in the public `media` bucket.
@@ -85,8 +88,9 @@ export function publicAuth(): AuthConfig {
85
88
  return { url: env.authUrl, key: env.authKey };
86
89
  }
87
90
 
88
- /** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5). */
89
- export function publicContent(): ContentConfig {
91
+ /** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5).
92
+ * The space is asked of the database once per process. */
93
+ export function publicContent(): Promise<ContentConfig> {
90
94
  return publicContentConfig(smoodlyEnv());
91
95
  }
92
96
 
@@ -0,0 +1,56 @@
1
+ -- Smoodly core schema, version 5 (docs/superpowers/specs/2026-09-29-nested-collection-paths-design.md,
2
+ -- docs/superpowers/specs/2026-09-29-anchored-paths-design.md).
3
+ -- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
4
+ -- Never edit a published file: the next change is the next file.
5
+ -- The delta over version 4: `pages.fixed` names the registration that owns a
6
+ -- code-owned record, unique per space; `paths.anchor` names the code address
7
+ -- a URL row hangs under, and the row keeps only the part editors made.
8
+ -- No backfill: a record without `fixed` is an editor's page, a row without
9
+ -- an anchor is the editors' tree.
10
+
11
+ set client_min_messages = warning;
12
+
13
+ alter table "pages" add column if not exists "fixed" text;
14
+
15
+ create unique index if not exists "pages_fixed_idx"
16
+ on "pages" ("space_id", "fixed") where "fixed" is not null;
17
+
18
+ alter table "paths" add column if not exists "anchor" text not null default '';
19
+ alter table "paths" drop constraint if exists "paths_pkey";
20
+ alter table "paths" add constraint "paths_pkey" primary key ("space_id", "locale", "anchor", "path");
21
+
22
+ create or replace function "smoodly_rewrite_paths"(p_targets jsonb, p_rows jsonb) returns void language sql set search_path = public as $$
23
+ delete from "paths" p
24
+ using jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid, keys jsonb)
25
+ where p."redirect" and p."locale" = r.locale
26
+ and exists (select 1 from jsonb_to_recordset(r.keys) as k(anchor text, path text) where k.anchor = p."anchor" and k.path = p."path")
27
+ and p."space_id" = "smoodly_current_space"();
28
+ update "paths" p set "redirect" = true
29
+ from jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
30
+ where not p."redirect" and p."kind" = t.kind and p."target_id" = t.id
31
+ and p."locale" = any (coalesce(t.redirect_locales, '{}'::text[]))
32
+ and not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid, keys jsonb)
33
+ cross join lateral jsonb_to_recordset(r.keys) as k(anchor text, path text)
34
+ where r.locale = p."locale" and k.anchor = p."anchor" and k.path = p."path")
35
+ and p."space_id" = "smoodly_current_space"();
36
+ delete from "paths" p
37
+ using jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
38
+ where p."kind" = t.kind and p."target_id" = t.id
39
+ and (not p."redirect" or not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid)
40
+ where r.kind = p."kind" and r.id = p."target_id" and r.locale = p."locale"))
41
+ and p."space_id" = "smoodly_current_space"();
42
+ insert into "paths" ("locale", "anchor", "path", "kind", "target_id")
43
+ select r.locale, r.anchor, r.path, r.kind, r.id
44
+ from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid);
45
+ $$;
46
+
47
+ revoke execute on function "smoodly_rewrite_paths"(jsonb, jsonb) from public, anon;
48
+
49
+ create or replace function "smoodly_resolve_path"(p_locale text, p_keys jsonb) returns setof "paths" language sql stable set search_path = public as $$
50
+ select p.* from "paths" p
51
+ join jsonb_to_recordset(p_keys) as k(anchor text, path text) on p."anchor" = k.anchor and p."path" = k.path
52
+ where p."locale" = p_locale;
53
+ $$;
54
+
55
+ insert into "smoodly_meta" ("key", "value") values ('schema_version', '5')
56
+ on conflict ("key") do update set "value" = excluded."value";