create-smoodly-app 0.0.13 → 0.0.15

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 (173) hide show
  1. package/README.md +38 -10
  2. package/dist/index.js +39 -15
  3. package/package.json +3 -2
  4. package/templates/blank/AGENTS.md +50 -0
  5. package/templates/blank/CLAUDE.md +1 -0
  6. package/templates/blank/README.md +75 -0
  7. package/templates/{next/app → blank/app/(site)}/[...path]/page.tsx +2 -2
  8. package/templates/blank/app/(site)/layout.tsx +25 -0
  9. package/templates/blank/app/globals.css +47 -0
  10. package/templates/blank/components/elements/Button.tsx +26 -0
  11. package/templates/blank/components/pages/Home.tsx +22 -0
  12. package/templates/blank/components/pages/Page.tsx +25 -0
  13. package/templates/blank/components/pages/Post.tsx +21 -0
  14. package/templates/blank/components/sections/Text.tsx +39 -0
  15. package/templates/{next → blank}/package.json +1 -1
  16. package/templates/blank/scripts/seed.ts +72 -0
  17. package/templates/blank/smoodly/collections.ts +26 -0
  18. package/templates/blank/smoodly/config.ts +35 -0
  19. package/templates/blank/smoodly/elements.ts +5 -0
  20. package/templates/blank/smoodly/sections.ts +5 -0
  21. package/templates/{next → blank}/smoodly/server.ts +17 -13
  22. package/templates/blank/smoodly/shared.ts +16 -0
  23. package/templates/blank/smoodly/toolsets.ts +14 -0
  24. package/templates/blank/supabase/migrations/00000000000000_smoodly.sql +518 -0
  25. package/templates/blank-intl/AGENTS.md +50 -0
  26. package/templates/blank-intl/CLAUDE.md +1 -0
  27. package/templates/blank-intl/README.md +76 -0
  28. package/templates/blank-intl/app/[locale]/layout.tsx +42 -0
  29. package/templates/blank-intl/app/globals.css +47 -0
  30. package/templates/blank-intl/components/elements/Button.tsx +26 -0
  31. package/templates/blank-intl/components/pages/Home.tsx +22 -0
  32. package/templates/blank-intl/components/pages/Page.tsx +25 -0
  33. package/templates/blank-intl/components/pages/Post.tsx +21 -0
  34. package/templates/blank-intl/components/sections/Text.tsx +39 -0
  35. package/templates/blank-intl/messages/en.json +1 -0
  36. package/templates/{next-intl → blank-intl}/package.json +1 -1
  37. package/templates/blank-intl/scripts/seed.ts +72 -0
  38. package/templates/blank-intl/smoodly/collections.ts +26 -0
  39. package/templates/blank-intl/smoodly/config.ts +38 -0
  40. package/templates/blank-intl/smoodly/elements.ts +5 -0
  41. package/templates/blank-intl/smoodly/sections.ts +5 -0
  42. package/templates/{next-intl → blank-intl}/smoodly/server.ts +17 -13
  43. package/templates/blank-intl/smoodly/shared.ts +16 -0
  44. package/templates/blank-intl/smoodly/toolsets.ts +14 -0
  45. package/templates/blank-intl/supabase/migrations/00000000000000_smoodly.sql +518 -0
  46. package/templates/blank-intl.tsconfig.json +28 -0
  47. package/templates/blank.tsconfig.json +28 -0
  48. package/templates/lorem/AGENTS.md +50 -0
  49. package/templates/lorem/CLAUDE.md +1 -0
  50. package/templates/{next → lorem}/README.md +5 -4
  51. package/templates/lorem/_gitignore +5 -0
  52. package/templates/lorem/app/(site)/[...path]/page.tsx +15 -0
  53. package/templates/{next/app → lorem/app/(site)}/layout.tsx +11 -11
  54. package/templates/lorem/app/(site)/page.tsx +11 -0
  55. package/templates/lorem/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
  56. package/templates/lorem/app/(smoodly)/admin/layout.tsx +1 -0
  57. package/templates/{next → lorem}/app/globals.css +4 -0
  58. package/templates/{next-intl → lorem}/components/elements/Callout.tsx +1 -1
  59. package/templates/{next → lorem}/components/elements/Cta.tsx +1 -1
  60. package/templates/{next → lorem}/components/pages/Article.tsx +3 -2
  61. package/templates/{next → lorem}/components/pages/Articles.tsx +7 -7
  62. package/templates/{next-intl → lorem}/components/pages/Page.tsx +11 -1
  63. package/templates/{next → lorem}/components/sections/ImageText.tsx +1 -1
  64. package/templates/{next → lorem}/components/sections/Points.tsx +1 -1
  65. package/templates/lorem/next.config.ts +8 -0
  66. package/templates/lorem/package.json +27 -0
  67. package/templates/{next-intl → lorem}/scripts/seed.ts +30 -5
  68. package/templates/lorem/smoodly/admin.tsx +6 -0
  69. package/templates/lorem/smoodly/ops.ts +11 -0
  70. package/templates/lorem/smoodly/richtext.tsx +7 -0
  71. package/templates/lorem/smoodly/server.ts +96 -0
  72. package/templates/lorem/tsconfig.json +41 -0
  73. package/templates/lorem-intl/AGENTS.md +50 -0
  74. package/templates/lorem-intl/CLAUDE.md +1 -0
  75. package/templates/{next-intl → lorem-intl}/README.md +2 -1
  76. package/templates/lorem-intl/_gitignore +5 -0
  77. package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
  78. package/templates/lorem-intl/app/(smoodly)/admin/layout.tsx +15 -0
  79. package/templates/lorem-intl/app/[locale]/[...path]/page.tsx +18 -0
  80. package/templates/{next-intl → lorem-intl}/app/[locale]/layout.tsx +5 -6
  81. package/templates/lorem-intl/app/[locale]/page.tsx +15 -0
  82. package/templates/{next-intl → lorem-intl}/app/globals.css +4 -0
  83. package/templates/{next → lorem-intl}/components/elements/Callout.tsx +1 -1
  84. package/templates/{next-intl → lorem-intl}/components/elements/Cta.tsx +1 -1
  85. package/templates/{next-intl → lorem-intl}/components/pages/Article.tsx +3 -2
  86. package/templates/{next-intl → lorem-intl}/components/pages/Articles.tsx +7 -7
  87. package/templates/{next → lorem-intl}/components/pages/Page.tsx +11 -1
  88. package/templates/{next-intl → lorem-intl}/components/sections/ImageText.tsx +1 -1
  89. package/templates/{next-intl → lorem-intl}/components/sections/Points.tsx +1 -1
  90. package/templates/lorem-intl/i18n/navigation.ts +6 -0
  91. package/templates/lorem-intl/i18n/request.ts +24 -0
  92. package/templates/lorem-intl/i18n/routing.ts +17 -0
  93. package/templates/lorem-intl/next.config.ts +11 -0
  94. package/templates/lorem-intl/package.json +28 -0
  95. package/templates/lorem-intl/proxy.ts +11 -0
  96. package/templates/{next → lorem-intl}/scripts/seed.ts +30 -5
  97. package/templates/lorem-intl/smoodly/admin.tsx +6 -0
  98. package/templates/lorem-intl/smoodly/ops.ts +11 -0
  99. package/templates/lorem-intl/smoodly/richtext.tsx +7 -0
  100. package/templates/lorem-intl/smoodly/server.ts +97 -0
  101. package/templates/lorem-intl/tsconfig.json +41 -0
  102. package/templates/lorem-intl.tsconfig.json +28 -0
  103. package/templates/lorem.tsconfig.json +28 -0
  104. /package/templates/{next → blank}/_gitignore +0 -0
  105. /package/templates/{next/app → blank/app/(site)}/page.tsx +0 -0
  106. /package/templates/{next → blank}/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -0
  107. /package/templates/{next → blank}/app/(smoodly)/admin/layout.tsx +0 -0
  108. /package/templates/{next → blank}/next.config.ts +0 -0
  109. /package/templates/{next → blank}/smoodly/admin.tsx +0 -0
  110. /package/templates/{next → blank}/smoodly/ops.ts +0 -0
  111. /package/templates/{next → blank}/smoodly/richtext.tsx +0 -0
  112. /package/templates/{next → blank}/tsconfig.json +0 -0
  113. /package/templates/{next-intl → blank-intl}/_gitignore +0 -0
  114. /package/templates/{next-intl → blank-intl}/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -0
  115. /package/templates/{next-intl → blank-intl}/app/(smoodly)/admin/layout.tsx +0 -0
  116. /package/templates/{next-intl → blank-intl}/app/[locale]/[...path]/page.tsx +0 -0
  117. /package/templates/{next-intl → blank-intl}/app/[locale]/page.tsx +0 -0
  118. /package/templates/{next-intl → blank-intl}/i18n/navigation.ts +0 -0
  119. /package/templates/{next-intl → blank-intl}/i18n/request.ts +0 -0
  120. /package/templates/{next-intl → blank-intl}/i18n/routing.ts +0 -0
  121. /package/templates/{next-intl → blank-intl}/next.config.ts +0 -0
  122. /package/templates/{next-intl → blank-intl}/proxy.ts +0 -0
  123. /package/templates/{next-intl → blank-intl}/smoodly/admin.tsx +0 -0
  124. /package/templates/{next-intl → blank-intl}/smoodly/ops.ts +0 -0
  125. /package/templates/{next-intl → blank-intl}/smoodly/richtext.tsx +0 -0
  126. /package/templates/{next-intl → blank-intl}/tsconfig.json +0 -0
  127. /package/templates/{next → lorem}/components/ArticleCard.tsx +0 -0
  128. /package/templates/{next → lorem}/components/articles.ts +0 -0
  129. /package/templates/{next → lorem}/components/pages/Home.tsx +0 -0
  130. /package/templates/{next → lorem}/components/sections/FavouriteArticles.tsx +0 -0
  131. /package/templates/{next → lorem}/components/sections/Footer.tsx +0 -0
  132. /package/templates/{next → lorem}/components/sections/Header.tsx +0 -0
  133. /package/templates/{next → lorem}/components/sections/Hero.tsx +0 -0
  134. /package/templates/{next → lorem}/components/sections/PageHero.tsx +0 -0
  135. /package/templates/{next → lorem}/components/sections/People.tsx +0 -0
  136. /package/templates/{next → lorem}/components/sections/Quote.tsx +0 -0
  137. /package/templates/{next → lorem}/components/strings.ts +0 -0
  138. /package/templates/{next → lorem}/messages/en.json +0 -0
  139. /package/templates/{next → lorem}/public/images/about_main.jpg +0 -0
  140. /package/templates/{next → lorem}/public/images/article_photo.jpg +0 -0
  141. /package/templates/{next → lorem}/public/images/hero_image.jpg +0 -0
  142. /package/templates/{next → lorem}/public/images/home_section_2.jpg +0 -0
  143. /package/templates/{next → lorem}/smoodly/collections.ts +0 -0
  144. /package/templates/{next → lorem}/smoodly/config.ts +0 -0
  145. /package/templates/{next → lorem}/smoodly/elements.ts +0 -0
  146. /package/templates/{next → lorem}/smoodly/sections.ts +0 -0
  147. /package/templates/{next → lorem}/smoodly/shared.ts +0 -0
  148. /package/templates/{next → lorem}/smoodly/toolsets.ts +0 -0
  149. /package/templates/{next → lorem}/supabase/migrations/00000000000000_smoodly.sql +0 -0
  150. /package/templates/{next-intl → lorem-intl}/components/ArticleCard.tsx +0 -0
  151. /package/templates/{next-intl → lorem-intl}/components/articles.ts +0 -0
  152. /package/templates/{next-intl → lorem-intl}/components/pages/Home.tsx +0 -0
  153. /package/templates/{next-intl → lorem-intl}/components/sections/FavouriteArticles.tsx +0 -0
  154. /package/templates/{next-intl → lorem-intl}/components/sections/Footer.tsx +0 -0
  155. /package/templates/{next-intl → lorem-intl}/components/sections/Header.tsx +0 -0
  156. /package/templates/{next-intl → lorem-intl}/components/sections/Hero.tsx +0 -0
  157. /package/templates/{next-intl → lorem-intl}/components/sections/PageHero.tsx +0 -0
  158. /package/templates/{next-intl → lorem-intl}/components/sections/People.tsx +0 -0
  159. /package/templates/{next-intl → lorem-intl}/components/sections/Quote.tsx +0 -0
  160. /package/templates/{next-intl → lorem-intl}/components/strings.ts +0 -0
  161. /package/templates/{next-intl → lorem-intl}/messages/en.json +0 -0
  162. /package/templates/{next-intl → lorem-intl}/messages/fi.json +0 -0
  163. /package/templates/{next-intl → lorem-intl}/public/images/about_main.jpg +0 -0
  164. /package/templates/{next-intl → lorem-intl}/public/images/article_photo.jpg +0 -0
  165. /package/templates/{next-intl → lorem-intl}/public/images/hero_image.jpg +0 -0
  166. /package/templates/{next-intl → lorem-intl}/public/images/home_section_2.jpg +0 -0
  167. /package/templates/{next-intl → lorem-intl}/smoodly/collections.ts +0 -0
  168. /package/templates/{next-intl → lorem-intl}/smoodly/config.ts +0 -0
  169. /package/templates/{next-intl → lorem-intl}/smoodly/elements.ts +0 -0
  170. /package/templates/{next-intl → lorem-intl}/smoodly/sections.ts +0 -0
  171. /package/templates/{next-intl → lorem-intl}/smoodly/shared.ts +0 -0
  172. /package/templates/{next-intl → lorem-intl}/smoodly/toolsets.ts +0 -0
  173. /package/templates/{next-intl → lorem-intl}/supabase/migrations/00000000000000_smoodly.sql +0 -0
