create-smoodly-app 0.0.34 → 0.0.36
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 +4 -0
- package/package.json +1 -1
- package/templates/blank/app/(site)/layout.tsx +4 -2
- package/templates/blank/app/api/preview/route.ts +3 -0
- package/templates/blank/components/pages/Home.tsx +1 -1
- package/templates/blank/components/pages/Page.tsx +1 -1
- package/templates/blank/components/pages/Post.tsx +5 -7
- package/templates/blank/package.json +1 -1
- package/templates/blank/smoodly/admin.tsx +2 -2
- package/templates/blank/smoodly/server.ts +3 -3
- package/templates/blank/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/templates/blank/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/templates/blank/supabase/migrations/{20260929120100_smoodly_views.sql → 20260930041056_smoodly_views.sql} +3 -6
- package/templates/blank-intl/app/[locale]/layout.tsx +4 -2
- package/templates/blank-intl/app/api/preview/route.ts +3 -0
- package/templates/blank-intl/components/pages/Home.tsx +1 -1
- package/templates/blank-intl/components/pages/Page.tsx +1 -1
- package/templates/blank-intl/components/pages/Post.tsx +5 -7
- package/templates/blank-intl/package.json +1 -1
- package/templates/blank-intl/smoodly/admin.tsx +2 -2
- package/templates/blank-intl/smoodly/server.ts +3 -3
- package/templates/blank-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/templates/blank-intl/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/templates/blank-intl/supabase/migrations/{20260929120100_smoodly_views.sql → 20260930041056_smoodly_views.sql} +3 -6
- package/templates/blank-intl.tsconfig.json +0 -3
- package/templates/blank.tsconfig.json +0 -3
- package/templates/lorem/app/(site)/layout.tsx +5 -3
- package/templates/lorem/app/api/preview/route.ts +3 -0
- package/templates/lorem/components/PageFrame.tsx +1 -1
- package/templates/lorem/components/articles.ts +0 -1
- package/templates/lorem/components/news.ts +0 -1
- package/templates/lorem/components/pages/Article.tsx +32 -14
- package/templates/lorem/components/pages/Articles.tsx +4 -6
- package/templates/lorem/components/pages/Home.tsx +1 -1
- package/templates/lorem/components/pages/News.tsx +8 -12
- package/templates/lorem/components/pages/NewsPost.tsx +6 -6
- package/templates/lorem/components/pages/Page.tsx +1 -1
- package/templates/lorem/components/sections/FavouriteArticles.tsx +2 -3
- package/templates/lorem/components/sections/People.tsx +1 -1
- package/templates/lorem/messages/en.json +1 -0
- package/templates/lorem/package.json +1 -1
- package/templates/lorem/smoodly/admin.tsx +2 -2
- package/templates/lorem/smoodly/collections.ts +2 -0
- package/templates/lorem/smoodly/server.ts +3 -3
- package/templates/lorem/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/templates/lorem/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/templates/lorem/supabase/migrations/{20260929120100_smoodly_views.sql → 20260930041056_smoodly_views.sql} +4 -10
- package/templates/lorem-intl/app/[locale]/layout.tsx +5 -3
- package/templates/lorem-intl/app/api/preview/route.ts +3 -0
- package/templates/lorem-intl/components/PageFrame.tsx +1 -1
- package/templates/lorem-intl/components/articles.ts +0 -1
- package/templates/lorem-intl/components/news.ts +0 -1
- package/templates/lorem-intl/components/pages/Article.tsx +32 -14
- package/templates/lorem-intl/components/pages/Articles.tsx +4 -6
- package/templates/lorem-intl/components/pages/Home.tsx +1 -1
- package/templates/lorem-intl/components/pages/News.tsx +8 -12
- package/templates/lorem-intl/components/pages/NewsPost.tsx +6 -6
- package/templates/lorem-intl/components/pages/Page.tsx +1 -1
- package/templates/lorem-intl/components/sections/FavouriteArticles.tsx +2 -3
- package/templates/lorem-intl/components/sections/People.tsx +1 -1
- package/templates/lorem-intl/messages/en.json +1 -0
- package/templates/lorem-intl/messages/fi.json +1 -0
- package/templates/lorem-intl/package.json +1 -1
- package/templates/lorem-intl/smoodly/admin.tsx +2 -2
- package/templates/lorem-intl/smoodly/collections.ts +2 -0
- package/templates/lorem-intl/smoodly/server.ts +3 -3
- package/templates/lorem-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
- package/templates/lorem-intl/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/templates/lorem-intl/supabase/migrations/{20260929120100_smoodly_views.sql → 20260930041056_smoodly_views.sql} +4 -10
- 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
|
}
|
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?.fields.name ?? "Smoodly site" };
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
@@ -18,7 +20,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|
|
18
20
|
<html lang="en">
|
|
19
21
|
<body>
|
|
20
22
|
<main>{children}</main>
|
|
21
|
-
{site && <footer className="container site-footer">{site.name} · {site.tagline}</footer>}
|
|
23
|
+
{site && <footer className="container site-footer">{site.fields.name} · {site.fields.tagline}</footer>}
|
|
22
24
|
</body>
|
|
23
25
|
</html>
|
|
24
26
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { smoodly
|
|
1
|
+
import { smoodly } from "smoodly";
|
|
2
2
|
import { RichText } from "@/smoodly/richtext";
|
|
3
3
|
import { posts } from "@/smoodly/collections";
|
|
4
4
|
import { LocaleNav } from "../LocaleNav";
|
|
@@ -6,10 +6,9 @@ import { LocaleNav } from "../LocaleNav";
|
|
|
6
6
|
// The entry page for `posts`, served at /posts/<slug>. It receives the
|
|
7
7
|
// entry's fields; the body is richtext rendered by the site's bound
|
|
8
8
|
// RichText, which knows the registered elements.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const post = entry.fields as PostFields;
|
|
9
|
+
export const PostPage = smoodly.entryPage(posts, function PostView({ entry }) {
|
|
10
|
+
const { locale, locales } = entry;
|
|
11
|
+
const post = entry.fields;
|
|
13
12
|
return (
|
|
14
13
|
<>
|
|
15
14
|
<LocaleNav locale={locale} locales={locales} />
|
|
@@ -20,6 +19,5 @@ function PostView({ entry, locale, locales }: EntryPageProps) {
|
|
|
20
19
|
</article>
|
|
21
20
|
</>
|
|
22
21
|
);
|
|
23
|
-
}
|
|
22
|
+
});
|
|
24
23
|
|
|
25
|
-
export const PostPage = smoodly.entryPage(posts, PostView);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSmoodlyAdmin } from "smoodly/admin/next";
|
|
2
2
|
import { config } from "./config";
|
|
3
3
|
import { smoodlyOp } from "./ops";
|
|
4
|
-
import { publicAuth,
|
|
4
|
+
import { publicAuth, presence } from "./server";
|
|
5
5
|
|
|
6
|
-
export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth,
|
|
6
|
+
export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth, presence });
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// admin route enters it from config.ts — so nothing here may read the
|
|
10
10
|
// config at module scope.
|
|
11
11
|
import {
|
|
12
|
-
createSmoodlyClient, createSmoodlySite,
|
|
12
|
+
createSmoodlyClient, createSmoodlySite, presenceConfig, smoodlyEnv, SupabaseAssetStore, SupabaseEntryStore, SupabaseMemberStore, SupabasePageStore, SupabasePathIndex, type PresenceConfig, type SmoodlySite,
|
|
13
13
|
} from "smoodly";
|
|
14
14
|
// The admin gate from `smoodly/admin`, which holds no client code: the site
|
|
15
15
|
// routes import this file, and `smoodly/admin/next` would put the admin
|
|
@@ -89,8 +89,8 @@ export function publicAuth(): AuthConfig {
|
|
|
89
89
|
|
|
90
90
|
/** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5).
|
|
91
91
|
* The space is asked of the database once per process. */
|
|
92
|
-
export function
|
|
93
|
-
return
|
|
92
|
+
export function presence(): Promise<PresenceConfig> {
|
|
93
|
+
return presenceConfig(smoodlyEnv());
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Locale-relative CMS path → site URL: the one seam for the renderer
|
|
@@ -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";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
-- Smoodly core schema, version 7 (docs/superpowers/specs/2026-09-29-content-api-contract-design.md §5).
|
|
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 6: smoodly_list_entries, which filters, orders and
|
|
5
|
+
-- limits a collection over the snapshot the reader is shown, and the read
|
|
6
|
+
-- policies of every content table admit a write key only (§8).
|
|
7
|
+
|
|
8
|
+
-- The shown snapshot of one collection's entries in a locale, filtered,
|
|
9
|
+
-- ordered and limited in one query (spec 2026-09-29 content API contract
|
|
10
|
+
-- §5): the published snapshot, or the live merge in preview. Invoker
|
|
11
|
+
-- rights, so RLS decides what a key sees; a preview read needs the
|
|
12
|
+
-- service role or a write key, anything else reads published.
|
|
13
|
+
create or replace function "smoodly_list_entries"(
|
|
14
|
+
p_collection text, p_locale text, p_draft boolean, p_where jsonb, p_order jsonb,
|
|
15
|
+
p_limit integer, p_parent uuid, p_has_parent boolean, p_ids uuid[]
|
|
16
|
+
) returns table ("id" uuid, "fields" jsonb) language sql stable set search_path = public as $$
|
|
17
|
+
with mode as (
|
|
18
|
+
select p_draft and (current_user = 'service_role' or "smoodly_key_kind"() = 'write') as draft
|
|
19
|
+
), shown as (
|
|
20
|
+
select e."id", e."sort", e."created_at", l."updated_at",
|
|
21
|
+
case when (select draft from mode) then e."fields" || l."fields" || jsonb_build_object('slug', l."slug")
|
|
22
|
+
else coalesce(v."fields", e."fields" || l."fields" || jsonb_build_object('slug', l."slug")) end as "fields"
|
|
23
|
+
from "entries" e
|
|
24
|
+
join "entry_locales" l on l."entry_id" = e."id" and l."locale" = p_locale
|
|
25
|
+
left join "entry_versions" v on v."id" = l."published_version_id"
|
|
26
|
+
where e."collection" = p_collection
|
|
27
|
+
and ((select draft from mode) or l."status" = 'published')
|
|
28
|
+
and (not p_has_parent or e."parent_id" is not distinct from p_parent)
|
|
29
|
+
and (p_ids is null or e."id" = any(p_ids))
|
|
30
|
+
)
|
|
31
|
+
select s."id", s."fields" from shown s
|
|
32
|
+
where not exists (
|
|
33
|
+
select 1 from jsonb_each(coalesce(p_where, '{}'::jsonb)) w
|
|
34
|
+
where not (case when w.value ? 'in'
|
|
35
|
+
then (s."fields" ->> w.key) = any (select jsonb_array_elements_text(w.value -> 'in'))
|
|
36
|
+
else s."fields" @> jsonb_build_object(w.key, w.value -> 'contains') end))
|
|
37
|
+
order by
|
|
38
|
+
case when p_order ->> 'by' = 'manual' then s."sort" end asc nulls last,
|
|
39
|
+
case when p_order ->> 'direction' = 'asc' then
|
|
40
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
41
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end asc nulls last,
|
|
42
|
+
case when p_order ->> 'direction' = 'desc' then
|
|
43
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
44
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end desc nulls last,
|
|
45
|
+
s."created_at" asc, s."id" asc
|
|
46
|
+
limit p_limit;
|
|
47
|
+
$$;
|
|
48
|
+
|
|
49
|
+
-- No public content reads (spec 2026-09-29 content API contract §8): the
|
|
50
|
+
-- publishable key and a read key read nothing; a write key reads its own space.
|
|
51
|
+
drop policy if exists "smoodly_read" on "pages";
|
|
52
|
+
create policy "smoodly_read" on "pages" for select
|
|
53
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
54
|
+
|
|
55
|
+
drop policy if exists "smoodly_read" on "page_locales";
|
|
56
|
+
create policy "smoodly_read" on "page_locales" for select
|
|
57
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
58
|
+
|
|
59
|
+
drop policy if exists "smoodly_read" on "page_versions";
|
|
60
|
+
create policy "smoodly_read" on "page_versions" for select
|
|
61
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
62
|
+
|
|
63
|
+
drop policy if exists "smoodly_read" on "entries";
|
|
64
|
+
create policy "smoodly_read" on "entries" for select
|
|
65
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
66
|
+
|
|
67
|
+
drop policy if exists "smoodly_read" on "entry_locales";
|
|
68
|
+
create policy "smoodly_read" on "entry_locales" for select
|
|
69
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
70
|
+
|
|
71
|
+
drop policy if exists "smoodly_read" on "entry_versions";
|
|
72
|
+
create policy "smoodly_read" on "entry_versions" for select
|
|
73
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
74
|
+
|
|
75
|
+
drop policy if exists "smoodly_read" on "paths";
|
|
76
|
+
create policy "smoodly_read" on "paths" for select
|
|
77
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
78
|
+
|
|
79
|
+
drop policy if exists "smoodly_read" on "assets";
|
|
80
|
+
create policy "smoodly_read" on "assets" for select
|
|
81
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
82
|
+
|
|
83
|
+
insert into "smoodly_meta" ("key", "value") values ('schema_version', '7')
|
|
84
|
+
on conflict ("key") do update set "value" = excluded."value";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
-- Smoodly: read views
|
|
2
|
-
-- (2026-09-
|
|
1
|
+
-- Smoodly: read views for this app's collections, written by `npx smoodly migrate`
|
|
2
|
+
-- (2026-09-30T04:10:56.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
|
-
-- smoodly:collection posts sha256:
|
|
5
|
+
-- smoodly:collection posts sha256:38ba2918f83ab367
|
|
6
6
|
create or replace view "posts" with (security_invoker = true) as
|
|
7
7
|
select
|
|
8
8
|
e."id",
|
|
@@ -18,6 +18,3 @@ from "entries" e
|
|
|
18
18
|
join "entry_locales" l on l."entry_id" = e."id"
|
|
19
19
|
left join "entry_versions" v on v."id" = l."published_version_id"
|
|
20
20
|
where e."collection" = 'posts' and l."status" = 'published';
|
|
21
|
-
|
|
22
|
-
create index if not exists "entries_posts_date_idx"
|
|
23
|
-
on "entries" (("fields"->'date')) where "collection" = 'posts';
|
|
@@ -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?.fields.name ?? "Smoodly site" };
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export default async function LocaleLayout({ children, params }: { children: ReactNode; params: Promise<{ locale: string }> }) {
|
|
@@ -29,7 +31,7 @@ export default async function LocaleLayout({ children, params }: { children: Rea
|
|
|
29
31
|
<body>
|
|
30
32
|
<NextIntlClientProvider>
|
|
31
33
|
<main>{children}</main>
|
|
32
|
-
{site && <footer className="container site-footer">{site.name} · {site.tagline}</footer>}
|
|
34
|
+
{site && <footer className="container site-footer">{site.fields.name} · {site.fields.tagline}</footer>}
|
|
33
35
|
</NextIntlClientProvider>
|
|
34
36
|
</body>
|
|
35
37
|
</html>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { smoodly
|
|
1
|
+
import { smoodly } from "smoodly";
|
|
2
2
|
import { RichText } from "@/smoodly/richtext";
|
|
3
3
|
import { posts } from "@/smoodly/collections";
|
|
4
4
|
import { LocaleNav } from "../LocaleNav";
|
|
@@ -6,10 +6,9 @@ import { LocaleNav } from "../LocaleNav";
|
|
|
6
6
|
// The entry page for `posts`, served at /posts/<slug>. It receives the
|
|
7
7
|
// entry's fields; the body is richtext rendered by the site's bound
|
|
8
8
|
// RichText, which knows the registered elements.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const post = entry.fields as PostFields;
|
|
9
|
+
export const PostPage = smoodly.entryPage(posts, function PostView({ entry }) {
|
|
10
|
+
const { locale, locales } = entry;
|
|
11
|
+
const post = entry.fields;
|
|
13
12
|
return (
|
|
14
13
|
<>
|
|
15
14
|
<LocaleNav locale={locale} locales={locales} />
|
|
@@ -20,6 +19,5 @@ function PostView({ entry, locale, locales }: EntryPageProps) {
|
|
|
20
19
|
</article>
|
|
21
20
|
</>
|
|
22
21
|
);
|
|
23
|
-
}
|
|
22
|
+
});
|
|
24
23
|
|
|
25
|
-
export const PostPage = smoodly.entryPage(posts, PostView);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSmoodlyAdmin } from "smoodly/admin/next";
|
|
2
2
|
import { config } from "./config";
|
|
3
3
|
import { smoodlyOp } from "./ops";
|
|
4
|
-
import { publicAuth,
|
|
4
|
+
import { publicAuth, presence } from "./server";
|
|
5
5
|
|
|
6
|
-
export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth,
|
|
6
|
+
export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth, presence });
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// admin route enters it from config.ts — so nothing here may read the
|
|
10
10
|
// config at module scope.
|
|
11
11
|
import {
|
|
12
|
-
createSmoodlyClient, createSmoodlySite,
|
|
12
|
+
createSmoodlyClient, createSmoodlySite, presenceConfig, smoodlyEnv, SupabaseAssetStore, SupabaseEntryStore, SupabaseMemberStore, SupabasePageStore, SupabasePathIndex, type PresenceConfig, type SmoodlySite,
|
|
13
13
|
} from "smoodly";
|
|
14
14
|
// The admin gate from `smoodly/admin`, which holds no client code: the site
|
|
15
15
|
// routes import this file, and `smoodly/admin/next` would put the admin
|
|
@@ -90,8 +90,8 @@ export function publicAuth(): AuthConfig {
|
|
|
90
90
|
|
|
91
91
|
/** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5).
|
|
92
92
|
* The space is asked of the database once per process. */
|
|
93
|
-
export function
|
|
94
|
-
return
|
|
93
|
+
export function presence(): Promise<PresenceConfig> {
|
|
94
|
+
return presenceConfig(smoodlyEnv());
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** Locale-relative CMS path → site URL: the one seam for the renderer
|