create-smoodly-app 0.0.45 → 0.0.46

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 (387) hide show
  1. package/README.md +15 -10
  2. package/dist/index.js +75 -33
  3. package/package.json +2 -2
  4. package/templates/astro-blank/AGENTS.md +55 -0
  5. package/templates/astro-blank/CLAUDE.md +1 -0
  6. package/templates/astro-blank/README.md +88 -0
  7. package/templates/astro-blank/_gitignore +5 -0
  8. package/templates/astro-blank/astro.config.ts +15 -0
  9. package/templates/astro-blank/package.json +24 -0
  10. package/templates/astro-blank/scripts/seed.ts +80 -0
  11. package/templates/astro-blank/src/components/LocaleNav.astro +15 -0
  12. package/templates/astro-blank/src/components/elements/Button.schema.ts +22 -0
  13. package/templates/astro-blank/src/components/pages/Home.schema.ts +17 -0
  14. package/templates/astro-blank/src/components/pages/Page.schema.ts +14 -0
  15. package/templates/astro-blank/src/components/sections/Text.schema.ts +24 -0
  16. package/templates/astro-blank/src/elements/button.astro +10 -0
  17. package/templates/astro-blank/src/entry-pages/posts.astro +23 -0
  18. package/templates/astro-blank/src/layouts/Layout.astro +25 -0
  19. package/templates/astro-blank/src/live.config.ts +13 -0
  20. package/templates/astro-blank/src/page-types/home.astro +15 -0
  21. package/templates/astro-blank/src/page-types/page.astro +15 -0
  22. package/templates/astro-blank/src/pages/404.astro +7 -0
  23. package/templates/astro-blank/src/sections/text.astro +18 -0
  24. package/templates/astro-blank/src/smoodly/collections.ts +24 -0
  25. package/templates/astro-blank/src/smoodly/config.ts +28 -0
  26. package/templates/astro-blank/src/smoodly/elements.ts +5 -0
  27. package/templates/astro-blank/src/smoodly/globals.ts +18 -0
  28. package/templates/astro-blank/src/smoodly/sections.ts +5 -0
  29. package/templates/astro-blank/src/smoodly/toolsets.ts +14 -0
  30. package/templates/astro-blank/src/styles/global.css +57 -0
  31. package/templates/astro-blank/supabase/migrations/20260926120000_smoodly_v1.sql +535 -0
  32. package/templates/astro-blank/supabase/migrations/20260926130000_smoodly_v2.sql +26 -0
  33. package/templates/astro-blank/supabase/migrations/20260926140000_smoodly_v3.sql +38 -0
  34. package/templates/astro-blank/supabase/migrations/20260926150000_smoodly_v4.sql +41 -0
  35. package/templates/astro-blank/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  36. package/templates/astro-blank/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  37. package/templates/astro-blank/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
  38. package/templates/astro-blank/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  39. package/templates/astro-blank/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  40. package/templates/{blank/supabase/migrations/20260930041056_smoodly_views.sql → astro-blank/supabase/migrations/20261007200100_smoodly_views.sql} +1 -1
  41. package/templates/astro-blank/tsconfig.json +17 -0
  42. package/templates/astro-blank-intl/AGENTS.md +55 -0
  43. package/templates/astro-blank-intl/CLAUDE.md +1 -0
  44. package/templates/astro-blank-intl/README.md +89 -0
  45. package/templates/astro-blank-intl/_gitignore +5 -0
  46. package/templates/astro-blank-intl/astro.config.ts +17 -0
  47. package/templates/astro-blank-intl/package.json +24 -0
  48. package/templates/astro-blank-intl/scripts/seed.ts +80 -0
  49. package/templates/astro-blank-intl/src/components/LocaleNav.astro +15 -0
  50. package/templates/astro-blank-intl/src/components/elements/Button.schema.ts +22 -0
  51. package/templates/astro-blank-intl/src/components/pages/Home.schema.ts +17 -0
  52. package/templates/astro-blank-intl/src/components/pages/Page.schema.ts +14 -0
  53. package/templates/astro-blank-intl/src/components/sections/Text.schema.ts +24 -0
  54. package/templates/astro-blank-intl/src/elements/button.astro +10 -0
  55. package/templates/astro-blank-intl/src/entry-pages/posts.astro +23 -0
  56. package/templates/astro-blank-intl/src/layouts/Layout.astro +25 -0
  57. package/templates/astro-blank-intl/src/live.config.ts +13 -0
  58. package/templates/astro-blank-intl/src/page-types/home.astro +15 -0
  59. package/templates/astro-blank-intl/src/page-types/page.astro +15 -0
  60. package/templates/astro-blank-intl/src/pages/404.astro +7 -0
  61. package/templates/astro-blank-intl/src/sections/text.astro +18 -0
  62. package/templates/astro-blank-intl/src/smoodly/collections.ts +24 -0
  63. package/templates/astro-blank-intl/src/smoodly/config.ts +29 -0
  64. package/templates/astro-blank-intl/src/smoodly/elements.ts +5 -0
  65. package/templates/astro-blank-intl/src/smoodly/globals.ts +18 -0
  66. package/templates/astro-blank-intl/src/smoodly/sections.ts +5 -0
  67. package/templates/astro-blank-intl/src/smoodly/toolsets.ts +14 -0
  68. package/templates/astro-blank-intl/src/styles/global.css +57 -0
  69. package/templates/astro-blank-intl/supabase/migrations/20260926120000_smoodly_v1.sql +535 -0
  70. package/templates/astro-blank-intl/supabase/migrations/20260926130000_smoodly_v2.sql +26 -0
  71. package/templates/astro-blank-intl/supabase/migrations/20260926140000_smoodly_v3.sql +38 -0
  72. package/templates/astro-blank-intl/supabase/migrations/20260926150000_smoodly_v4.sql +41 -0
  73. package/templates/astro-blank-intl/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  74. package/templates/astro-blank-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  75. package/templates/astro-blank-intl/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
  76. package/templates/astro-blank-intl/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  77. package/templates/astro-blank-intl/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  78. package/templates/{blank-intl/supabase/migrations/20260930041056_smoodly_views.sql → astro-blank-intl/supabase/migrations/20261007200100_smoodly_views.sql} +1 -1
  79. package/templates/astro-blank-intl/tsconfig.json +17 -0
  80. package/templates/astro-blank-intl.tsconfig.json +24 -0
  81. package/templates/astro-blank.tsconfig.json +24 -0
  82. package/templates/astro-lorem/AGENTS.md +55 -0
  83. package/templates/astro-lorem/CLAUDE.md +1 -0
  84. package/templates/astro-lorem/README.md +60 -0
  85. package/templates/astro-lorem/_gitignore +5 -0
  86. package/templates/astro-lorem/astro.config.ts +15 -0
  87. package/templates/astro-lorem/package.json +24 -0
  88. package/templates/astro-lorem/public/images/about_main.jpg +0 -0
  89. package/templates/astro-lorem/public/images/article_photo.jpg +0 -0
  90. package/templates/astro-lorem/public/images/hero_image.jpg +0 -0
  91. package/templates/astro-lorem/public/images/home_section_2.jpg +0 -0
  92. package/templates/astro-lorem/scripts/seed.ts +405 -0
  93. package/templates/astro-lorem/src/components/ArticleCard.astro +19 -0
  94. package/templates/astro-lorem/src/components/articles.ts +42 -0
  95. package/templates/astro-lorem/src/components/elements/Callout.schema.ts +24 -0
  96. package/templates/astro-lorem/src/components/elements/Cta.schema.ts +21 -0
  97. package/templates/astro-lorem/src/components/news.ts +13 -0
  98. package/templates/astro-lorem/src/components/pages/Articles.schema.ts +20 -0
  99. package/templates/astro-lorem/src/components/pages/Home.schema.ts +26 -0
  100. package/templates/astro-lorem/src/components/pages/News.schema.ts +17 -0
  101. package/templates/astro-lorem/src/components/pages/Page.schema.ts +24 -0
  102. package/templates/astro-lorem/src/components/sections/FavouriteArticles.schema.ts +17 -0
  103. package/templates/astro-lorem/src/components/sections/Footer.schema.ts +31 -0
  104. package/templates/astro-lorem/src/components/sections/Header.schema.ts +21 -0
  105. package/templates/astro-lorem/src/components/sections/Hero.schema.ts +24 -0
  106. package/templates/astro-lorem/src/components/sections/ImageText.schema.ts +30 -0
  107. package/templates/astro-lorem/src/components/sections/PageHero.schema.ts +21 -0
  108. package/templates/astro-lorem/src/components/sections/People.schema.ts +15 -0
  109. package/templates/astro-lorem/src/components/sections/Points.schema.ts +21 -0
  110. package/templates/astro-lorem/src/components/sections/Quote.schema.ts +23 -0
  111. package/templates/astro-lorem/src/elements/callout.astro +13 -0
  112. package/templates/astro-lorem/src/elements/cta.astro +15 -0
  113. package/templates/astro-lorem/src/entry-pages/articles.astro +71 -0
  114. package/templates/astro-lorem/src/entry-pages/news.astro +29 -0
  115. package/templates/astro-lorem/src/layouts/Layout.astro +39 -0
  116. package/templates/astro-lorem/src/live.config.ts +20 -0
  117. package/templates/astro-lorem/src/messages/en.json +7 -0
  118. package/templates/astro-lorem/src/messages/fi.json +7 -0
  119. package/templates/astro-lorem/src/page-types/articlesIndex.astro +24 -0
  120. package/templates/astro-lorem/src/page-types/home.astro +14 -0
  121. package/templates/astro-lorem/src/page-types/newsIndex.astro +28 -0
  122. package/templates/astro-lorem/src/page-types/page.astro +27 -0
  123. package/templates/astro-lorem/src/pages/404.astro +7 -0
  124. package/templates/astro-lorem/src/sections/favouriteArticles.astro +26 -0
  125. package/templates/astro-lorem/src/sections/footer.astro +37 -0
  126. package/templates/astro-lorem/src/sections/header.astro +27 -0
  127. package/templates/astro-lorem/src/sections/hero.astro +28 -0
  128. package/templates/astro-lorem/src/sections/imageText.astro +22 -0
  129. package/templates/astro-lorem/src/sections/pageHero.astro +20 -0
  130. package/templates/astro-lorem/src/sections/peopleSection.astro +24 -0
  131. package/templates/astro-lorem/src/sections/points.astro +33 -0
  132. package/templates/astro-lorem/src/sections/quote.astro +16 -0
  133. package/templates/astro-lorem/src/smoodly/collections.ts +63 -0
  134. package/templates/astro-lorem/src/smoodly/config.ts +48 -0
  135. package/templates/astro-lorem/src/smoodly/elements.ts +6 -0
  136. package/templates/astro-lorem/src/smoodly/globals.ts +29 -0
  137. package/templates/astro-lorem/src/smoodly/sections.ts +18 -0
  138. package/templates/astro-lorem/src/smoodly/toolsets.ts +16 -0
  139. package/templates/astro-lorem/src/strings.ts +17 -0
  140. package/templates/astro-lorem/src/styles/global.css +228 -0
  141. package/templates/astro-lorem/supabase/migrations/20260926120000_smoodly_v1.sql +535 -0
  142. package/templates/astro-lorem/supabase/migrations/20260926130000_smoodly_v2.sql +26 -0
  143. package/templates/astro-lorem/supabase/migrations/20260926140000_smoodly_v3.sql +38 -0
  144. package/templates/astro-lorem/supabase/migrations/20260926150000_smoodly_v4.sql +41 -0
  145. package/templates/astro-lorem/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  146. package/templates/astro-lorem/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  147. package/templates/astro-lorem/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
  148. package/templates/astro-lorem/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  149. package/templates/astro-lorem/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  150. package/templates/{lorem/supabase/migrations/20260930041056_smoodly_views.sql → astro-lorem/supabase/migrations/20261007200100_smoodly_views.sql} +1 -1
  151. package/templates/astro-lorem/tsconfig.json +17 -0
  152. package/templates/astro-lorem-intl/AGENTS.md +55 -0
  153. package/templates/astro-lorem-intl/CLAUDE.md +1 -0
  154. package/templates/astro-lorem-intl/README.md +62 -0
  155. package/templates/astro-lorem-intl/_gitignore +5 -0
  156. package/templates/astro-lorem-intl/astro.config.ts +17 -0
  157. package/templates/astro-lorem-intl/package.json +24 -0
  158. package/templates/astro-lorem-intl/public/images/about_main.jpg +0 -0
  159. package/templates/astro-lorem-intl/public/images/article_photo.jpg +0 -0
  160. package/templates/astro-lorem-intl/public/images/hero_image.jpg +0 -0
  161. package/templates/astro-lorem-intl/public/images/home_section_2.jpg +0 -0
  162. package/templates/astro-lorem-intl/scripts/seed.ts +405 -0
  163. package/templates/astro-lorem-intl/src/components/ArticleCard.astro +19 -0
  164. package/templates/astro-lorem-intl/src/components/articles.ts +42 -0
  165. package/templates/astro-lorem-intl/src/components/elements/Callout.schema.ts +24 -0
  166. package/templates/astro-lorem-intl/src/components/elements/Cta.schema.ts +21 -0
  167. package/templates/astro-lorem-intl/src/components/news.ts +13 -0
  168. package/templates/astro-lorem-intl/src/components/pages/Articles.schema.ts +20 -0
  169. package/templates/astro-lorem-intl/src/components/pages/Home.schema.ts +26 -0
  170. package/templates/astro-lorem-intl/src/components/pages/News.schema.ts +17 -0
  171. package/templates/astro-lorem-intl/src/components/pages/Page.schema.ts +24 -0
  172. package/templates/astro-lorem-intl/src/components/sections/FavouriteArticles.schema.ts +17 -0
  173. package/templates/astro-lorem-intl/src/components/sections/Footer.schema.ts +31 -0
  174. package/templates/astro-lorem-intl/src/components/sections/Header.schema.ts +21 -0
  175. package/templates/astro-lorem-intl/src/components/sections/Hero.schema.ts +24 -0
  176. package/templates/astro-lorem-intl/src/components/sections/ImageText.schema.ts +30 -0
  177. package/templates/astro-lorem-intl/src/components/sections/PageHero.schema.ts +21 -0
  178. package/templates/astro-lorem-intl/src/components/sections/People.schema.ts +15 -0
  179. package/templates/astro-lorem-intl/src/components/sections/Points.schema.ts +21 -0
  180. package/templates/astro-lorem-intl/src/components/sections/Quote.schema.ts +23 -0
  181. package/templates/astro-lorem-intl/src/elements/callout.astro +13 -0
  182. package/templates/astro-lorem-intl/src/elements/cta.astro +15 -0
  183. package/templates/astro-lorem-intl/src/entry-pages/articles.astro +71 -0
  184. package/templates/astro-lorem-intl/src/entry-pages/news.astro +29 -0
  185. package/templates/astro-lorem-intl/src/layouts/Layout.astro +39 -0
  186. package/templates/astro-lorem-intl/src/live.config.ts +20 -0
  187. package/templates/astro-lorem-intl/src/messages/en.json +7 -0
  188. package/templates/astro-lorem-intl/src/messages/fi.json +7 -0
  189. package/templates/astro-lorem-intl/src/page-types/articlesIndex.astro +24 -0
  190. package/templates/astro-lorem-intl/src/page-types/home.astro +14 -0
  191. package/templates/astro-lorem-intl/src/page-types/newsIndex.astro +28 -0
  192. package/templates/astro-lorem-intl/src/page-types/page.astro +27 -0
  193. package/templates/astro-lorem-intl/src/pages/404.astro +7 -0
  194. package/templates/astro-lorem-intl/src/sections/favouriteArticles.astro +26 -0
  195. package/templates/astro-lorem-intl/src/sections/footer.astro +37 -0
  196. package/templates/astro-lorem-intl/src/sections/header.astro +27 -0
  197. package/templates/astro-lorem-intl/src/sections/hero.astro +28 -0
  198. package/templates/astro-lorem-intl/src/sections/imageText.astro +22 -0
  199. package/templates/astro-lorem-intl/src/sections/pageHero.astro +20 -0
  200. package/templates/astro-lorem-intl/src/sections/peopleSection.astro +24 -0
  201. package/templates/astro-lorem-intl/src/sections/points.astro +33 -0
  202. package/templates/astro-lorem-intl/src/sections/quote.astro +16 -0
  203. package/templates/astro-lorem-intl/src/smoodly/collections.ts +63 -0
  204. package/templates/astro-lorem-intl/src/smoodly/config.ts +48 -0
  205. package/templates/astro-lorem-intl/src/smoodly/elements.ts +6 -0
  206. package/templates/astro-lorem-intl/src/smoodly/globals.ts +29 -0
  207. package/templates/astro-lorem-intl/src/smoodly/sections.ts +18 -0
  208. package/templates/astro-lorem-intl/src/smoodly/toolsets.ts +16 -0
  209. package/templates/astro-lorem-intl/src/strings.ts +17 -0
  210. package/templates/astro-lorem-intl/src/styles/global.css +228 -0
  211. package/templates/astro-lorem-intl/supabase/migrations/20260926120000_smoodly_v1.sql +535 -0
  212. package/templates/astro-lorem-intl/supabase/migrations/20260926130000_smoodly_v2.sql +26 -0
  213. package/templates/astro-lorem-intl/supabase/migrations/20260926140000_smoodly_v3.sql +38 -0
  214. package/templates/astro-lorem-intl/supabase/migrations/20260926150000_smoodly_v4.sql +41 -0
  215. package/templates/astro-lorem-intl/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  216. package/templates/astro-lorem-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  217. package/templates/astro-lorem-intl/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
  218. package/templates/astro-lorem-intl/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  219. package/templates/astro-lorem-intl/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  220. package/templates/{lorem-intl/supabase/migrations/20260930041056_smoodly_views.sql → astro-lorem-intl/supabase/migrations/20261007200100_smoodly_views.sql} +1 -1
  221. package/templates/astro-lorem-intl/tsconfig.json +17 -0
  222. package/templates/astro-lorem-intl.tsconfig.json +24 -0
  223. package/templates/astro-lorem.tsconfig.json +24 -0
  224. package/templates/blank/AGENTS.md +23 -16
  225. package/templates/blank/README.md +20 -15
  226. package/templates/blank/app/(smoodly)/admin/[[...segments]]/route.ts +8 -0
  227. package/templates/blank/components/elements/Button.schema.ts +22 -0
  228. package/templates/blank/components/elements/Button.tsx +2 -20
  229. package/templates/blank/components/pages/Home.schema.ts +17 -0
  230. package/templates/blank/components/pages/Home.tsx +2 -16
  231. package/templates/blank/components/pages/Page.schema.ts +14 -0
  232. package/templates/blank/components/pages/Page.tsx +2 -14
  233. package/templates/blank/components/pages/Post.tsx +5 -4
  234. package/templates/blank/components/sections/Text.schema.ts +24 -0
  235. package/templates/blank/components/sections/Text.tsx +7 -27
  236. package/templates/blank/package.json +1 -1
  237. package/templates/blank/scripts/seed.ts +2 -2
  238. package/templates/blank/smoodly/config.ts +8 -7
  239. package/templates/blank/smoodly/elements.ts +4 -4
  240. package/templates/blank/smoodly/globals.ts +4 -2
  241. package/templates/blank/smoodly/richtext.tsx +4 -5
  242. package/templates/blank/smoodly/sections.ts +4 -4
  243. package/templates/blank/smoodly/server.ts +22 -44
  244. package/templates/blank/smoodly/toolsets.ts +2 -2
  245. package/templates/blank/smoodly/views.ts +15 -0
  246. package/templates/blank/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  247. package/templates/blank/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  248. package/templates/blank/supabase/migrations/20261007200100_smoodly_views.sql +20 -0
  249. package/templates/blank-intl/AGENTS.md +23 -16
  250. package/templates/blank-intl/README.md +20 -15
  251. package/templates/blank-intl/app/(smoodly)/admin/[[...segments]]/route.ts +8 -0
  252. package/templates/blank-intl/components/elements/Button.schema.ts +22 -0
  253. package/templates/blank-intl/components/elements/Button.tsx +2 -20
  254. package/templates/blank-intl/components/pages/Home.schema.ts +17 -0
  255. package/templates/blank-intl/components/pages/Home.tsx +2 -16
  256. package/templates/blank-intl/components/pages/Page.schema.ts +14 -0
  257. package/templates/blank-intl/components/pages/Page.tsx +2 -14
  258. package/templates/blank-intl/components/pages/Post.tsx +5 -4
  259. package/templates/blank-intl/components/sections/Text.schema.ts +24 -0
  260. package/templates/blank-intl/components/sections/Text.tsx +7 -27
  261. package/templates/blank-intl/package.json +1 -1
  262. package/templates/blank-intl/scripts/seed.ts +2 -2
  263. package/templates/blank-intl/smoodly/config.ts +8 -7
  264. package/templates/blank-intl/smoodly/elements.ts +4 -4
  265. package/templates/blank-intl/smoodly/globals.ts +4 -2
  266. package/templates/blank-intl/smoodly/richtext.tsx +4 -5
  267. package/templates/blank-intl/smoodly/sections.ts +4 -4
  268. package/templates/blank-intl/smoodly/server.ts +22 -44
  269. package/templates/blank-intl/smoodly/toolsets.ts +2 -2
  270. package/templates/blank-intl/smoodly/views.ts +15 -0
  271. package/templates/blank-intl/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  272. package/templates/blank-intl/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  273. package/templates/blank-intl/supabase/migrations/20261007200100_smoodly_views.sql +20 -0
  274. package/templates/lorem/AGENTS.md +23 -16
  275. package/templates/lorem/README.md +3 -3
  276. package/templates/lorem/app/(smoodly)/admin/[[...segments]]/route.ts +8 -0
  277. package/templates/lorem/components/elements/Callout.schema.ts +24 -0
  278. package/templates/lorem/components/elements/Callout.tsx +2 -24
  279. package/templates/lorem/components/elements/Cta.schema.ts +21 -0
  280. package/templates/lorem/components/elements/Cta.tsx +2 -21
  281. package/templates/lorem/components/pages/Article.tsx +1 -1
  282. package/templates/lorem/components/pages/Articles.schema.ts +20 -0
  283. package/templates/lorem/components/pages/Articles.tsx +2 -20
  284. package/templates/lorem/components/pages/Home.schema.ts +26 -0
  285. package/templates/lorem/components/pages/Home.tsx +2 -26
  286. package/templates/lorem/components/pages/News.schema.ts +17 -0
  287. package/templates/lorem/components/pages/News.tsx +2 -17
  288. package/templates/lorem/components/pages/NewsPost.tsx +1 -1
  289. package/templates/lorem/components/pages/Page.schema.ts +24 -0
  290. package/templates/lorem/components/pages/Page.tsx +2 -24
  291. package/templates/lorem/components/sections/FavouriteArticles.schema.ts +17 -0
  292. package/templates/lorem/components/sections/FavouriteArticles.tsx +2 -17
  293. package/templates/lorem/components/sections/Footer.schema.ts +31 -0
  294. package/templates/lorem/components/sections/Footer.tsx +4 -32
  295. package/templates/lorem/components/sections/Header.schema.ts +21 -0
  296. package/templates/lorem/components/sections/Header.tsx +2 -21
  297. package/templates/lorem/components/sections/Hero.schema.ts +24 -0
  298. package/templates/lorem/components/sections/Hero.tsx +2 -24
  299. package/templates/lorem/components/sections/ImageText.schema.ts +30 -0
  300. package/templates/lorem/components/sections/ImageText.tsx +2 -30
  301. package/templates/lorem/components/sections/PageHero.schema.ts +21 -0
  302. package/templates/lorem/components/sections/PageHero.tsx +2 -21
  303. package/templates/lorem/components/sections/People.schema.ts +15 -0
  304. package/templates/lorem/components/sections/People.tsx +2 -14
  305. package/templates/lorem/components/sections/Points.schema.ts +21 -0
  306. package/templates/lorem/components/sections/Points.tsx +2 -21
  307. package/templates/lorem/components/sections/Quote.schema.ts +23 -0
  308. package/templates/lorem/components/sections/Quote.tsx +2 -23
  309. package/templates/lorem/package.json +1 -1
  310. package/templates/lorem/scripts/seed.ts +18 -18
  311. package/templates/lorem/smoodly/collections.ts +2 -2
  312. package/templates/lorem/smoodly/config.ts +14 -15
  313. package/templates/lorem/smoodly/elements.ts +5 -5
  314. package/templates/lorem/smoodly/globals.ts +10 -7
  315. package/templates/lorem/smoodly/richtext.tsx +5 -5
  316. package/templates/lorem/smoodly/sections.ts +12 -13
  317. package/templates/lorem/smoodly/server.ts +22 -44
  318. package/templates/lorem/smoodly/toolsets.ts +3 -3
  319. package/templates/lorem/smoodly/views.ts +33 -0
  320. package/templates/lorem/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  321. package/templates/lorem/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  322. package/templates/lorem/supabase/migrations/20261007200100_smoodly_views.sql +58 -0
  323. package/templates/lorem-intl/AGENTS.md +23 -16
  324. package/templates/lorem-intl/README.md +3 -3
  325. package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/route.ts +8 -0
  326. package/templates/lorem-intl/components/elements/Callout.schema.ts +24 -0
  327. package/templates/lorem-intl/components/elements/Callout.tsx +2 -24
  328. package/templates/lorem-intl/components/elements/Cta.schema.ts +21 -0
  329. package/templates/lorem-intl/components/elements/Cta.tsx +2 -21
  330. package/templates/lorem-intl/components/pages/Article.tsx +1 -1
  331. package/templates/lorem-intl/components/pages/Articles.schema.ts +20 -0
  332. package/templates/lorem-intl/components/pages/Articles.tsx +2 -20
  333. package/templates/lorem-intl/components/pages/Home.schema.ts +26 -0
  334. package/templates/lorem-intl/components/pages/Home.tsx +2 -26
  335. package/templates/lorem-intl/components/pages/News.schema.ts +17 -0
  336. package/templates/lorem-intl/components/pages/News.tsx +2 -17
  337. package/templates/lorem-intl/components/pages/NewsPost.tsx +1 -1
  338. package/templates/lorem-intl/components/pages/Page.schema.ts +24 -0
  339. package/templates/lorem-intl/components/pages/Page.tsx +2 -24
  340. package/templates/lorem-intl/components/sections/FavouriteArticles.schema.ts +17 -0
  341. package/templates/lorem-intl/components/sections/FavouriteArticles.tsx +2 -17
  342. package/templates/lorem-intl/components/sections/Footer.schema.ts +31 -0
  343. package/templates/lorem-intl/components/sections/Footer.tsx +4 -32
  344. package/templates/lorem-intl/components/sections/Header.schema.ts +21 -0
  345. package/templates/lorem-intl/components/sections/Header.tsx +2 -21
  346. package/templates/lorem-intl/components/sections/Hero.schema.ts +24 -0
  347. package/templates/lorem-intl/components/sections/Hero.tsx +2 -24
  348. package/templates/lorem-intl/components/sections/ImageText.schema.ts +30 -0
  349. package/templates/lorem-intl/components/sections/ImageText.tsx +2 -30
  350. package/templates/lorem-intl/components/sections/PageHero.schema.ts +21 -0
  351. package/templates/lorem-intl/components/sections/PageHero.tsx +2 -21
  352. package/templates/lorem-intl/components/sections/People.schema.ts +15 -0
  353. package/templates/lorem-intl/components/sections/People.tsx +2 -14
  354. package/templates/lorem-intl/components/sections/Points.schema.ts +21 -0
  355. package/templates/lorem-intl/components/sections/Points.tsx +2 -21
  356. package/templates/lorem-intl/components/sections/Quote.schema.ts +23 -0
  357. package/templates/lorem-intl/components/sections/Quote.tsx +2 -23
  358. package/templates/lorem-intl/package.json +1 -1
  359. package/templates/lorem-intl/scripts/seed.ts +18 -18
  360. package/templates/lorem-intl/smoodly/collections.ts +2 -2
  361. package/templates/lorem-intl/smoodly/config.ts +14 -15
  362. package/templates/lorem-intl/smoodly/elements.ts +5 -5
  363. package/templates/lorem-intl/smoodly/globals.ts +10 -7
  364. package/templates/lorem-intl/smoodly/richtext.tsx +5 -5
  365. package/templates/lorem-intl/smoodly/sections.ts +12 -13
  366. package/templates/lorem-intl/smoodly/server.ts +22 -44
  367. package/templates/lorem-intl/smoodly/toolsets.ts +3 -3
  368. package/templates/lorem-intl/smoodly/views.ts +33 -0
  369. package/templates/lorem-intl/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  370. package/templates/lorem-intl/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  371. package/templates/lorem-intl/supabase/migrations/20261007200100_smoodly_views.sql +58 -0
  372. package/templates/blank/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -1
  373. package/templates/blank/app/(smoodly)/admin/layout.tsx +0 -15
  374. package/templates/blank/smoodly/admin.tsx +0 -6
  375. package/templates/blank/smoodly/ops.ts +0 -11
  376. package/templates/blank-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -1
  377. package/templates/blank-intl/app/(smoodly)/admin/layout.tsx +0 -15
  378. package/templates/blank-intl/smoodly/admin.tsx +0 -6
  379. package/templates/blank-intl/smoodly/ops.ts +0 -11
  380. package/templates/lorem/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -1
  381. package/templates/lorem/app/(smoodly)/admin/layout.tsx +0 -15
  382. package/templates/lorem/smoodly/admin.tsx +0 -6
  383. package/templates/lorem/smoodly/ops.ts +0 -11
  384. package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -1
  385. package/templates/lorem-intl/app/(smoodly)/admin/layout.tsx +0 -15
  386. package/templates/lorem-intl/smoodly/admin.tsx +0 -6
  387. package/templates/lorem-intl/smoodly/ops.ts +0 -11