@@ -0,0 +1,518 @@
1
+ set client_min_messages = warning;
2
+
3
+ -- The space a request acts in: the key's claim in Smoodly Cloud, the
4
+ -- default space in self-host (service role, anon key). Policies, the
5
+ -- insert trigger and the reorder RPC all read this ONE function.
6
+ create or replace function "smoodly_current_space"() returns uuid
7
+ language sql stable as $$
8
+ select coalesce((auth.jwt() ->> 'space_id')::uuid, '00000000-0000-0000-0000-000000000000');
9
+ $$;
10
+
11
+ create table if not exists "pages" (
12
+ "id" uuid primary key default gen_random_uuid(),
13
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
14
+ "parent_id" uuid references "pages" ("id") on delete restrict,
15
+ "template" text,
16
+ "sort" numeric,
17
+ "created_at" timestamptz not null default now(),
18
+ "updated_at" timestamptz not null default now()
19
+ );
20
+
21
+ -- The tree is the URL (spec 2026-09-05 §1): a node stores its parent and
22
+ -- its position; template null = folder. Everything a locale owns — the
23
+ -- node's own segment, title, publish state and version pointers — lives
24
+ -- in page_locales, one row per locale the page exists in (spec 2026-09-06
25
+ -- §2). There is no sibling-slug index: URL uniqueness per locale is the
26
+ -- primary key of "paths", which the stores write inside the same operation.
27
+ create index if not exists "pages_parent_idx"
28
+ on "pages" ("space_id", "parent_id", "sort");
29
+
30
+ create table if not exists "page_locales" (
31
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
32
+ "page_id" uuid not null references "pages" ("id") on delete cascade,
33
+ "locale" text not null,
34
+ "slug" text not null,
35
+ "title" text not null,
36
+ "status" text not null default 'draft',
37
+ "draft_version_id" uuid,
38
+ "published_version_id" uuid,
39
+ "updated_at" timestamptz not null default now(),
40
+ primary key ("page_id", "locale")
41
+ );
42
+
43
+ -- Sibling reorder for pages: same shape as smoodly_reorder for entries.
44
+ create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void language sql as $$
45
+ update "pages" pg set "sort" = u.ord
46
+ from unnest(p_ids) with ordinality as u(id, ord)
47
+ where pg."id" = u.id
48
+ and pg."space_id" = "smoodly_current_space"();
49
+ $$;
50
+
51
+ -- A version belongs to ONE locale of one page: each locale has its own
52
+ -- draft, its own publish and its own history.
53
+ create table if not exists "page_versions" (
54
+ "id" uuid primary key default gen_random_uuid(),
55
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
56
+ "page_id" uuid not null references "pages" ("id") on delete cascade,
57
+ "locale" text not null,
58
+ "tree" jsonb not null,
59
+ "created_by" text,
60
+ "created_at" timestamptz not null default now()
61
+ );
62
+
63
+ create index if not exists "page_versions_page_locale_idx"
64
+ on "page_versions" ("page_id", "locale", "created_at");
65
+
66
+ create table if not exists "entries" (
67
+ "id" uuid primary key default gen_random_uuid(),
68
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
69
+ "collection" text not null,
70
+ "parent_id" uuid references "entries" ("id") on delete restrict,
71
+ "fields" jsonb not null default '{}'::jsonb,
72
+ "sort" numeric,
73
+ "created_at" timestamptz not null default now(),
74
+ "updated_at" timestamptz not null default now()
75
+ );
76
+
77
+ -- The node holds identity, tree position and the SHARED fields — every
78
+ -- field without .localized() (spec 2026-09-06 §7): a Person's photo is
79
+ -- the same in every language. Everything a locale owns — its segment,
80
+ -- publish state, the .localized() fields and the version pointers —
81
+ -- lives in entry_locales, one row per locale the entry exists in. There
82
+ -- is no sibling-slug index any more: URL uniqueness per locale is the
83
+ -- primary key of "paths", which the stores write inside the same
84
+ -- operation, and both adapters pre-check siblings per locale in code.
85
+ create index if not exists "entries_collection_sort_idx"
86
+ on "entries" ("space_id", "collection", "sort");
87
+
88
+ create index if not exists "entries_parent_idx"
89
+ on "entries" ("space_id", "collection", "parent_id");
90
+
91
+ create table if not exists "entry_locales" (
92
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
93
+ "entry_id" uuid not null references "entries" ("id") on delete cascade,
94
+ "locale" text not null,
95
+ "slug" text,
96
+ "status" text not null default 'draft',
97
+ "fields" jsonb not null default '{}'::jsonb,
98
+ "draft_version_id" uuid,
99
+ "published_version_id" uuid,
100
+ "updated_at" timestamptz not null default now(),
101
+ primary key ("entry_id", "locale")
102
+ );
103
+
104
+ create index if not exists "entry_locales_status_idx"
105
+ on "entry_locales" ("space_id", "locale", "status");
106
+
107
+ -- A version belongs to ONE locale of one entry and stores the MERGED
108
+ -- fields (shared + localized + slug) as they were at that save, so
109
+ -- restoring an old Finnish version restores the shared fields too.
110
+ -- Rows exist only for collections registered with `versions: true`.
111
+ create table if not exists "entry_versions" (
112
+ "id" uuid primary key default gen_random_uuid(),
113
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
114
+ "entry_id" uuid not null references "entries" ("id") on delete cascade,
115
+ "locale" text not null,
116
+ "fields" jsonb not null,
117
+ "created_by" text,
118
+ "created_at" timestamptz not null default now()
119
+ );
120
+
121
+ create index if not exists "entry_versions_entry_locale_idx"
122
+ on "entry_versions" ("entry_id", "locale", "created_at");
123
+
124
+ -- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
125
+ -- statement. Deliberately does not touch updated_at — a drag must not
126
+ -- make the whole collection look freshly edited. Scoped to the current
127
+ -- space by the same function the policies use.
128
+ create or replace function "smoodly_reorder"(
129
+ p_collection text,
130
+ p_ids uuid[]
131
+ ) returns void language sql as $$
132
+ update "entries" e set "sort" = u.ord
133
+ from unnest(p_ids) with ordinality as u(id, ord)
134
+ where e."id" = u.id
135
+ and e."collection" = p_collection
136
+ and e."space_id" = "smoodly_current_space"();
137
+ $$;
138
+
139
+ create table if not exists "refs" (
140
+ "source_kind" text not null,
141
+ "source_id" uuid not null,
142
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
143
+ "source_field" text not null,
144
+ "target_collection" text not null,
145
+ "target_id" uuid not null,
146
+ primary key ("source_kind", "source_id", "source_field", "target_id")
147
+ );
148
+
149
+ -- The URL index (spec 2026-09-05 §4): the refs counterpart for paths.
150
+ -- Derived — the stores rewrite a target's rows on every create, rename,
151
+ -- move and delete — so one primary-key hit resolves any URL in any locale
152
+ -- and no page or entry can share a URL with another in any locale.
153
+ -- Rows exist for drafts too (the editor needs them); the read policy
154
+ -- admits a row only when its target is published.
155
+ create table if not exists "paths" (
156
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
157
+ "locale" text not null,
158
+ "path" text not null,
159
+ "kind" text not null check ("kind" in ('page', 'entry')),
160
+ "target_id" uuid not null,
161
+ primary key ("space_id", "locale", "path")
162
+ );
163
+
164
+ create index if not exists "paths_target_idx"
165
+ on "paths" ("space_id", "kind", "target_id");
166
+
167
+ -- Media (spec 2026-09-08): one row per upload, bytes in the "media"
168
+ -- bucket at "path". Immediate — never draft-gated, never versioned —
169
+ -- so there is no locale row and no version table. status 'pending' is
170
+ -- a row whose signed upload has not been confirmed; list never shows it.
171
+ create table if not exists "assets" (
172
+ "id" uuid primary key default gen_random_uuid(),
173
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
174
+ "kind" text not null check ("kind" in ('image', 'video', 'file')),
175
+ "status" text not null default 'pending' check ("status" in ('pending', 'ready')),
176
+ "path" text not null,
177
+ "mime" text not null,
178
+ "size" bigint not null,
179
+ "width" integer,
180
+ "height" integer,
181
+ "duration" numeric,
182
+ "poster_id" uuid references "assets" ("id") on delete set null,
183
+ "filename" text not null,
184
+ "title" text,
185
+ "alt" text,
186
+ "caption" text,
187
+ "credit" text,
188
+ "created_by" text,
189
+ "created_at" timestamptz not null default now(),
190
+ "updated_at" timestamptz not null default now()
191
+ );
192
+
193
+ create index if not exists "assets_space_created_idx"
194
+ on "assets" ("space_id", "status", "created_at" desc);
195
+
196
+ -- Replace the targets' rows with the given rows in ONE call: a SQL
197
+ -- function runs in the caller's transaction, so a primary-key collision
198
+ -- rolls the whole rewrite back. Inserted rows get their space from the
199
+ -- a_set_space trigger (the claim, or the default space).
200
+ create or replace function "smoodly_rewrite_paths"(p_targets jsonb, p_rows jsonb) returns void language sql as $$
201
+ delete from "paths" p
202
+ using jsonb_to_recordset(p_targets) as t(kind text, id uuid)
203
+ where p."kind" = t.kind and p."target_id" = t.id
204
+ and p."space_id" = "smoodly_current_space"();
205
+ insert into "paths" ("locale", "path", "kind", "target_id")
206
+ select r.locale, r.path, r.kind, r.id
207
+ from jsonb_to_recordset(p_rows) as r(locale text, path text, kind text, id uuid);
208
+ $$;
209
+
210
+ -- RLS on every core table. The service role bypasses it (self-host
211
+ -- adapters untouched). The policy set that follows (space layer) admits
212
+ -- a key to its own space: everything for a write key, published rows for
213
+ -- a read key or the anon key (default space). Drafts stay unreachable
214
+ -- to anything but a write key. The generated per-collection views
215
+ -- (collectionSQL) run with security_invoker = true, so they are subject
216
+ -- to this same smoodly_read policy rather than their owner's privileges.
217
+ alter table "pages" enable row level security;
218
+ alter table "page_locales" enable row level security;
219
+ alter table "page_versions" enable row level security;
220
+ alter table "entries" enable row level security;
221
+ alter table "entry_locales" enable row level security;
222
+ alter table "entry_versions" enable row level security;
223
+ alter table "refs" enable row level security;
224
+ alter table "paths" enable row level security;
225
+ alter table "assets" enable row level security;
226
+
227
+ -- ── Cloud state (spec 2026-09-04 §1): three ordinary tables, shipped
228
+ -- for everyone with ONE definition. Self-host fills them by hand or
229
+ -- leaves them empty; smoodly.io writes rows with the service role.
230
+ -- There is never cloud-only SQL on a content database.
231
+ create table if not exists "editors" (
232
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
233
+ "user_id" uuid not null,
234
+ "role" text not null default 'editor',
235
+ primary key ("space_id", "user_id")
236
+ );
237
+
238
+ create table if not exists "space_keys" (
239
+ "id" uuid primary key,
240
+ "space_id" uuid not null,
241
+ "kind" text not null check ("kind" in ('read', 'write')),
242
+ "revoked_at" timestamptz
243
+ );
244
+
245
+ create table if not exists "space_caps" (
246
+ "space_id" uuid primary key,
247
+ "row_cap" integer not null
248
+ );
249
+
250
+ alter table "editors" enable row level security;
251
+ alter table "space_keys" enable row level security;
252
+ alter table "space_caps" enable row level security;
253
+
254
+ -- ── Claim readers. The two lookups run as definer so they can read the
255
+ -- service-role-only tables from inside a policy; search_path is pinned.
256
+ create or replace function "smoodly_key_kind"() returns text
257
+ language sql stable as $$
258
+ select auth.jwt() ->> 'kind';
259
+ $$;
260
+
261
+ create or replace function "smoodly_space_key_revoked"() returns boolean
262
+ language sql stable security definer set search_path = public as $$
263
+ select coalesce(
264
+ (select k."revoked_at" is not null from "space_keys" k
265
+ where k."id" = (auth.jwt() ->> 'key_id')::uuid),
266
+ false);
267
+ $$;
268
+
269
+ create or replace function "smoodly_space_row_cap"() returns integer
270
+ language sql stable security definer set search_path = public as $$
271
+ select c."row_cap" from "space_caps" c where c."space_id" = "smoodly_current_space"();
272
+ $$;
273
+
274
+ -- ── Triggers. A key's claim wins over whatever the insert said, so an
275
+ -- adapter never has to know its space and cannot write into another.
276
+ -- No claim (service role, anon) leaves the given or default value —
277
+ -- that is how smoodly.io writes cloud-state rows for a chosen space.
278
+ create or replace function "smoodly_set_space"() returns trigger
279
+ language plpgsql as $$
280
+ begin
281
+ if auth.jwt() ->> 'space_id' is not null then
282
+ new."space_id" := (auth.jwt() ->> 'space_id')::uuid;
283
+ end if;
284
+ return new;
285
+ end;
286
+ $$;
287
+
288
+ -- Per table, per space. No space_caps row = no cap (self-host). Trigger
289
+ -- a_ (smoodly_set_space) runs first, so new.space_id already equals the
290
+ -- current space for a keyed insert; smoodly_space_row_cap() reads the
291
+ -- current space itself, so this never needs the row's space_id as an
292
+ -- argument (which would let one space's insert probe another's cap).
293
+ -- A service-role insert that names another space explicitly is a
294
+ -- different case: there is no JWT claim, so smoodly_current_space()
295
+ -- still resolves to the default space, and the cap read is the default
296
+ -- space's (none) — the service role is never capped by another space's row.
297
+ -- search_path is pinned empty, so every reference below is schema-qualified.
298
+ create or replace function "smoodly_enforce_row_cap"() returns trigger
299
+ language plpgsql set search_path = '' as $$
300
+ declare
301
+ cap integer;
302
+ n bigint;
303
+ begin
304
+ cap := public."smoodly_space_row_cap"();
305
+ if cap is null then
306
+ return new;
307
+ end if;
308
+ -- Advisory only (free-tier abuse guard), not a hard invariant: two
309
+ -- concurrent inserts can both observe n = cap - 1 and both pass.
310
+ execute format('select count(*) from %I.%I where "space_id" = $1', tg_table_schema, tg_table_name)
311
+ into n using new."space_id";
312
+ if n >= cap then
313
+ raise exception 'smoodly: row cap (%) reached for this space in %', cap, tg_table_name
314
+ using errcode = 'check_violation';
315
+ end if;
316
+ return new;
317
+ end;
318
+ $$;
319
+
320
+ create index if not exists "page_locales_space_idx" on "page_locales" ("space_id");
321
+
322
+ create index if not exists "page_versions_space_idx" on "page_versions" ("space_id");
323
+
324
+ create index if not exists "entry_locales_space_idx" on "entry_locales" ("space_id");
325
+
326
+ create index if not exists "entry_versions_space_idx" on "entry_versions" ("space_id");
327
+
328
+ create index if not exists "refs_space_idx" on "refs" ("space_id");
329
+
330
+ drop trigger if exists "smoodly_a_set_space" on "pages";
331
+ create trigger "smoodly_a_set_space" before insert on "pages"
332
+ for each row execute function "smoodly_set_space"();
333
+
334
+ drop trigger if exists "smoodly_a_set_space" on "page_locales";
335
+ create trigger "smoodly_a_set_space" before insert on "page_locales"
336
+ for each row execute function "smoodly_set_space"();
337
+
338
+ drop trigger if exists "smoodly_a_set_space" on "page_versions";
339
+ create trigger "smoodly_a_set_space" before insert on "page_versions"
340
+ for each row execute function "smoodly_set_space"();
341
+
342
+ drop trigger if exists "smoodly_a_set_space" on "entries";
343
+ create trigger "smoodly_a_set_space" before insert on "entries"
344
+ for each row execute function "smoodly_set_space"();
345
+
346
+ drop trigger if exists "smoodly_a_set_space" on "entry_locales";
347
+ create trigger "smoodly_a_set_space" before insert on "entry_locales"
348
+ for each row execute function "smoodly_set_space"();
349
+
350
+ drop trigger if exists "smoodly_a_set_space" on "entry_versions";
351
+ create trigger "smoodly_a_set_space" before insert on "entry_versions"
352
+ for each row execute function "smoodly_set_space"();
353
+
354
+ drop trigger if exists "smoodly_a_set_space" on "refs";
355
+ create trigger "smoodly_a_set_space" before insert on "refs"
356
+ for each row execute function "smoodly_set_space"();
357
+
358
+ drop trigger if exists "smoodly_a_set_space" on "paths";
359
+ create trigger "smoodly_a_set_space" before insert on "paths"
360
+ for each row execute function "smoodly_set_space"();
361
+
362
+ drop trigger if exists "smoodly_a_set_space" on "assets";
363
+ create trigger "smoodly_a_set_space" before insert on "assets"
364
+ for each row execute function "smoodly_set_space"();
365
+
366
+ drop trigger if exists "smoodly_a_set_space" on "editors";
367
+ create trigger "smoodly_a_set_space" before insert on "editors"
368
+ for each row execute function "smoodly_set_space"();
369
+
370
+ drop trigger if exists "smoodly_b_row_cap" on "pages";
371
+ create trigger "smoodly_b_row_cap" before insert on "pages"
372
+ for each row execute function "smoodly_enforce_row_cap"();
373
+
374
+ drop trigger if exists "smoodly_b_row_cap" on "page_locales";
375
+ create trigger "smoodly_b_row_cap" before insert on "page_locales"
376
+ for each row execute function "smoodly_enforce_row_cap"();
377
+
378
+ drop trigger if exists "smoodly_b_row_cap" on "page_versions";
379
+ create trigger "smoodly_b_row_cap" before insert on "page_versions"
380
+ for each row execute function "smoodly_enforce_row_cap"();
381
+
382
+ drop trigger if exists "smoodly_b_row_cap" on "entries";
383
+ create trigger "smoodly_b_row_cap" before insert on "entries"
384
+ for each row execute function "smoodly_enforce_row_cap"();
385
+
386
+ drop trigger if exists "smoodly_b_row_cap" on "entry_locales";
387
+ create trigger "smoodly_b_row_cap" before insert on "entry_locales"
388
+ for each row execute function "smoodly_enforce_row_cap"();
389
+
390
+ drop trigger if exists "smoodly_b_row_cap" on "entry_versions";
391
+ create trigger "smoodly_b_row_cap" before insert on "entry_versions"
392
+ for each row execute function "smoodly_enforce_row_cap"();
393
+
394
+ drop trigger if exists "smoodly_b_row_cap" on "refs";
395
+ create trigger "smoodly_b_row_cap" before insert on "refs"
396
+ for each row execute function "smoodly_enforce_row_cap"();
397
+
398
+ drop trigger if exists "smoodly_b_row_cap" on "paths";
399
+ create trigger "smoodly_b_row_cap" before insert on "paths"
400
+ for each row execute function "smoodly_enforce_row_cap"();
401
+
402
+ drop trigger if exists "smoodly_b_row_cap" on "assets";
403
+ create trigger "smoodly_b_row_cap" before insert on "assets"
404
+ for each row execute function "smoodly_enforce_row_cap"();
405
+
406
+ drop policy if exists "smoodly_write" on "pages";
407
+ create policy "smoodly_write" on "pages" for all
408
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
409
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
410
+
411
+ drop policy if exists "smoodly_read" on "pages";
412
+ create policy "smoodly_read" on "pages" for select
413
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "page_locales" l where l."page_id" = "pages"."id" and l."status" = 'published'));
414
+
415
+ drop policy if exists "smoodly_write" on "page_locales";
416
+ create policy "smoodly_write" on "page_locales" for all
417
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
418
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
419
+
420
+ drop policy if exists "smoodly_read" on "page_locales";
421
+ create policy "smoodly_read" on "page_locales" for select
422
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
423
+
424
+ drop policy if exists "smoodly_write" on "page_versions";
425
+ create policy "smoodly_write" on "page_versions" for all
426
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
427
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
428
+
429
+ drop policy if exists "smoodly_read" on "page_versions";
430
+ create policy "smoodly_read" on "page_versions" for select
431
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "page_locales" l where l."published_version_id" = "page_versions"."id"));
432
+
433
+ drop policy if exists "smoodly_write" on "entries";
434
+ create policy "smoodly_write" on "entries" for all
435
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
436
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
437
+
438
+ drop policy if exists "smoodly_read" on "entries";
439
+ create policy "smoodly_read" on "entries" for select
440
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "entry_locales" l where l."entry_id" = "entries"."id" and l."status" = 'published'));
441
+
442
+ drop policy if exists "smoodly_write" on "entry_locales";
443
+ create policy "smoodly_write" on "entry_locales" for all
444
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
445
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
446
+
447
+ drop policy if exists "smoodly_read" on "entry_locales";
448
+ create policy "smoodly_read" on "entry_locales" for select
449
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
450
+
451
+ drop policy if exists "smoodly_write" on "entry_versions";
452
+ create policy "smoodly_write" on "entry_versions" for all
453
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
454
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
455
+
456
+ drop policy if exists "smoodly_read" on "entry_versions";
457
+ create policy "smoodly_read" on "entry_versions" for select
458
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "entry_locales" l where l."published_version_id" = "entry_versions"."id"));
459
+
460
+ drop policy if exists "smoodly_write" on "refs";
461
+ create policy "smoodly_write" on "refs" for all
462
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
463
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
464
+
465
+ drop policy if exists "smoodly_write" on "paths";
466
+ create policy "smoodly_write" on "paths" for all
467
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
468
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
469
+
470
+ drop policy if exists "smoodly_read" on "paths";
471
+ create policy "smoodly_read" on "paths" for select
472
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (("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'))));
473
+
474
+ drop policy if exists "smoodly_write" on "assets";
475
+ create policy "smoodly_write" on "assets" for all
476
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
477
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
478
+
479
+ drop policy if exists "smoodly_read" on "assets";
480
+ create policy "smoodly_read" on "assets" for select
481
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'ready');
482
+
483
+ drop policy if exists "smoodly_write" on "editors";
484
+ create policy "smoodly_write" on "editors" for all
485
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
486
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
487
+
488
+ insert into storage.buckets ("id", "name", "public", "file_size_limit")
489
+ values ('media', 'media', true, 104857600)
490
+ on conflict ("id") do update set "public" = excluded."public", "file_size_limit" = excluded."file_size_limit";
491
+
492
+ drop policy if exists "smoodly_media_read" on storage.objects;
493
+ create policy "smoodly_media_read" on storage.objects for select
494
+ using ("bucket_id" = 'media');
495
+
496
+ drop policy if exists "smoodly_media_write" on storage.objects;
497
+ create policy "smoodly_media_write" on storage.objects for all
498
+ 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)
499
+ 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);
500
+
501
+ create or replace view "posts" with (security_invoker = true) as
502
+ select
503
+ e."id",
504
+ l."locale",
505
+ l."slug",
506
+ e."sort",
507
+ e."created_at",
508
+ l."updated_at",
509
+ v."fields"->>'title' as "title",
510
+ (v."fields"->>'date')::timestamptz as "date",
511
+ v."fields"->'body' as "body"
512
+ from "entries" e
513
+ join "entry_locales" l on l."entry_id" = e."id"
514
+ join "entry_versions" v on v."id" = l."published_version_id"
515
+ where e."collection" = 'posts' and l."status" = 'published';
516
+
517
+ create index if not exists "entries_posts_date_idx"
518
+ on "entries" (("fields"->'date')) where "collection" = 'posts';
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "./blank-intl/tsconfig.json",
3
+ "compilerOptions": {
4
+ "incremental": false,
5
+ "allowImportingTsExtensions": true,
6
+ "paths": {
7
+ "@/*": [
8
+ "./blank-intl/*"
9
+ ],
10
+ "smoodly": [
11
+ "../../smoodly/src/index.ts"
12
+ ],
13
+ "smoodly/bridge": [
14
+ "../../smoodly/src/admin/next/bridge.tsx"
15
+ ],
16
+ "smoodly/image/config": [
17
+ "../../smoodly/src/image/next-config.ts"
18
+ ],
19
+ "smoodly/*": [
20
+ "../../smoodly/src/*"
21
+ ]
22
+ }
23
+ },
24
+ "include": [
25
+ "./blank-intl/**/*.ts",
26
+ "./blank-intl/**/*.tsx"
27
+ ]
28
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "./blank/tsconfig.json",
3
+ "compilerOptions": {
4
+ "incremental": false,
5
+ "allowImportingTsExtensions": true,
6
+ "paths": {
7
+ "@/*": [
8
+ "./blank/*"
9
+ ],
10
+ "smoodly": [
11
+ "../../smoodly/src/index.ts"
12
+ ],
13
+ "smoodly/bridge": [
14
+ "../../smoodly/src/admin/next/bridge.tsx"
15
+ ],
16
+ "smoodly/image/config": [
17
+ "../../smoodly/src/image/next-config.ts"
18
+ ],
19
+ "smoodly/*": [
20
+ "../../smoodly/src/*"
21
+ ]
22
+ }
23
+ },
24
+ "include": [
25
+ "./blank/**/*.ts",
26
+ "./blank/**/*.tsx"
27
+ ]
28
+ }
@@ -0,0 +1,50 @@
1
+ # Working in this Smoodly site
2
+
3
+ This is a Next.js app with Smoodly installed: content lives in Supabase,
4
+ the admin is mounted at /admin from the `smoodly` package, and the site
5
+ reads content through `smoodly/server.ts`. Read README.md first: it says
6
+ where things are.
7
+
8
+ ## Rules
9
+
10
+ - **Register everything in `smoodly/`.** Every section, element,
11
+ collection and shared item is listed in its barrel (`sections.ts`,
12
+ `elements.ts`, `collections.ts`, `shared.ts`) and `smoodly/config.ts`
13
+ is the one registry. A component that is not registered does not exist
14
+ for the admin.
15
+ - **A new collection is a new migration.** Write its view with
16
+ `collectionSQL` from `smoodly` into a new file under
17
+ `supabase/migrations/`, then apply it: `supabase db push` (hosted) or
18
+ `supabase db reset` (local). The admin cannot list a collection whose
19
+ view is missing.
20
+ - **Seed through the stores.** `scripts/seed.ts` is starter content,
21
+ written through the page and entry stores, never raw SQL. It is
22
+ idempotent; run it with `npm run seed`.
23
+ - **Never import the admin into the site.** Nothing under `app/(site)`,
24
+ `app/[locale]` or `components/` may import `@/smoodly/admin` or
25
+ `smoodly/admin/next`; the admin's chunk must not reach visitors.
26
+ - **Two root layouts.** The site's layout lives in its route group or
27
+ locale segment and is never the admin's parent; `app/(smoodly)/admin`
28
+ is a second root layout. Do not add an `app/layout.tsx`.
29
+ - **Toolsets and elements.** A richtext field names a toolset
30
+ (`smoodly/toolsets.ts`); a toolset embeds elements by value; the config
31
+ must register those elements too. Files the toolset imports use
32
+ relative paths, not `@/`.
33
+ - **Section styles are data attributes.** `<smoodly.Section>` renders the
34
+ root element and emits the config's section styles as `data-*`
35
+ attributes; `app/globals.css` styles them. Marks
36
+ (`smoodly.marks(schema)`) tie rendered elements to fields so the
37
+ editor can select and edit them in place.
38
+ - **Multilingual sites.** Content locales are Smoodly's; URL locales are
39
+ next-intl's. The locale set is `i18n/routing.ts`. UI strings of the
40
+ site's own go through next-intl's messages in `messages/<locale>.json`.
41
+ - **Fixed versus open pages.** A page shape with a `slug` is fixed: one
42
+ record, created for you, never deleted. Without one it is open: editors
43
+ create as many as they like.
44
+
45
+ ## Commands
46
+
47
+ npm run dev # http://localhost:3000, /admin is the editor
48
+ npm run build # next build
49
+ npm run seed # starter content, idempotent
50
+ npm run typecheck # tsc --noEmit
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -1,15 +1,16 @@
1
1
  # Smoodly site
