create-smoodly-app 0.0.45 → 0.0.47

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 (395) 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/astro-blank/supabase/migrations/20261008120000_smoodly_v10.sql +47 -0
  41. package/templates/{blank/supabase/migrations/20260930041056_smoodly_views.sql → astro-blank/supabase/migrations/20261008120100_smoodly_views.sql} +1 -1
  42. package/templates/astro-blank/tsconfig.json +17 -0
  43. package/templates/astro-blank-intl/AGENTS.md +55 -0
  44. package/templates/astro-blank-intl/CLAUDE.md +1 -0
  45. package/templates/astro-blank-intl/README.md +89 -0
  46. package/templates/astro-blank-intl/_gitignore +5 -0
  47. package/templates/astro-blank-intl/astro.config.ts +17 -0
  48. package/templates/astro-blank-intl/package.json +24 -0
  49. package/templates/astro-blank-intl/scripts/seed.ts +80 -0
  50. package/templates/astro-blank-intl/src/components/LocaleNav.astro +15 -0
  51. package/templates/astro-blank-intl/src/components/elements/Button.schema.ts +22 -0
  52. package/templates/astro-blank-intl/src/components/pages/Home.schema.ts +17 -0
  53. package/templates/astro-blank-intl/src/components/pages/Page.schema.ts +14 -0
  54. package/templates/astro-blank-intl/src/components/sections/Text.schema.ts +24 -0
  55. package/templates/astro-blank-intl/src/elements/button.astro +10 -0
  56. package/templates/astro-blank-intl/src/entry-pages/posts.astro +23 -0
  57. package/templates/astro-blank-intl/src/layouts/Layout.astro +25 -0
  58. package/templates/astro-blank-intl/src/live.config.ts +13 -0
  59. package/templates/astro-blank-intl/src/page-types/home.astro +15 -0
  60. package/templates/astro-blank-intl/src/page-types/page.astro +15 -0
  61. package/templates/astro-blank-intl/src/pages/404.astro +7 -0
  62. package/templates/astro-blank-intl/src/sections/text.astro +18 -0
  63. package/templates/astro-blank-intl/src/smoodly/collections.ts +24 -0
  64. package/templates/astro-blank-intl/src/smoodly/config.ts +29 -0
  65. package/templates/astro-blank-intl/src/smoodly/elements.ts +5 -0
  66. package/templates/astro-blank-intl/src/smoodly/globals.ts +18 -0
  67. package/templates/astro-blank-intl/src/smoodly/sections.ts +5 -0
  68. package/templates/astro-blank-intl/src/smoodly/toolsets.ts +14 -0
  69. package/templates/astro-blank-intl/src/styles/global.css +57 -0
  70. package/templates/astro-blank-intl/supabase/migrations/20260926120000_smoodly_v1.sql +535 -0
  71. package/templates/astro-blank-intl/supabase/migrations/20260926130000_smoodly_v2.sql +26 -0
  72. package/templates/astro-blank-intl/supabase/migrations/20260926140000_smoodly_v3.sql +38 -0
  73. package/templates/astro-blank-intl/supabase/migrations/20260926150000_smoodly_v4.sql +41 -0
  74. package/templates/astro-blank-intl/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  75. package/templates/astro-blank-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  76. package/templates/astro-blank-intl/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
  77. package/templates/astro-blank-intl/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  78. package/templates/astro-blank-intl/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  79. package/templates/astro-blank-intl/supabase/migrations/20261008120000_smoodly_v10.sql +47 -0
  80. package/templates/{blank-intl/supabase/migrations/20260930041056_smoodly_views.sql → astro-blank-intl/supabase/migrations/20261008120100_smoodly_views.sql} +1 -1
  81. package/templates/astro-blank-intl/tsconfig.json +17 -0
  82. package/templates/astro-blank-intl.tsconfig.json +24 -0
  83. package/templates/astro-blank.tsconfig.json +24 -0
  84. package/templates/astro-lorem/AGENTS.md +55 -0
  85. package/templates/astro-lorem/CLAUDE.md +1 -0
  86. package/templates/astro-lorem/README.md +60 -0
  87. package/templates/astro-lorem/_gitignore +5 -0
  88. package/templates/astro-lorem/astro.config.ts +15 -0
  89. package/templates/astro-lorem/package.json +24 -0
  90. package/templates/astro-lorem/public/images/about_main.jpg +0 -0
  91. package/templates/astro-lorem/public/images/article_photo.jpg +0 -0
  92. package/templates/astro-lorem/public/images/hero_image.jpg +0 -0
  93. package/templates/astro-lorem/public/images/home_section_2.jpg +0 -0
  94. package/templates/astro-lorem/scripts/seed.ts +405 -0
  95. package/templates/astro-lorem/src/components/ArticleCard.astro +19 -0
  96. package/templates/astro-lorem/src/components/articles.ts +42 -0
  97. package/templates/astro-lorem/src/components/elements/Callout.schema.ts +24 -0
  98. package/templates/astro-lorem/src/components/elements/Cta.schema.ts +21 -0
  99. package/templates/astro-lorem/src/components/news.ts +13 -0
  100. package/templates/astro-lorem/src/components/pages/Articles.schema.ts +20 -0
  101. package/templates/astro-lorem/src/components/pages/Home.schema.ts +26 -0
  102. package/templates/astro-lorem/src/components/pages/News.schema.ts +17 -0
  103. package/templates/astro-lorem/src/components/pages/Page.schema.ts +24 -0
  104. package/templates/astro-lorem/src/components/sections/FavouriteArticles.schema.ts +17 -0
  105. package/templates/astro-lorem/src/components/sections/Footer.schema.ts +31 -0
  106. package/templates/astro-lorem/src/components/sections/Header.schema.ts +21 -0
  107. package/templates/astro-lorem/src/components/sections/Hero.schema.ts +24 -0
  108. package/templates/astro-lorem/src/components/sections/ImageText.schema.ts +30 -0
  109. package/templates/astro-lorem/src/components/sections/PageHero.schema.ts +21 -0
  110. package/templates/astro-lorem/src/components/sections/People.schema.ts +15 -0
  111. package/templates/astro-lorem/src/components/sections/Points.schema.ts +21 -0
  112. package/templates/astro-lorem/src/components/sections/Quote.schema.ts +23 -0
  113. package/templates/astro-lorem/src/elements/callout.astro +13 -0
  114. package/templates/astro-lorem/src/elements/cta.astro +15 -0
  115. package/templates/astro-lorem/src/entry-pages/articles.astro +71 -0
  116. package/templates/astro-lorem/src/entry-pages/news.astro +29 -0
  117. package/templates/astro-lorem/src/layouts/Layout.astro +39 -0
  118. package/templates/astro-lorem/src/live.config.ts +20 -0
  119. package/templates/astro-lorem/src/messages/en.json +7 -0
  120. package/templates/astro-lorem/src/messages/fi.json +7 -0
  121. package/templates/astro-lorem/src/page-types/articlesIndex.astro +24 -0
  122. package/templates/astro-lorem/src/page-types/home.astro +14 -0
  123. package/templates/astro-lorem/src/page-types/newsIndex.astro +28 -0
  124. package/templates/astro-lorem/src/page-types/page.astro +27 -0
  125. package/templates/astro-lorem/src/pages/404.astro +7 -0
  126. package/templates/astro-lorem/src/sections/favouriteArticles.astro +26 -0
  127. package/templates/astro-lorem/src/sections/footer.astro +37 -0
  128. package/templates/astro-lorem/src/sections/header.astro +27 -0
  129. package/templates/astro-lorem/src/sections/hero.astro +28 -0
  130. package/templates/astro-lorem/src/sections/imageText.astro +22 -0
  131. package/templates/astro-lorem/src/sections/pageHero.astro +20 -0
  132. package/templates/astro-lorem/src/sections/peopleSection.astro +24 -0
  133. package/templates/astro-lorem/src/sections/points.astro +32 -0
  134. package/templates/astro-lorem/src/sections/quote.astro +16 -0
  135. package/templates/astro-lorem/src/smoodly/collections.ts +63 -0
  136. package/templates/astro-lorem/src/smoodly/config.ts +48 -0
  137. package/templates/astro-lorem/src/smoodly/elements.ts +6 -0
  138. package/templates/astro-lorem/src/smoodly/globals.ts +29 -0
  139. package/templates/astro-lorem/src/smoodly/sections.ts +18 -0
  140. package/templates/astro-lorem/src/smoodly/toolsets.ts +16 -0
  141. package/templates/astro-lorem/src/strings.ts +17 -0
  142. package/templates/astro-lorem/src/styles/global.css +228 -0
  143. package/templates/astro-lorem/supabase/migrations/20260926120000_smoodly_v1.sql +535 -0
  144. package/templates/astro-lorem/supabase/migrations/20260926130000_smoodly_v2.sql +26 -0
  145. package/templates/astro-lorem/supabase/migrations/20260926140000_smoodly_v3.sql +38 -0
  146. package/templates/astro-lorem/supabase/migrations/20260926150000_smoodly_v4.sql +41 -0
  147. package/templates/astro-lorem/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  148. package/templates/astro-lorem/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  149. package/templates/astro-lorem/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
  150. package/templates/astro-lorem/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  151. package/templates/astro-lorem/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  152. package/templates/astro-lorem/supabase/migrations/20261008120000_smoodly_v10.sql +47 -0
  153. package/templates/{lorem/supabase/migrations/20260930041056_smoodly_views.sql → astro-lorem/supabase/migrations/20261008120100_smoodly_views.sql} +1 -1
  154. package/templates/astro-lorem/tsconfig.json +17 -0
  155. package/templates/astro-lorem-intl/AGENTS.md +55 -0
  156. package/templates/astro-lorem-intl/CLAUDE.md +1 -0
  157. package/templates/astro-lorem-intl/README.md +62 -0
  158. package/templates/astro-lorem-intl/_gitignore +5 -0
  159. package/templates/astro-lorem-intl/astro.config.ts +17 -0
  160. package/templates/astro-lorem-intl/package.json +24 -0
  161. package/templates/astro-lorem-intl/public/images/about_main.jpg +0 -0
  162. package/templates/astro-lorem-intl/public/images/article_photo.jpg +0 -0
  163. package/templates/astro-lorem-intl/public/images/hero_image.jpg +0 -0
  164. package/templates/astro-lorem-intl/public/images/home_section_2.jpg +0 -0
  165. package/templates/astro-lorem-intl/scripts/seed.ts +405 -0
  166. package/templates/astro-lorem-intl/src/components/ArticleCard.astro +19 -0
  167. package/templates/astro-lorem-intl/src/components/articles.ts +42 -0
  168. package/templates/astro-lorem-intl/src/components/elements/Callout.schema.ts +24 -0
  169. package/templates/astro-lorem-intl/src/components/elements/Cta.schema.ts +21 -0
  170. package/templates/astro-lorem-intl/src/components/news.ts +13 -0
  171. package/templates/astro-lorem-intl/src/components/pages/Articles.schema.ts +20 -0
  172. package/templates/astro-lorem-intl/src/components/pages/Home.schema.ts +26 -0
  173. package/templates/astro-lorem-intl/src/components/pages/News.schema.ts +17 -0
  174. package/templates/astro-lorem-intl/src/components/pages/Page.schema.ts +24 -0
  175. package/templates/astro-lorem-intl/src/components/sections/FavouriteArticles.schema.ts +17 -0
  176. package/templates/astro-lorem-intl/src/components/sections/Footer.schema.ts +31 -0
  177. package/templates/astro-lorem-intl/src/components/sections/Header.schema.ts +21 -0
  178. package/templates/astro-lorem-intl/src/components/sections/Hero.schema.ts +24 -0
  179. package/templates/astro-lorem-intl/src/components/sections/ImageText.schema.ts +30 -0
  180. package/templates/astro-lorem-intl/src/components/sections/PageHero.schema.ts +21 -0
  181. package/templates/astro-lorem-intl/src/components/sections/People.schema.ts +15 -0
  182. package/templates/astro-lorem-intl/src/components/sections/Points.schema.ts +21 -0
  183. package/templates/astro-lorem-intl/src/components/sections/Quote.schema.ts +23 -0
  184. package/templates/astro-lorem-intl/src/elements/callout.astro +13 -0
  185. package/templates/astro-lorem-intl/src/elements/cta.astro +15 -0
  186. package/templates/astro-lorem-intl/src/entry-pages/articles.astro +71 -0
  187. package/templates/astro-lorem-intl/src/entry-pages/news.astro +29 -0
  188. package/templates/astro-lorem-intl/src/layouts/Layout.astro +39 -0
  189. package/templates/astro-lorem-intl/src/live.config.ts +20 -0
  190. package/templates/astro-lorem-intl/src/messages/en.json +7 -0
  191. package/templates/astro-lorem-intl/src/messages/fi.json +7 -0
  192. package/templates/astro-lorem-intl/src/page-types/articlesIndex.astro +24 -0
  193. package/templates/astro-lorem-intl/src/page-types/home.astro +14 -0
  194. package/templates/astro-lorem-intl/src/page-types/newsIndex.astro +28 -0
  195. package/templates/astro-lorem-intl/src/page-types/page.astro +27 -0
  196. package/templates/astro-lorem-intl/src/pages/404.astro +7 -0
  197. package/templates/astro-lorem-intl/src/sections/favouriteArticles.astro +26 -0
  198. package/templates/astro-lorem-intl/src/sections/footer.astro +37 -0
  199. package/templates/astro-lorem-intl/src/sections/header.astro +27 -0
  200. package/templates/astro-lorem-intl/src/sections/hero.astro +28 -0
  201. package/templates/astro-lorem-intl/src/sections/imageText.astro +22 -0
  202. package/templates/astro-lorem-intl/src/sections/pageHero.astro +20 -0
  203. package/templates/astro-lorem-intl/src/sections/peopleSection.astro +24 -0
  204. package/templates/astro-lorem-intl/src/sections/points.astro +32 -0
  205. package/templates/astro-lorem-intl/src/sections/quote.astro +16 -0
  206. package/templates/astro-lorem-intl/src/smoodly/collections.ts +63 -0
  207. package/templates/astro-lorem-intl/src/smoodly/config.ts +48 -0
  208. package/templates/astro-lorem-intl/src/smoodly/elements.ts +6 -0
  209. package/templates/astro-lorem-intl/src/smoodly/globals.ts +29 -0
  210. package/templates/astro-lorem-intl/src/smoodly/sections.ts +18 -0
  211. package/templates/astro-lorem-intl/src/smoodly/toolsets.ts +16 -0
  212. package/templates/astro-lorem-intl/src/strings.ts +17 -0
  213. package/templates/astro-lorem-intl/src/styles/global.css +228 -0
  214. package/templates/astro-lorem-intl/supabase/migrations/20260926120000_smoodly_v1.sql +535 -0
  215. package/templates/astro-lorem-intl/supabase/migrations/20260926130000_smoodly_v2.sql +26 -0
  216. package/templates/astro-lorem-intl/supabase/migrations/20260926140000_smoodly_v3.sql +38 -0
  217. package/templates/astro-lorem-intl/supabase/migrations/20260926150000_smoodly_v4.sql +41 -0
  218. package/templates/astro-lorem-intl/supabase/migrations/20260929120000_smoodly_v5.sql +56 -0
  219. package/templates/astro-lorem-intl/supabase/migrations/20260929135523_smoodly_v6.sql +200 -0
  220. package/templates/astro-lorem-intl/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
  221. package/templates/astro-lorem-intl/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  222. package/templates/astro-lorem-intl/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  223. package/templates/astro-lorem-intl/supabase/migrations/20261008120000_smoodly_v10.sql +47 -0
  224. package/templates/{lorem-intl/supabase/migrations/20260930041056_smoodly_views.sql → astro-lorem-intl/supabase/migrations/20261008120100_smoodly_views.sql} +1 -1
  225. package/templates/astro-lorem-intl/tsconfig.json +17 -0
  226. package/templates/astro-lorem-intl.tsconfig.json +24 -0
  227. package/templates/astro-lorem.tsconfig.json +24 -0
  228. package/templates/blank/AGENTS.md +23 -16
  229. package/templates/blank/README.md +20 -15
  230. package/templates/blank/app/(smoodly)/admin/[[...segments]]/route.ts +8 -0
  231. package/templates/blank/components/elements/Button.schema.ts +22 -0
  232. package/templates/blank/components/elements/Button.tsx +2 -20
  233. package/templates/blank/components/pages/Home.schema.ts +17 -0
  234. package/templates/blank/components/pages/Home.tsx +2 -16
  235. package/templates/blank/components/pages/Page.schema.ts +14 -0
  236. package/templates/blank/components/pages/Page.tsx +2 -14
  237. package/templates/blank/components/pages/Post.tsx +5 -4
  238. package/templates/blank/components/sections/Text.schema.ts +24 -0
  239. package/templates/blank/components/sections/Text.tsx +7 -27
  240. package/templates/blank/package.json +1 -1
  241. package/templates/blank/scripts/seed.ts +2 -2
  242. package/templates/blank/smoodly/config.ts +8 -7
  243. package/templates/blank/smoodly/elements.ts +4 -4
  244. package/templates/blank/smoodly/globals.ts +4 -2
  245. package/templates/blank/smoodly/richtext.tsx +4 -5
  246. package/templates/blank/smoodly/sections.ts +4 -4
  247. package/templates/blank/smoodly/server.ts +22 -44
  248. package/templates/blank/smoodly/toolsets.ts +2 -2
  249. package/templates/blank/smoodly/views.ts +15 -0
  250. package/templates/blank/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  251. package/templates/blank/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  252. package/templates/blank/supabase/migrations/20261008120000_smoodly_v10.sql +47 -0
  253. package/templates/blank/supabase/migrations/20261008120100_smoodly_views.sql +20 -0
  254. package/templates/blank-intl/AGENTS.md +23 -16
  255. package/templates/blank-intl/README.md +20 -15
  256. package/templates/blank-intl/app/(smoodly)/admin/[[...segments]]/route.ts +8 -0
  257. package/templates/blank-intl/components/elements/Button.schema.ts +22 -0
  258. package/templates/blank-intl/components/elements/Button.tsx +2 -20
  259. package/templates/blank-intl/components/pages/Home.schema.ts +17 -0
  260. package/templates/blank-intl/components/pages/Home.tsx +2 -16
  261. package/templates/blank-intl/components/pages/Page.schema.ts +14 -0
  262. package/templates/blank-intl/components/pages/Page.tsx +2 -14
  263. package/templates/blank-intl/components/pages/Post.tsx +5 -4
  264. package/templates/blank-intl/components/sections/Text.schema.ts +24 -0
  265. package/templates/blank-intl/components/sections/Text.tsx +7 -27
  266. package/templates/blank-intl/package.json +1 -1
  267. package/templates/blank-intl/scripts/seed.ts +2 -2
  268. package/templates/blank-intl/smoodly/config.ts +8 -7
  269. package/templates/blank-intl/smoodly/elements.ts +4 -4
  270. package/templates/blank-intl/smoodly/globals.ts +4 -2
  271. package/templates/blank-intl/smoodly/richtext.tsx +4 -5
  272. package/templates/blank-intl/smoodly/sections.ts +4 -4
  273. package/templates/blank-intl/smoodly/server.ts +22 -44
  274. package/templates/blank-intl/smoodly/toolsets.ts +2 -2
  275. package/templates/blank-intl/smoodly/views.ts +15 -0
  276. package/templates/blank-intl/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  277. package/templates/blank-intl/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  278. package/templates/blank-intl/supabase/migrations/20261008120000_smoodly_v10.sql +47 -0
  279. package/templates/blank-intl/supabase/migrations/20261008120100_smoodly_views.sql +20 -0
  280. package/templates/lorem/AGENTS.md +23 -16
  281. package/templates/lorem/README.md +3 -3
  282. package/templates/lorem/app/(smoodly)/admin/[[...segments]]/route.ts +8 -0
  283. package/templates/lorem/components/elements/Callout.schema.ts +24 -0
  284. package/templates/lorem/components/elements/Callout.tsx +2 -24
  285. package/templates/lorem/components/elements/Cta.schema.ts +21 -0
  286. package/templates/lorem/components/elements/Cta.tsx +2 -21
  287. package/templates/lorem/components/pages/Article.tsx +1 -1
  288. package/templates/lorem/components/pages/Articles.schema.ts +20 -0
  289. package/templates/lorem/components/pages/Articles.tsx +2 -20
  290. package/templates/lorem/components/pages/Home.schema.ts +26 -0
  291. package/templates/lorem/components/pages/Home.tsx +2 -26
  292. package/templates/lorem/components/pages/News.schema.ts +17 -0
  293. package/templates/lorem/components/pages/News.tsx +2 -17
  294. package/templates/lorem/components/pages/NewsPost.tsx +1 -1
  295. package/templates/lorem/components/pages/Page.schema.ts +24 -0
  296. package/templates/lorem/components/pages/Page.tsx +2 -24
  297. package/templates/lorem/components/sections/FavouriteArticles.schema.ts +17 -0
  298. package/templates/lorem/components/sections/FavouriteArticles.tsx +2 -17
  299. package/templates/lorem/components/sections/Footer.schema.ts +31 -0
  300. package/templates/lorem/components/sections/Footer.tsx +4 -32
  301. package/templates/lorem/components/sections/Header.schema.ts +21 -0
  302. package/templates/lorem/components/sections/Header.tsx +2 -21
  303. package/templates/lorem/components/sections/Hero.schema.ts +24 -0
  304. package/templates/lorem/components/sections/Hero.tsx +2 -24
  305. package/templates/lorem/components/sections/ImageText.schema.ts +30 -0
  306. package/templates/lorem/components/sections/ImageText.tsx +2 -30
  307. package/templates/lorem/components/sections/PageHero.schema.ts +21 -0
  308. package/templates/lorem/components/sections/PageHero.tsx +2 -21
  309. package/templates/lorem/components/sections/People.schema.ts +15 -0
  310. package/templates/lorem/components/sections/People.tsx +2 -14
  311. package/templates/lorem/components/sections/Points.schema.ts +21 -0
  312. package/templates/lorem/components/sections/Points.tsx +2 -21
  313. package/templates/lorem/components/sections/Quote.schema.ts +23 -0
  314. package/templates/lorem/components/sections/Quote.tsx +2 -23
  315. package/templates/lorem/package.json +1 -1
  316. package/templates/lorem/scripts/seed.ts +18 -18
  317. package/templates/lorem/smoodly/collections.ts +2 -2
  318. package/templates/lorem/smoodly/config.ts +14 -15
  319. package/templates/lorem/smoodly/elements.ts +5 -5
  320. package/templates/lorem/smoodly/globals.ts +10 -7
  321. package/templates/lorem/smoodly/richtext.tsx +5 -5
  322. package/templates/lorem/smoodly/sections.ts +12 -13
  323. package/templates/lorem/smoodly/server.ts +22 -44
  324. package/templates/lorem/smoodly/toolsets.ts +3 -3
  325. package/templates/lorem/smoodly/views.ts +33 -0
  326. package/templates/lorem/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  327. package/templates/lorem/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  328. package/templates/lorem/supabase/migrations/20261008120000_smoodly_v10.sql +47 -0
  329. package/templates/lorem/supabase/migrations/20261008120100_smoodly_views.sql +58 -0
  330. package/templates/lorem-intl/AGENTS.md +23 -16
  331. package/templates/lorem-intl/README.md +3 -3
  332. package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/route.ts +8 -0
  333. package/templates/lorem-intl/components/elements/Callout.schema.ts +24 -0
  334. package/templates/lorem-intl/components/elements/Callout.tsx +2 -24
  335. package/templates/lorem-intl/components/elements/Cta.schema.ts +21 -0
  336. package/templates/lorem-intl/components/elements/Cta.tsx +2 -21
  337. package/templates/lorem-intl/components/pages/Article.tsx +1 -1
  338. package/templates/lorem-intl/components/pages/Articles.schema.ts +20 -0
  339. package/templates/lorem-intl/components/pages/Articles.tsx +2 -20
  340. package/templates/lorem-intl/components/pages/Home.schema.ts +26 -0
  341. package/templates/lorem-intl/components/pages/Home.tsx +2 -26
  342. package/templates/lorem-intl/components/pages/News.schema.ts +17 -0
  343. package/templates/lorem-intl/components/pages/News.tsx +2 -17
  344. package/templates/lorem-intl/components/pages/NewsPost.tsx +1 -1
  345. package/templates/lorem-intl/components/pages/Page.schema.ts +24 -0
  346. package/templates/lorem-intl/components/pages/Page.tsx +2 -24
  347. package/templates/lorem-intl/components/sections/FavouriteArticles.schema.ts +17 -0
  348. package/templates/lorem-intl/components/sections/FavouriteArticles.tsx +2 -17
  349. package/templates/lorem-intl/components/sections/Footer.schema.ts +31 -0
  350. package/templates/lorem-intl/components/sections/Footer.tsx +4 -32
  351. package/templates/lorem-intl/components/sections/Header.schema.ts +21 -0
  352. package/templates/lorem-intl/components/sections/Header.tsx +2 -21
  353. package/templates/lorem-intl/components/sections/Hero.schema.ts +24 -0
  354. package/templates/lorem-intl/components/sections/Hero.tsx +2 -24
  355. package/templates/lorem-intl/components/sections/ImageText.schema.ts +30 -0
  356. package/templates/lorem-intl/components/sections/ImageText.tsx +2 -30
  357. package/templates/lorem-intl/components/sections/PageHero.schema.ts +21 -0
  358. package/templates/lorem-intl/components/sections/PageHero.tsx +2 -21
  359. package/templates/lorem-intl/components/sections/People.schema.ts +15 -0
  360. package/templates/lorem-intl/components/sections/People.tsx +2 -14
  361. package/templates/lorem-intl/components/sections/Points.schema.ts +21 -0
  362. package/templates/lorem-intl/components/sections/Points.tsx +2 -21
  363. package/templates/lorem-intl/components/sections/Quote.schema.ts +23 -0
  364. package/templates/lorem-intl/components/sections/Quote.tsx +2 -23
  365. package/templates/lorem-intl/package.json +1 -1
  366. package/templates/lorem-intl/scripts/seed.ts +18 -18
  367. package/templates/lorem-intl/smoodly/collections.ts +2 -2
  368. package/templates/lorem-intl/smoodly/config.ts +14 -15
  369. package/templates/lorem-intl/smoodly/elements.ts +5 -5
  370. package/templates/lorem-intl/smoodly/globals.ts +10 -7
  371. package/templates/lorem-intl/smoodly/richtext.tsx +5 -5
  372. package/templates/lorem-intl/smoodly/sections.ts +12 -13
  373. package/templates/lorem-intl/smoodly/server.ts +22 -44
  374. package/templates/lorem-intl/smoodly/toolsets.ts +3 -3
  375. package/templates/lorem-intl/smoodly/views.ts +33 -0
  376. package/templates/lorem-intl/supabase/migrations/20261007120000_smoodly_v8.sql +154 -0
  377. package/templates/lorem-intl/supabase/migrations/20261007200000_smoodly_v9.sql +30 -0
  378. package/templates/lorem-intl/supabase/migrations/20261008120000_smoodly_v10.sql +47 -0
  379. package/templates/lorem-intl/supabase/migrations/20261008120100_smoodly_views.sql +58 -0
  380. package/templates/blank/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -1
  381. package/templates/blank/app/(smoodly)/admin/layout.tsx +0 -15
  382. package/templates/blank/smoodly/admin.tsx +0 -6
  383. package/templates/blank/smoodly/ops.ts +0 -11
  384. package/templates/blank-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -1
  385. package/templates/blank-intl/app/(smoodly)/admin/layout.tsx +0 -15
  386. package/templates/blank-intl/smoodly/admin.tsx +0 -6
  387. package/templates/blank-intl/smoodly/ops.ts +0 -11
  388. package/templates/lorem/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -1
  389. package/templates/lorem/app/(smoodly)/admin/layout.tsx +0 -15
  390. package/templates/lorem/smoodly/admin.tsx +0 -6
  391. package/templates/lorem/smoodly/ops.ts +0 -11
  392. package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -1
  393. package/templates/lorem-intl/app/(smoodly)/admin/layout.tsx +0 -15
  394. package/templates/lorem-intl/smoodly/admin.tsx +0 -6
  395. package/templates/lorem-intl/smoodly/ops.ts +0 -11