@@ -0,0 +1,55 @@
1
+ # Working in this Smoodly site
2
+
3
+ This is an Astro site with Smoodly installed: content lives in Supabase, the admin is mounted at /admin by the `smoodly/astro` integration in `astro.config.ts`, and the site reads content through live collections (`src/live.config.ts`). Read README.md first: it says where things are.
4
+
5
+ ## Rules
6
+
7
+ - **Register schemas in the config; views are files by name.** A section's view is
8
+ `src/sections/<name>.astro`, an element's `src/elements/<name>.astro`, a page
9
+ type's `src/page-types/<name>.astro`, an entry page's
10
+ `src/entry-pages/<collection>.astro`, where <name> is the schema's `name`.
11
+ The schema is listed in its barrel (`src/smoodly/sections.ts`, `elements.ts`,
12
+ the config's `pages`; collections and global items in `collections.ts`,
13
+ `globals.ts`) and `src/smoodly/config.ts` is the one registry. The server
14
+ names a schema with no file, and a file with no schema, at the first request.
15
+ - **A `.schema.ts` file imports no component.** Zones, toolsets, global items and
16
+ `node()` take schemas (`z.locked(heroSchema)`), never views.
17
+ - **After a new collection or a `smoodly` upgrade, run `npx smoodly migrate`**
18
+ and apply what it writes (`supabase db push` hosted, `supabase migration up`
19
+ local). It copies the core schema files the package ships and writes a
20
+ read view and order index per collection. Never edit a file it wrote.
21
+ The admin reads the base tables, so it lists a collection either way;
22
+ the view is for SQL and PostgREST readers, the index for `order`.
23
+ - **Content from code goes through `createContent(config)`** from `smoodly/astro`:
24
+ `scripts/seed.ts` is starter content written with it — the admin's own
25
+ operations, so validation and publish checks apply and a failure throws. Never
26
+ write rows or call the stores directly. The seed is idempotent; `npm run seed`.
27
+ - **Reads are live collections.** `src/live.config.ts` names one loader per
28
+ collection, global item, the pages and the page tree; a view reads with
29
+ `getLiveCollection("articles", { locale })` or `getLiveEntry("footer", { locale })`
30
+ from `astro:content`. Every read tags the response, and a publish expires it.
31
+ - **The admin, the preview address and the page shell are injected** by
32
+ `smoodly()` in `astro.config.ts`; the project has no route file for them. A page
33
+ file of your own under `src/pages` wins its address. `site` in `astro.config.ts`
34
+ is the site's own address (from `SITE_URL`).
35
+ - **A link an editor typed goes through `safeHref`** from "smoodly" in an `.astro`
36
+ view: `<a href={safeHref(link.href)}>`. Astro writes attributes as given, so a
37
+ `javascript:` URL would reach the page; the template build refuses a shipped
38
+ view that skips it.
39
+ - **Section styles are inline.** `<Section>` from `smoodly/astro/components` renders
40
+ the root element with Smoodly's styles set and the node id; marks
41
+ (`smoodly.marks(schema)`) tie rendered elements to fields.
42
+ - **Multilingual sites.** The locale set is `locales` in `src/smoodly/config.ts`
43
+ and `i18n` in `astro.config.ts`; the two must agree. UI strings of the site's
44
+ own go through `src/strings.ts` and `src/messages/<locale>.json`.
45
+ - **Fixed versus open pages.** A page type with a `slug` is fixed: one
46
+ record, created for you, never deleted. Without one it is open: editors
47
+ create as many as they like.
48
+
49
+ ## Commands
50
+
51
+ npm run dev # http://localhost:4321, /admin is the editor
52
+ npm run build # checks the schema files (smoodly migrate --check), then astro build
53
+ npm run start # the built server (dist/server/entry.mjs)
54
+ npm run seed # starter content, idempotent
55
+ npm run typecheck # astro check
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,60 @@
1
+ # Smoodly site
2
+
3
+ Created by `create-smoodly-app`. Content lives in Supabase; the admin is
4
+ mounted at `/admin` by the `smoodly/astro` integration. AGENTS.md holds the
5
+ rules for an agent (or a person) building the site.
6
+
7
+ npm run dev # http://localhost:4321 — /admin is the editor
8
+
9
+ ## Where things are
10
+
11
+ - `astro.config.ts` — the `smoodly()` integration (it mounts the admin, the preview address and the page shell), the node adapter, the cache and `site` (from `SITE_URL`).
12
+ - `src/smoodly/` — the wiring: `config.ts` (the registry of schemas and the settings), `collections.ts`, `sections.ts`, `elements.ts`, `globals.ts` and `toolsets.ts`. The schemas they register are `*.schema.ts` files under `src/components/`.
13
+ - `src/sections/` — one `<name>.astro` view per section schema, named by the schema's `name`; the header, footer and quote behind the global items are here too.
14
+ - `src/elements/` — one view per element schema, embedded in richtext.
15
+ - `src/page-types/` — one view per page type: `home`, `page` (open; About lives here), `articlesIndex` and `newsIndex` (fixed pages at a collection's path).
16
+ - `src/entry-pages/` — one view per collection that has entry pages: `articles` and `news`.
17
+ - `src/layouts/Layout.astro` — the page shell every view renders: head, header, footer, `src/styles/global.css`.
18
+ - `src/live.config.ts` — the live collections the views read with `getLiveCollection` and `getLiveEntry`.
19
+ - `src/strings.ts`, `src/messages/` — the site's own UI strings in the page's language.
20
+ - `public/images/` — the starter's photos; image fields hold URLs like `/images/hero_image.jpg`.
21
+ - `scripts/seed.ts` — the starter content, written through `createContent(config)`. `npm run seed` is idempotent.
22
+ - `.env` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
23
+
24
+ ## What is in the starter
25
+
26
+ The Lorem Co. site the installer seeded is a tour of the features:
27
+
28
+ - **Home** — a locked hero (fields only; nobody adds, moves or removes it), then a freeform body: image and text (richtext), the global **quote** placed from the picker's Global group, points (a list with a minimum and a maximum, an object per item), favourite articles (an editor-picked list of refs).
29
+ - **Articles** — a fixed page at the collection's own URL segment, so it is the collection's index: the hero is the editor's, the grid is code fetching published articles.
30
+ - **Company / About** — a path and a page under it (the breadcrumb), the same body sections, plus people.
31
+ - **News** — a collection at a nested address, `company/news`: code owns the whole address, so its index page and posts stay put when an editor renames Company, and nobody can create a page there.
32
+ - **An article** — a collection entry with a richtext body, tags (a localized list), a date and an author ref; versioned, so every save is a snapshot.
33
+ - **Global content** — the header and footer every page renders through \`src/layouts/Layout.astro\`, the contact details the footer reads, the quote pages place.
34
+
35
+ Reseed an empty database any time with `npm run seed`.
36
+
37
+ ## Moving the admin
38
+
39
+ The admin is at `/admin`. To serve it somewhere else, say `/studio`:
40
+
41
+ 1. set `smoodly({ adminPath: "/studio" })` in `astro.config.ts`,
42
+ 2. set `admin: { path: "/studio" }` in `src/smoodly/config.ts`.
43
+
44
+ The two must agree; the admin says so when they do not.
45
+
46
+ ## Editors
47
+
48
+ Anyone who may open `/admin` is a Supabase Auth user with a row in the
49
+ `editors` table. To add one: create the user in Supabase Studio
50
+ (Authentication → Users), then in the SQL editor:
51
+
52
+ insert into editors (user_id, role) values ('<the user id>', 'editor');
53
+
54
+ ## Hosted Supabase
55
+
56
+ supabase init # if the CLI has not been set up here yet
57
+ supabase link # pick the project
58
+ supabase db push # applies supabase/migrations/
59
+
60
+ Then put the project's URL and keys in `.env`.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .astro/
4
+ .env
5
+ smoodly-export-*
@@ -0,0 +1,15 @@
1
+ import { defineConfig, memoryCache } from "astro/config";
2
+ import node from "@astrojs/node";
3
+ import smoodly from "smoodly/astro";
4
+
5
+ export default defineConfig({
6
+ output: "server",
7
+ adapter: node({ mode: "standalone" }),
8
+ // Pages are cached whole and expire by tag when an editor publishes (one process, one cache).
9
+ cache: { provider: memoryCache() },
10
+ // The site's own address: canonical, hreflang and social links are absolute on it. Astro reads astro.config
11
+ // before .env, so this sees only a SITE_URL set in the shell; when it is unset, smoodly() sets `site` from
12
+ // SITE_URL in .env. Set SITE_URL where you deploy: `site` is fixed at `astro build`.
13
+ site: process.env.SITE_URL,
14
+ integrations: [smoodly({ config: "src/smoodly/config.ts" })],
15
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "smoodly-site",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "astro dev",
7
+ "build": "smoodly migrate --check && astro build",
8
+ "start": "node --env-file-if-exists=.env dist/server/entry.mjs",
9
+ "seed": "tsx scripts/seed.ts",
10
+ "typecheck": "astro check"
11
+ },
12
+ "dependencies": {
13
+ "@astrojs/node": "^11.1.6",
14
+ "@supabase/supabase-js": "^2.113.0",
15
+ "astro": "^7.3.5",
16
+ "smoodly": "0.0.46"
17
+ },
18
+ "devDependencies": {
19
+ "@astrojs/check": "^0.9.10",
20
+ "@types/node": "^26.4.1",
21
+ "typescript": "^5",
22
+ "tsx": "^4"
23
+ }
24
+ }
@@ -0,0 +1,405 @@
1
+ // The starter's content, written through the content API (`createContent(config)`
2
+ // from smoodly/astro): the admin's own operations, so validation and
3
+ // the publish checks apply and a failure throws. This file is also the
4
+ // example of writing content from code. Content only; it ships in the template and create-smoodly-app runs
5
+ // `npm run seed` after the first editor. (The example repo's demo
6
+ // editor account is a separate, example-only script.)
7
+ // Run: npm run seed (needs .env and a Supabase stack).
8
+ //
9
+ // Locales: the English content goes into the configured default locale,
10
+ // whatever its code, and the Finnish rows are written only when `fi` is
11
+ // supported and not the default (spec 2026-09-07 starter §4). The
12
+ // installer accepts any locale set; URL segments come from the
13
+ // registrations (the articles collection's per-locale `path`), so a
14
+ // `fi` default lands the index on `artikkelit`. Body copy is Latin in
15
+ // both; what a reader perceives as language (nav, headings, tags,
16
+ // roles, footer titles) is translated. Idempotent: exits once the home
17
+ // page has content.
18
+ import type { PageTree, TreeNode } from "smoodly";
19
+
20
+ const { config } = await import("../src/smoodly/config");
21
+ const { articles, news } = await import("../src/smoodly/collections");
22
+ const { node } = await import("smoodly");
23
+ const { createContent } = await import("smoodly/astro");
24
+ const { content, href } = createContent(config);
25
+ const { heroSchema } = await import("../src/components/sections/Hero.schema");
26
+ const { pageHeroSchema } = await import("../src/components/sections/PageHero.schema");
27
+ const { imageTextSchema } = await import("../src/components/sections/ImageText.schema");
28
+ const { pointsSchema } = await import("../src/components/sections/Points.schema");
29
+ const { favouriteArticlesSchema } = await import("../src/components/sections/FavouriteArticles.schema");
30
+ const { peopleSchema } = await import("../src/components/sections/People.schema");
31
+ const c = content;
32
+
33
+ const EN = config.locales.default;
34
+ const FI = config.locales.supported.includes("fi") && EN !== "fi" ? "fi" : null;
35
+
36
+ // The home page is code-owned: `find` returns its record, creating it when
37
+ // the database is new. It is seeded once somebody has given it sections.
38
+ const home = (await c.pages.find(EN, "/"))!;
39
+ if (Object.values((await c.pages.getTree(home.id, EN))?.zones ?? {}).some((nodes) => nodes.length > 0)) {
40
+ console.log(`Already seeded — the home page has content in "${EN}".`);
41
+ process.exit(0);
42
+ }
43
+
44
+ // --- helpers -------------------------------------------------------
45
+
46
+ type Fields = Record<string, unknown>;
47
+ type Node = Record<string, unknown>;
48
+
49
+ // Richtext is TipTap JSON, the stored format; RichText renders it.
50
+ const text = (s: string) => [{ type: "text", text: s }];
51
+ const p = (s: string): Node => ({ type: "paragraph", content: text(s) });
52
+ const h2 = (s: string): Node => ({ type: "heading", attrs: { level: 2 }, content: text(s) });
53
+ const quoteBlock = (s: string): Node => ({ type: "blockquote", content: [p(s)] });
54
+ const bullets = (...items: string[]): Node => ({ type: "bulletList", content: items.map((s) => ({ type: "listItem", content: [p(s)] })) });
55
+ // Block nodes (spec 2026-09-08): a library image, and the starter's two elements.
56
+ const imageBlock = (image: { asset: string }, caption: string): Node => ({ type: "image", attrs: { image, caption } });
57
+ const callout = (title: string, text: string, tone: "info" | "success" | "warning" = "info"): Node =>
58
+ ({ type: "element", attrs: { name: "callout", fields: { title, body: doc(p(text)) }, settings: { tone } } });
59
+ const cta = (heading: string, text: string, label: string, href: string): Node =>
60
+ ({ type: "element", attrs: { name: "cta", fields: { heading, text, label, href }, settings: { variant: "primary" } } });
61
+ const doc = (...content: Node[]) => ({ type: "doc", content });
62
+
63
+ // The starter's photos become assets: each file is uploaded once (a row
64
+ // in `assets`, the bytes in the `media` bucket), and a field value is
65
+ // `{ asset: <id> }` — the alt lives on the asset (spec 2026-09-08).
66
+ const image = async (name: string, alt: string): Promise<{ asset: string }> =>
67
+ ({ asset: (await c.media.upload(`public/images/${name}.jpg`, { alt, title: alt })).asset });
68
+ const img = {
69
+ article: await image("article_photo", "Notebook on a desk"),
70
+ home2: await image("home_section_2", "Team working"),
71
+ hero: await image("hero_image", "Office interior"),
72
+ about: await image("about_main", "Team meeting"),
73
+ };
74
+
75
+ /** The articles collection's URL segment in a locale, from its own `path`. */
76
+ const articlesSegment = (locale: string) => {
77
+ const declared = articles.path;
78
+ return typeof declared === "string" ? declared : (declared?.[locale] ?? declared?.[EN] ?? "posts");
79
+ };
80
+ /** The news collection's nested address in a locale (`company/news`), from its own `path`. */
81
+ const newsAddress = (locale: string) => {
82
+ const declared = news.path;
83
+ return typeof declared === "string" ? declared : (declared?.[locale] ?? declared?.[EN] ?? "company/news");
84
+ };
85
+ /** Site URLs for the starter's own pages in a locale. */
86
+ const paths = (locale: string) => ({
87
+ home: href("/", locale),
88
+ articles: href(`/${articlesSegment(locale)}`, locale),
89
+ news: href(`/${newsAddress(locale)}`, locale),
90
+ about: href(locale === FI ? "/yritys/tietoa-meista" : "/company/about-us", locale),
91
+ });
92
+
93
+ /** Create an entry in EN, add the FI row when there is one, and publish
94
+ * both — every entry is published in every locale it has BEFORE any
95
+ * page, because publish integrity is per locale and would (rightly)
96
+ * block a page whose tree references an entry that is a draft, or
97
+ * absent, in that locale. `fi` holds the localized fields; shared ones
98
+ * are repeated from `en`. */
99
+ async function seedEntry(collection: string, en: Fields, fi?: Fields) {
100
+ const row = await c.entries.create(collection, { locale: EN, fields: en });
101
+ await c.entries.publish(collection, row.id, EN);
102
+ if (FI && fi) {
103
+ await c.entries.addLocale(collection, row.id, FI, { from: EN, fields: { ...en, ...fi } });
104
+ await c.entries.publish(collection, row.id, FI);
105
+ }
106
+ return row;
107
+ }
108
+
109
+ /** A global item exists in every locale from the start: set and publish each. */
110
+ async function seedGlobal(name: string, en: Fields, fi?: Fields) {
111
+ const row = await c.globals.set(name, EN, en);
112
+ await c.globals.publish(name, EN);
113
+ if (FI) {
114
+ await c.globals.set(name, FI, { ...en, ...fi });
115
+ await c.globals.publish(name, FI);
116
+ }
117
+ return row;
118
+ }
119
+
120
+ type Tree = PageTree;
121
+ type Trees = { en: Tree; fi?: Tree };
122
+
123
+ /** Create a page (or a folder: `template: null`) in EN with its tree,
124
+ * publish it, then the FI row with its own tree — a locale's tree is
125
+ * its own (spec 2026-09-06); the Finnish page is copied from the
126
+ * English draft, then overwritten with the Finnish tree. */
127
+ async function seedPage(
128
+ input: { slug: string; title: string; template: string | null; parentId?: string; fi?: { slug?: string; title: string } },
129
+ trees?: Trees,
130
+ ) {
131
+ const page = await c.pages.create({ parentId: input.parentId ?? null, locale: EN, slug: input.slug, title: input.title, template: input.template });
132
+ if (trees) {
133
+ await c.pages.setTree(page.id, EN, trees.en);
134
+ await c.pages.publish(page.id, EN);
135
+ }
136
+ if (FI && input.fi) {
137
+ await c.pages.addLocale(page.id, FI, { from: EN, ...input.fi });
138
+ if (trees?.fi) {
139
+ await c.pages.setTree(page.id, FI, trees.fi);
140
+ await c.pages.publish(page.id, FI);
141
+ }
142
+ }
143
+ return page;
144
+ }
145
+
146
+ /** A code-owned page (the home page, a collection's index): its address,
147
+ * title and locales come from the registration, so there is nothing to
148
+ * create — `find` returns the record, and the trees are written to it. */
149
+ async function seedCodePage(path: string, trees: Trees) {
150
+ const page = (await c.pages.find(EN, path))!;
151
+ await c.pages.setTree(page.id, EN, trees.en);
152
+ await c.pages.publish(page.id, EN);
153
+ if (FI && trees.fi) {
154
+ await c.pages.setTree(page.id, FI, trees.fi);
155
+ await c.pages.publish(page.id, FI);
156
+ }
157
+ return page;
158
+ }
159
+
160
+ // --- people ---------------------------------------------------------
161
+
162
+ const jane = await seedEntry("people", { name: "Jane Doe", role: "Founder" }, { role: "Perustaja" });
163
+ const john = await seedEntry("people", { name: "John Smith", role: "Creative Director" }, { role: "Luova johtaja" });
164
+ const anna = await seedEntry("people", { name: "Anna Lorem", role: "Head of Design" }, { role: "Muotoilujohtaja" });
165
+ const mark = await seedEntry("people", { name: "Mark Ipsum", role: "Developer" }, { role: "Kehittäjä" });
166
+
167
+ // --- the article ----------------------------------------------------
168
+
169
+ const body = doc(
170
+ p("Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."),
171
+ p("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
172
+ h2("Sed ut perspiciatis unde omnis"),
173
+ p("Iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."),
174
+ imageBlock(img.home2, "Lorem ipsum dolor sit amet."),
175
+ quoteBlock("Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit."),
176
+ callout("Good to know", "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia."),
177
+ p("Sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit."),
178
+ h2("Quis autem vel eum iure"),
179
+ p("Reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur."),
180
+ bullets("At vero eos et accusamus et iusto odio", "Dignissimos ducimus qui blanditiis praesentium", "Voluptatum deleniti atque corrupti quos dolores"),
181
+ p("Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus."),
182
+ cta("Ready to talk?", "Sed ut perspiciatis unde omnis iste natus error sit voluptatem.", "Get in touch", paths(EN).about),
183
+ );
184
+
185
+ // Every collection is versioned: every save below is a
186
+ // snapshot. Title and body are Latin in both locales; tags and slug are
187
+ // per locale, so the Finnish row reads Finnish where a reader would notice.
188
+ const article = await seedEntry("articles", {
189
+ title: "Lorem ipsum dolor sit amet consectetur adipiscing",
190
+ slug: "lorem-ipsum-dolor-sit-amet",
191
+ excerpt: "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
192
+ body,
193
+ tags: ["Insights", "Design"],
194
+ image: img.article,
195
+ date: "2026-09-02",
196
+ author: jane.id,
197
+ }, {
198
+ slug: "lorem-ipsum-dolor-sit-amet-fi",
199
+ tags: ["Näkemyksiä", "Muotoilu"],
200
+ });
201
+
202
+ // Two news posts at the collection's nested address (/company/news/<slug>,
203
+ // /fi/yritys/uutiset/<slug>): Latin copy, a slug per locale.
204
+ await seedEntry("news", {
205
+ title: "Lorem ipsum dolor sit amet nova",
206
+ slug: "lorem-news",
207
+ date: "2026-09-20",
208
+ excerpt: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
209
+ body: doc(p("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")),
210
+ }, { slug: "lorem-uutinen" });
211
+ await seedEntry("news", {
212
+ title: "Ipsum dolor sit amet consectetur",
213
+ slug: "ipsum-update",
214
+ date: "2026-09-27",
215
+ excerpt: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum.",
216
+ body: doc(p("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.")),
217
+ }, { slug: "ipsum-paivitys" });
218
+
219
+ // --- global items ---------------------------------------------------
220
+
221
+ // No Home item: the brand in the header is the way home.
222
+ const nav = (locale: string, labels: [string, string]) => {
223
+ const links = paths(locale);
224
+ return [
225
+ { label: labels[0], href: links.articles },
226
+ { label: labels[1], href: links.about },
227
+ ];
228
+ };
229
+ await seedGlobal("header",
230
+ { brand: "Lorem Co.", nav: nav(EN, ["Articles", "About"]) },
231
+ FI ? { nav: nav(FI, ["Artikkelit", "Tietoa meistä"]) } : undefined,
232
+ );
233
+
234
+ await seedGlobal("footer", {
235
+ tagline: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
236
+ company: { title: "Company", links: [{ label: "About", href: paths(EN).about }, { label: "Articles", href: paths(EN).articles }, { label: "News", href: paths(EN).news }] },
237
+ legal: { title: "Legal", links: [{ label: "Privacy", href: "#" }, { label: "Terms", href: "#" }] },
238
+ social: { title: "Social", links: [{ label: "Instagram", href: "#" }, { label: "LinkedIn", href: "#" }] },
239
+ copyright: "© 2026 Lorem Co. Built with Smoodly.",
240
+ }, FI ? {
241
+ tagline: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
242
+ company: { title: "Yritys", links: [{ label: "Tietoa meistä", href: paths(FI).about }, { label: "Artikkelit", href: paths(FI).articles }, { label: "Uutiset", href: paths(FI).news }] },
243
+ legal: { title: "Lakiasiat", links: [{ label: "Tietosuoja", href: "#" }, { label: "Ehdot", href: "#" }] },
244
+ social: { title: "Some", links: [{ label: "Instagram", href: "#" }, { label: "LinkedIn", href: "#" }] },
245
+ } : undefined);
246
+
247
+ await seedGlobal("contact",
248
+ { email: "hello@lorem.co", phone: "+358 40 123 4567", address: "Lorem Street 1, 00100 Helsinki" },
249
+ { address: "Loremkatu 1, 00100 Helsinki" },
250
+ );
251
+
252
+ // A visual global item's settings ride the row under the reserved
253
+ // `$settings` key (spec 2026-09-13 §1): the quote is the site's one dark band.
254
+ const quoteRow = await seedGlobal("quote",
255
+ { text: "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores.", attribution: "Jane Doe, Founder", $settings: { background: "ink" } },
256
+ { attribution: "Jane Doe, perustaja" },
257
+ );
258
+ // A placement: the quote as a linked node, the same row wherever it sits.
259
+ const quote: TreeNode = { id: "node-quote", type: "global", fields: { item: "quote", ref: quoteRow.id } };
260
+
261
+ // --- pages ----------------------------------------------------------
262
+
263
+ const imageText = (locale: string, linkLabel: string) => node(imageTextSchema, {
264
+ image: img.home2,
265
+ heading: "Duis aute irure dolor in reprehenderit",
266
+ text: doc(
267
+ p("In voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
268
+ p("Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium."),
269
+ ),
270
+ link: { label: linkLabel, href: paths(locale).about },
271
+ }, { id: "node-intro", settings: { imageSide: "left" } });
272
+
273
+ const points: TreeNode = {
274
+ ...node(pointsSchema, {
275
+ heading: "Quis autem vel eum",
276
+ points: [
277
+ { icon: "circle", heading: "Iure reprehenderit", text: "Qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat." },
278
+ { icon: "square", heading: "Quo voluptas nulla", text: "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum." },
279
+ { icon: "diamond", heading: "Pariatur deleniti", text: "Similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga." },
280
+ ],
281
+ }, { id: "node-points" }),
282
+ // One Spacing & layout step stored (spec 2026-09-14 §2); the rest are Default and store nothing.
283
+ styles: { spacingBottom: "lg" },
284
+ };
285
+
286
+ const favourites = (heading: string) => node(favouriteArticlesSchema, { heading, items: [article.id] }, { id: "node-favourites" });
287
+
288
+ // The custom section (spec 2026-09-14 §6): the editor's own rows of
289
+ // richtext columns, widths in twelfths; Smoodly renders its layout CSS.
290
+ // Not a registered section, so it is a plain node object.
291
+ const customSection = (locale: string): TreeNode => ({
292
+ id: "node-custom",
293
+ type: "custom",
294
+ fields: { rows: [
295
+ { spans: [4, 8], gap: "lg", align: "top", reverse: false, columns: [
296
+ doc(h2("Sed ut perspiciatis"), p("Unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.")),
297
+ doc(p("Totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."), callout("Nemo enim", "Ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit.", "info")),
298
+ ] },
299
+ { spans: [12], gap: "md", align: "top", reverse: false, columns: [doc(cta("Neque porro quisquam", "Qui dolorem ipsum quia dolor sit amet.", "Read the articles", paths(locale).articles))] },
300
+ ] },
301
+ styles: { spacingBottom: "lg" },
302
+ });
303
+
304
+ const homeTree = (locale: string, t: { kicker: string; buttons: [string, string]; link: string; favourites: string }): Tree => ({
305
+ // The built-in SEO set rides the tree (spec 2026-09-10 page fields §2).
306
+ meta: { description: "Lorem Co. — consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore." },
307
+ zones: {
308
+ // The locked hero's one node. Pages created in the admin get theirs
309
+ // from the editor (fillLockedZones on load); a seeded tree brings its own.
310
+ hero: [node(heroSchema, {
311
+ kicker: t.kicker,
312
+ heading: "Consectetur adipiscing elit sed do eiusmod",
313
+ text: "Tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip.",
314
+ buttons: [
315
+ { label: t.buttons[0], href: paths(locale).about },
316
+ { label: t.buttons[1], href: paths(locale).articles },
317
+ ],
318
+ image: img.hero,
319
+ }, { id: "node-hero" })],
320
+ body: [imageText(locale, t.link), quote, points, customSection(locale), favourites(t.favourites)],
321
+ },
322
+ });
323
+
324
+ await seedCodePage("/", {
325
+ en: homeTree(EN, { kicker: "Lorem ipsum dolor", buttons: ["About us", "Read articles"], link: "About Lorem Co. →", favourites: "From the journal" }),
326
+ fi: FI ? homeTree(FI, { kicker: "Lorem ipsum dolor", buttons: ["Tietoa meistä", "Lue artikkelit"], link: "Tietoa Lorem Co:sta →", favourites: "Ajankohtaista" }) : undefined,
327
+ });
328
+
329
+ // The Articles index: the fixed page at the collection's own segments,
330
+ // so the mount in the Pages list opens this page. Only a hero; the grid
331
+ // is the view's own fetch.
332
+ const articlesHero = (heading: string, text: string): Tree => ({
333
+ zones: { hero: [node(pageHeroSchema, { heading, text }, { id: "node-articles-hero" })] },
334
+ });
335
+ await seedCodePage(`/${articlesSegment(EN)}`, {
336
+ en: articlesHero("Articles", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt."),
337
+ fi: articlesHero("Artikkelit", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt."),
338
+ });
339
+
340
+ // A folder and a page under it: the tree the admin lists, and the
341
+ // breadcrumb the Page shape renders from the `page` prop. Each exists in
342
+ // both locales with its own slug and title.
343
+ const company = await seedPage({ slug: "company", title: "Company", template: null, fi: { slug: "yritys", title: "Yritys" } });
344
+
345
+ // The News index: the code-owned page on the news collection's nested
346
+ // address (spec 2026-09-29 nested paths §3) — it shows under Company in
347
+ // URL only; renaming Company never moves it.
348
+ await seedCodePage(`/${newsAddress(EN)}`, {
349
+ en: { zones: { hero: [node(pageHeroSchema, { heading: "News", text: "Lorem ipsum dolor sit amet — what is new at Lorem Co." }, { id: "node-news-hero" })] } },
350
+ fi: { zones: { hero: [node(pageHeroSchema, { heading: "Uutiset", text: "Lorem ipsum dolor sit amet — mitä Lorem Co:lle kuuluu." }, { id: "node-news-hero" })] } },
351
+ });
352
+
353
+ const aboutTree = (t: { kicker: string; people: string; description: string }): Tree => ({
354
+ fields: { showInNavigation: true },
355
+ meta: { description: t.description },
356
+ zones: {
357
+ hero: [node(pageHeroSchema, {
358
+ kicker: t.kicker,
359
+ heading: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
360
+ text: "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
361
+ image: img.about,
362
+ }, { id: "node-about-hero" })],
363
+ body: [
364
+ node(imageTextSchema, {
365
+ heading: "Duis aute irure dolor in reprehenderit",
366
+ text: doc(
367
+ p("In voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
368
+ p("Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis."),
369
+ p("Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt."),
370
+ ),
371
+ }, { id: "node-about-text", settings: { imageSide: "left" } }),
372
+ quote,
373
+ node(peopleSchema, { heading: t.people, people: [jane.id, john.id, anna.id, mark.id] }, { id: "node-people" }),
374
+ ],
375
+ },
376
+ });
377
+ const about = await seedPage(
378
+ { slug: "about-us", title: "About us", template: "page", parentId: company.id, fi: { slug: "tietoa-meista", title: "Tietoa meistä" } },
379
+ {
380
+ en: aboutTree({ kicker: "About us", people: "Our people", description: "Lorem ipsum dolor sit amet — who we are and what we do." }),
381
+ fi: aboutTree({ kicker: "Tietoa meistä", people: "Meidän väki", description: "Lorem ipsum dolor sit amet — keitä olemme ja mitä teemme." }),
382
+ },
383
+ );
384
+
385
+ // A page under About us, shown in its sub-navigation through the Page
386
+ // shape's `showInNavigation` field (spec 2026-09-10 page fields §7).
387
+ const teamTree = (t: { heading: string; text: string; people: string }): Tree => ({
388
+ fields: { showInNavigation: true },
389
+ meta: { description: t.text },
390
+ zones: {
391
+ hero: [node(pageHeroSchema, { kicker: t.heading, heading: t.heading, text: t.text, image: img.about }, { id: "node-team-hero" })],
392
+ body: [node(peopleSchema, { heading: t.people, people: [jane.id, john.id, anna.id, mark.id] }, { id: "node-team-people" })],
393
+ },
394
+ });
395
+ await seedPage(
396
+ { slug: "team", title: "Team", template: "page", parentId: about.id, fi: { slug: "tiimi", title: "Tiimi" } },
397
+ {
398
+ en: teamTree({ heading: "Team", text: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip.", people: "The whole team" }),
399
+ fi: teamTree({ heading: "Tiimi", text: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip.", people: "Koko tiimi" }),
400
+ },
401
+ );
402
+
403
+ console.log(
404
+ `Seeded Lorem Co. in "${EN}"${FI ? ` and "${FI}"` : ""}: home, the Articles index (${articlesSegment(EN)}${FI ? ` / ${articlesSegment(FI)}` : ""}), company/about-us and its Team page${FI ? " (yritys/tietoa-meista/tiimi)" : ""}, one article, News (${newsAddress(EN)}${FI ? ` / ${newsAddress(FI)}` : ""}) with two posts, four people, header, footer, contact and quote — all published.`,
405
+ );
@@ -0,0 +1,19 @@
1
+ ---
2
+ // examples/astro/src/components/ArticleCard.astro
3
+ // One article card: photo, first tag and date, title, excerpt. The grid item on the Articles page and in
4
+ // Favourite articles; `compact` drops the excerpt for the Related list. The caller resolves the URL.
5
+ import { Image } from "smoodly/astro/components";
6
+ import { formatDate } from "@/components/articles";
7
+
8
+ const { article, url, locale, compact = false } = Astro.props;
9
+ const Title = compact ? "h3" : "h2";
10
+ ---
11
+ <a class="card" href={url}>
12
+ {article.image && <Image image={article.image} class="card__image" sizes="(min-width: 900px) 33vw, 100vw" />}
13
+ <div class="card__meta">
14
+ {article.tags?.[0] && <span class="card__tag">{article.tags[0]}</span>}
15
+ {article.date && <span>{formatDate(article.date, locale)}</span>}
16
+ </div>
17
+ <Title class="card__title">{article.title}</Title>
18
+ {!compact && article.excerpt && <p class="card__excerpt">{article.excerpt}</p>}
19
+ </a>
@@ -0,0 +1,42 @@
1
+ // Small helpers shared by the Articles index, the article page and the
2
+ // Favourite articles section — the article's URL, dates in the request
3
+ // locale, and the read time from the richtext body.
4
+ import type { ResolvedImage } from "smoodly";
5
+ import { articles } from "@/smoodly/collections";
6
+
7
+ export type ArticleFields = {
8
+ title: string;
9
+ slug: string;
10
+ excerpt?: string;
11
+ body?: unknown;
12
+ tags?: string[];
13
+ image?: ResolvedImage;
14
+ date: string;
15
+ };
16
+
17
+ /** The collection's segment in a locale: its per-locale path, or the
18
+ * first declared one for a locale the map does not name. */
19
+ function segment(locale: string): string {
20
+ const p = articles.path;
21
+ if (typeof p === "string") return p;
22
+ return p?.[locale] ?? Object.values(p ?? {})[0] ?? "posts";
23
+ }
24
+
25
+ export const articlesIndexPath = (locale: string) => `/${segment(locale)}`;
26
+
27
+ export const formatDate = (iso: string, locale: string) =>
28
+ new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(new Date(iso));
29
+
30
+ /** A datetime field's UTC instant, shown in the site's zone (config.timeZone). */
31
+ export const formatDateTime = (iso: string, locale: string, timeZone: string) =>
32
+ new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", timeZone }).format(new Date(iso));
33
+
34
+ type Node = { text?: string; content?: Node[] };
35
+ export function richtextText(doc: unknown): string {
36
+ const walk = (n: Node): string => n.text ?? (n.content ?? []).map(walk).join(" ");
37
+ return walk((doc ?? {}) as Node);
38
+ }
39
+
40
+ /** Words / 200, at least one minute. */
41
+ export const readingMinutes = (doc: unknown) =>
42
+ Math.max(1, Math.round(richtextText(doc).split(/\s+/).filter(Boolean).length / 200));