2
2
 
3
3
  Created by `create-smoodly-app`. Content lives in Supabase; the admin is
4
- mounted at `/admin` from the `smoodly` package.
4
+ mounted at `/admin` from the `smoodly` package. AGENTS.md holds the rules
5
+ for an agent (or a person) building the site.
5
6
 
6
7
  npm run dev # http://localhost:3000 — /admin is the editor
7
8
 
8
9
  ## Where things are
9
10
 
10
- - `app/page.tsx` — the home page: one line calling renderSmoodlyPath([]).
11
- - `app/[...path]/page.tsx` — every other page and every collection entry, at any depth.
12
- - `app/layout.tsx` — the site's root layout: fonts, `app/globals.css`, the header and footer from shared content.
11
+ - `app/(site)/page.tsx` — the home page: one line calling renderSmoodlyPath([]).
12
+ - `app/(site)/[...path]/page.tsx` — every other page and every collection entry, at any depth.
13
+ - `app/(site)/layout.tsx` — the site's root layout: fonts, `app/globals.css`, the header and footer from shared content; the admin has its own under `app/(smoodly)/admin/`.
13
14
  - `components/pages/` — the page shapes and the entry page: `Home` (locked hero, freeform body), `Articles` (a fixed page at the collection's path — its index — with a code-fetched grid), `Page` (open; About lives here), `Article` (the entry page).
14
15
  - `components/sections/` — the building blocks editors compose with, and the header, footer and quote behind the shared items. Add one, list it in `smoodly/sections.ts`.
15
16
  - `components/strings.ts`, `messages/` — the site's own UI strings and the request locale, one seam.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ .next/
3
+ .env.local
4
+ next-env.d.ts
5
+ *.tsbuildinfo