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.
- package/dist/index.js +21 -6
- package/package.json +1 -1
- package/templates/blank/app/(site)/layout.tsx +3 -1
- package/templates/blank/app/api/preview/route.ts +3 -0
- package/templates/blank/package.json +1 -1
- package/templates/blank/scripts/seed.ts +1 -1
- package/templates/blank/smoodly/server.ts +7 -3
- package/templates/blank/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
- package/templates/blank/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/templates/blank/supabase/migrations/{20260926150100_smoodly_views.sql → 20260929135623_smoodly_views.sql} +1 -1
- package/templates/blank-intl/app/[locale]/layout.tsx +3 -1
- package/templates/blank-intl/app/api/preview/route.ts +3 -0
- package/templates/blank-intl/package.json +1 -1
- package/templates/blank-intl/scripts/seed.ts +1 -1
- package/templates/blank-intl/smoodly/server.ts +7 -3
- package/templates/blank-intl/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
- package/templates/blank-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/templates/blank-intl/supabase/migrations/{20260926150100_smoodly_views.sql → 20260929135623_smoodly_views.sql} +1 -1
- package/templates/blank-intl.tsconfig.json +0 -3
- package/templates/blank.tsconfig.json +0 -3
- package/templates/lorem/README.md +3 -2
- package/templates/lorem/app/(site)/layout.tsx +3 -1
- package/templates/lorem/app/api/preview/route.ts +3 -0
- package/templates/lorem/app/globals.css +9 -0
- package/templates/lorem/components/news.ts +14 -0
- package/templates/lorem/components/pages/News.tsx +48 -0
- package/templates/lorem/components/pages/NewsPost.tsx +28 -0
- package/templates/lorem/messages/en.json +2 -1
- package/templates/lorem/package.json +1 -1
- package/templates/lorem/scripts/seed.ts +45 -8
- package/templates/lorem/smoodly/collections.ts +21 -1
- package/templates/lorem/smoodly/config.ts +3 -1
- package/templates/lorem/smoodly/server.ts +7 -3
- package/templates/lorem/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
- package/templates/lorem/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/templates/lorem/supabase/migrations/{20260926150100_smoodly_views.sql → 20260929135623_smoodly_views.sql} +22 -1
- package/templates/lorem-intl/README.md +3 -2
- package/templates/lorem-intl/app/[locale]/layout.tsx +3 -1
- package/templates/lorem-intl/app/api/preview/route.ts +3 -0
- package/templates/lorem-intl/app/globals.css +9 -0
- package/templates/lorem-intl/components/news.ts +14 -0
- package/templates/lorem-intl/components/pages/News.tsx +48 -0
- package/templates/lorem-intl/components/pages/NewsPost.tsx +28 -0
- package/templates/lorem-intl/messages/en.json +2 -1
- package/templates/lorem-intl/messages/fi.json +2 -1
- package/templates/lorem-intl/package.json +1 -1
- package/templates/lorem-intl/scripts/seed.ts +45 -8
- package/templates/lorem-intl/smoodly/collections.ts +21 -1
- package/templates/lorem-intl/smoodly/config.ts +3 -1
- package/templates/lorem-intl/smoodly/server.ts +7 -3
- package/templates/lorem-intl/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
- package/templates/lorem-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/templates/lorem-intl/supabase/migrations/{20260926150100_smoodly_views.sql → 20260929135623_smoodly_views.sql} +22 -1
- package/templates/lorem-intl.tsconfig.json +0 -3
- 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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
258
|
+
await writeFile(path, source.replace(line, line.replace(from, map)));
|
|
259
|
+
}
|
|
245
260
|
}
|
|
246
261
|
}
|
|
247
262
|
}
|
package/package.json
CHANGED
|
@@ -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 }) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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-
|
|
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 }> }) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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";
|