@@ -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
+ // 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));
@@ -0,0 +1,24 @@
1
+ import { f, smoodly } from "smoodly";
2
+
3
+ // An Element: a small registered block editors insert inside an article
4
+ // body (the `richtext` context). Its body is richtext too — marks only —
5
+ // rendered by the package's pure RichText (a schema file imports no view:
6
+ // the view imports this file, and an import back would cycle).
7
+ export const calloutSchema = smoodly.schema.element({
8
+ name: "callout",
9
+ title: "Callout",
10
+ icon: "!",
11
+ description: "A highlighted note inside the text.",
12
+ contexts: ["richtext"],
13
+ fields: {
14
+ title: f.text().max(80),
15
+ body: f.richtext({ toolset: smoodly.toolset({ marks: ["bold", "italic", "link"] }) }),
16
+ },
17
+ settings: {
18
+ tone: f.select([{ value: "info", label: "Info" }, { value: "success", label: "Success" }, { value: "warning", label: "Warning" }]).default("info"),
19
+ },
20
+ sample: {
21
+ title: "Good to know",
22
+ body: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." }] }] },
23
+ },
24
+ });
@@ -0,0 +1,21 @@
1
+ import { f, smoodly } from "smoodly";
2
+
3
+ // A call to action inside an article body: heading, a line of text and
4
+ // one button. `variant` is a style — never translated.
5
+ export const ctaSchema = smoodly.schema.element({
6
+ name: "cta",
7
+ title: "Call to action",
8
+ icon: "→",
9
+ description: "A heading, a line and a button.",
10
+ contexts: ["richtext"],
11
+ fields: {
12
+ heading: f.text().max(80),
13
+ text: f.text().max(200).optional(),
14
+ label: f.text().max(40),
15
+ href: f.link(),
16
+ },
17
+ settings: {
18
+ variant: f.select([{ value: "primary", label: "Primary" }, { value: "outline", label: "Outline" }]).default("primary"),
19
+ },
20
+ sample: { heading: "Ready to start?", text: "Sed ut perspiciatis unde omnis iste natus.", label: "Get in touch", href: "/company/about-us" },
21
+ });
@@ -0,0 +1,13 @@
1
+ // The news index's URL, for the post page's back link, from the
2
+ // collection's own nested path (smoodly/collections.ts).
3
+ import { news } from "@/smoodly/collections";
4
+
5
+ /** The collection's address in a locale: its per-locale path, or the
6
+ * first declared one for a locale the map does not name. */
7
+ function address(locale: string): string {
8
+ const p = news.path;
9
+ if (typeof p === "string") return p;
10
+ return p?.[locale] ?? Object.values(p ?? {})[0] ?? "company/news";
11
+ }
12
+
13
+ export const newsIndexPath = (locale: string) => `/${address(locale)}`;
@@ -0,0 +1,20 @@
1
+ import { smoodly, z } from "smoodly";
2
+ import { pageHeroSchema } from "../sections/PageHero.schema";
3
+
4
+ // The Articles index: a fixed page whose slugs are the collection's own
5
+ // segments (smoodly/collections.ts), which makes it the collection's
6
+ // index page — the mount in the Pages list opens this page's editor. The
7
+ // editor owns the hero; the grid below is developer JSX that reads the
8
+ // articles through `q`, newest first (the collection's `order`), cached
9
+ // under the collection's tag. In the editor canvas the site runs in draft
10
+ // mode, so the grid reads the live rows too: an unpublished article shows
11
+ // there as it will (spec 2026-09-11 §2).
12
+ export const articlesSchema = smoodly.schema.page({
13
+ name: "articlesIndex",
14
+ title: "Articles",
15
+ slug: "posts",
16
+ zones: {
17
+ hero: z.locked(pageHeroSchema),
18
+ },
19
+ seo: true,
20
+ });
@@ -0,0 +1,26 @@
1
+ import { smoodly, z } from "smoodly";
2
+ import { heroSchema } from "../sections/Hero.schema";
3
+ import { imageTextSchema } from "../sections/ImageText.schema";
4
+ import { pointsSchema } from "../sections/Points.schema";
5
+ import { favouriteArticlesSchema } from "../sections/FavouriteArticles.schema";
6
+ import { peopleSchema } from "../sections/People.schema";
7
+ import { quoteSchema } from "../sections/Quote.schema";
8
+
9
+ // A page type: zones declared here, placed by the page type's view (DESIGN.md §3).
10
+ // `slug` makes it fixed — one record the editor can neither delete nor
11
+ // duplicate — and the config's homePageSlug maps that record to "/".
12
+ // The hero is locked: fields only, never added, moved or removed. The
13
+ // body is freeform over five sections. Quote is among them because the
14
+ // picker offers a global item only where the zone allows its section: an
15
+ // editor may drop a one-off quote, or place the global one from the
16
+ // picker's Global group.
17
+ export const homeSchema = smoodly.schema.page({
18
+ name: "home",
19
+ title: "Home",
20
+ slug: "home",
21
+ zones: {
22
+ hero: z.locked(heroSchema),
23
+ body: z.freeform({ sections: [imageTextSchema, pointsSchema, favouriteArticlesSchema, peopleSchema, quoteSchema], custom: true }),
24
+ },
25
+ seo: true,
26
+ });
@@ -0,0 +1,17 @@
1
+ import { smoodly, z } from "smoodly";
2
+ import { pageHeroSchema } from "../sections/PageHero.schema";
3
+
4
+ // The News index: a fixed page on the news collection's own nested
5
+ // address (smoodly/collections.ts), so it is the collection's index page.
6
+ // Code owns /company/news whole: the Company page above it is an
7
+ // editor's, and renaming it leaves the news where it is. The editor owns
8
+ // the hero; the list below reads the posts through `q`, newest first.
9
+ export const newsSchema = smoodly.schema.page({
10
+ name: "newsIndex",
11
+ title: "News",
12
+ slug: "company/news",
13
+ zones: {
14
+ hero: z.locked(pageHeroSchema),
15
+ },
16
+ seo: true,
17
+ });
@@ -0,0 +1,24 @@
1
+ import { f, smoodly, z } from "smoodly";
2
+ import { pageHeroSchema } from "../sections/PageHero.schema";
3
+ import { imageTextSchema } from "../sections/ImageText.schema";
4
+ import { pointsSchema } from "../sections/Points.schema";
5
+ import { favouriteArticlesSchema } from "../sections/FavouriteArticles.schema";
6
+ import { peopleSchema } from "../sections/People.schema";
7
+ import { quoteSchema } from "../sections/Quote.schema";
8
+
9
+ // Open registration: editors create as many of these as they like, at
10
+ // any depth the config allows. `page` is where this one sits in the tree.
11
+ // Its locked hero is the page hero, not the home one: each shape locks
12
+ // its own.
13
+ export const pageSchema = smoodly.schema.page({
14
+ name: "page",
15
+ title: "Page",
16
+ zones: {
17
+ hero: z.locked(pageHeroSchema),
18
+ body: z.freeform({ sections: [imageTextSchema, pointsSchema, favouriteArticlesSchema, peopleSchema, quoteSchema], custom: true }),
19
+ },
20
+ seo: true,
21
+ // A page field (spec 2026-09-10 page fields §7): the parent's view lists
22
+ // the children that keep this on. Edited on the Settings tab.
23
+ fields: { showInNavigation: f.boolean().default(true) },
24
+ });
@@ -0,0 +1,17 @@
1
+ import { f, smoodly } from "smoodly";
2
+ import { articles } from "@/smoodly/collections";
3
+
4
+ // Articles an editor picks by hand (refList), as cards. The resolve pass
5
+ // hydrates each ref before render, so the view gets article fields, not
6
+ // ids; an article unpublished after this page went live resolves to
7
+ // null and is skipped.
8
+ export const favouriteArticlesSchema = smoodly.schema.section({
9
+ name: "favouriteArticles",
10
+ title: "Favourite articles",
11
+ description: "Articles picked by hand, as cards.",
12
+ fields: {
13
+ heading: f.text(),
14
+ items: f.refList(() => articles).max(6),
15
+ },
16
+ sample: { heading: "From the journal", items: [] },
17
+ });