@velora-cms/server 0.11.0

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 (367) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +65 -0
  3. package/dist/admin-bundle/assets/__virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react__loadShare__.js_commonjs-proxy-Ng0fOpdU.js +1 -0
  4. package/dist/admin-bundle/assets/__virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom__loadShare__.js_commonjs-proxy-oc1i_iBU.js +9 -0
  5. package/dist/admin-bundle/assets/_virtual_mf-localSharedImportMap___mfe_internal__velora_admin__mf_owner__1-DOKdWYyj.js +1 -0
  6. package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare___mf_0_velora_mf_2_cms_mf_1_plugin_mf_2_sdk__loadShare__.js-C1-ZAxwO.js +2 -0
  7. package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react__loadShare__.js-BuUBskgj.js +9 -0
  8. package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-DMaku-HC.js +9 -0
  9. package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom__loadShare__.js-DYfqn12F.js +1 -0
  10. package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-BPoj3DC9.js +25 -0
  11. package/dist/admin-bundle/assets/content-trash.schema-DfXHkC-w.js +72 -0
  12. package/dist/admin-bundle/assets/hostInit--C4xK6nj.js +1 -0
  13. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  14. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  15. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  16. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  17. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  18. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  19. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  20. package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  21. package/dist/admin-bundle/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  22. package/dist/admin-bundle/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  23. package/dist/admin-bundle/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  24. package/dist/admin-bundle/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  25. package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  26. package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  27. package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  28. package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  29. package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  30. package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  31. package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  32. package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  33. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-400-normal-CyU-ddYS.woff2 +0 -0
  34. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-400-normal-EA1o-u_Q.woff +0 -0
  35. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-500-normal-C4MQITzh.woff2 +0 -0
  36. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-500-normal-DPXcppbm.woff +0 -0
  37. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-600-normal-0pRdybE_.woff2 +0 -0
  38. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-600-normal-MJZ0liF5.woff +0 -0
  39. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-400-normal-Bo5KPYvw.woff2 +0 -0
  40. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-400-normal-CFZh1unN.woff +0 -0
  41. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-500-normal-BletK1Hp.woff +0 -0
  42. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-500-normal-Cd6jVIg7.woff2 +0 -0
  43. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-600-normal-Dk5eISD_.woff +0 -0
  44. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-600-normal-KrqB56Mw.woff2 +0 -0
  45. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-400-normal-BeVLg08x.woff +0 -0
  46. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-400-normal-BpLfq-63.woff2 +0 -0
  47. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-500-normal-Ck_W4G78.woff2 +0 -0
  48. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-500-normal-eJxPnL1P.woff +0 -0
  49. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-600-normal-DT29Q8HW.woff +0 -0
  50. package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-600-normal-DjDAeNQD.woff2 +0 -0
  51. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  52. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  53. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  54. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  55. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  56. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  57. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  58. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  59. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  60. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  61. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  62. package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  63. package/dist/admin-bundle/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  64. package/dist/admin-bundle/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  65. package/dist/admin-bundle/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  66. package/dist/admin-bundle/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  67. package/dist/admin-bundle/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  68. package/dist/admin-bundle/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  69. package/dist/admin-bundle/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  70. package/dist/admin-bundle/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  71. package/dist/admin-bundle/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  72. package/dist/admin-bundle/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  73. package/dist/admin-bundle/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  74. package/dist/admin-bundle/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  75. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  76. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  77. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  78. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  79. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  80. package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  81. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  82. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  83. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  84. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  85. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  86. package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  87. package/dist/admin-bundle/assets/index-BIAF0ShP.js +542 -0
  88. package/dist/admin-bundle/assets/index-DhMY-0sC.js +148 -0
  89. package/dist/admin-bundle/assets/index-DnZV_gsg.css +1 -0
  90. package/dist/admin-bundle/assets/index-Do71v6NZ.js +7 -0
  91. package/dist/admin-bundle/assets/literata-cyrillic-ext-opsz-italic-5yUt4G_9.woff2 +0 -0
  92. package/dist/admin-bundle/assets/literata-cyrillic-ext-opsz-normal-C5tfwALZ.woff2 +0 -0
  93. package/dist/admin-bundle/assets/literata-cyrillic-opsz-italic-Bj0ndbyp.woff2 +0 -0
  94. package/dist/admin-bundle/assets/literata-cyrillic-opsz-normal-DgmypKXX.woff2 +0 -0
  95. package/dist/admin-bundle/assets/literata-greek-ext-opsz-italic-DFpO5j-Z.woff2 +0 -0
  96. package/dist/admin-bundle/assets/literata-greek-ext-opsz-normal-DGm64vGR.woff2 +0 -0
  97. package/dist/admin-bundle/assets/literata-greek-opsz-italic-CHERNRFq.woff2 +0 -0
  98. package/dist/admin-bundle/assets/literata-greek-opsz-normal-DFglUmAd.woff2 +0 -0
  99. package/dist/admin-bundle/assets/literata-latin-ext-opsz-italic-4y8iAqqs.woff2 +0 -0
  100. package/dist/admin-bundle/assets/literata-latin-ext-opsz-normal-STiTPs0F.woff2 +0 -0
  101. package/dist/admin-bundle/assets/literata-latin-opsz-italic-CJ93D38p.woff2 +0 -0
  102. package/dist/admin-bundle/assets/literata-latin-opsz-normal-BHZsWIlc.woff2 +0 -0
  103. package/dist/admin-bundle/assets/literata-vietnamese-opsz-italic-BdcrZMcO.woff2 +0 -0
  104. package/dist/admin-bundle/assets/literata-vietnamese-opsz-normal-Gqw5JQlq.woff2 +0 -0
  105. package/dist/admin-bundle/assets/mf-entry-bootstrap-0-aef015cd.js +41 -0
  106. package/dist/admin-bundle/assets/remoteEntry-CNyN68bc.js +14 -0
  107. package/dist/admin-bundle/assets/virtualExposes-BDCSo5OS.js +1 -0
  108. package/dist/admin-bundle/assets/vite-preload-helper-Cgledu0r.js +1 -0
  109. package/dist/admin-bundle/index.html +16 -0
  110. package/dist/app.d.ts +8 -0
  111. package/dist/app.js +252 -0
  112. package/dist/auth/api-content-author.d.ts +2 -0
  113. package/dist/auth/api-content-author.js +27 -0
  114. package/dist/auth/api-keys.d.ts +7 -0
  115. package/dist/auth/api-keys.js +85 -0
  116. package/dist/auth/jwt-payload.d.ts +16 -0
  117. package/dist/auth/jwt-payload.js +1 -0
  118. package/dist/auth/preview-tokens.d.ts +7 -0
  119. package/dist/auth/preview-tokens.js +76 -0
  120. package/dist/auth/rate-limit.d.ts +16 -0
  121. package/dist/auth/rate-limit.js +283 -0
  122. package/dist/auth/require-auth.d.ts +4 -0
  123. package/dist/auth/require-auth.js +28 -0
  124. package/dist/auth/route-rate-limits.d.ts +31 -0
  125. package/dist/auth/route-rate-limits.js +155 -0
  126. package/dist/auth/tokens.d.ts +15 -0
  127. package/dist/auth/tokens.js +38 -0
  128. package/dist/bin.d.ts +2 -0
  129. package/dist/bin.js +6 -0
  130. package/dist/boot/core-migration-gate.d.ts +12 -0
  131. package/dist/boot/core-migration-gate.js +52 -0
  132. package/dist/boot/resolve-boot-mode.d.ts +6 -0
  133. package/dist/boot/resolve-boot-mode.js +206 -0
  134. package/dist/bundled-templates/com.velora.blog/manifest.json +39 -0
  135. package/dist/bundled-templates/com.velora.blog/package.tar.gz +0 -0
  136. package/dist/bundled-templates/com.velora.blog/sample-content.json +14 -0
  137. package/dist/config/admin-ui.d.ts +1 -0
  138. package/dist/config/admin-ui.js +10 -0
  139. package/dist/config/cors.d.ts +7 -0
  140. package/dist/config/cors.js +53 -0
  141. package/dist/config/env-file-path.d.ts +2 -0
  142. package/dist/config/env-file-path.js +22 -0
  143. package/dist/config/graphql-limits.d.ts +2 -0
  144. package/dist/config/graphql-limits.js +26 -0
  145. package/dist/config/headless.d.ts +1 -0
  146. package/dist/config/headless.js +8 -0
  147. package/dist/config/license-revalidation.d.ts +2 -0
  148. package/dist/config/license-revalidation.js +16 -0
  149. package/dist/config/load-env.d.ts +1 -0
  150. package/dist/config/load-env.js +53 -0
  151. package/dist/config/marketplace.d.ts +11 -0
  152. package/dist/config/marketplace.js +19 -0
  153. package/dist/config/preview.d.ts +3 -0
  154. package/dist/config/preview.js +39 -0
  155. package/dist/config/public-docs.d.ts +1 -0
  156. package/dist/config/public-docs.js +9 -0
  157. package/dist/config/site.d.ts +7 -0
  158. package/dist/config/site.js +12 -0
  159. package/dist/config/trust-proxy.d.ts +2 -0
  160. package/dist/config/trust-proxy.js +42 -0
  161. package/dist/data-types/validate-data-type-settings.d.ts +1 -0
  162. package/dist/data-types/validate-data-type-settings.js +33 -0
  163. package/dist/db/content-db.d.ts +16 -0
  164. package/dist/db/content-db.js +109 -0
  165. package/dist/db/core-migrations/index.d.ts +14 -0
  166. package/dist/db/core-migrations/index.js +184 -0
  167. package/dist/db/core-migrations/steps.d.ts +4 -0
  168. package/dist/db/core-migrations/steps.js +66 -0
  169. package/dist/db/load-prisma-client.d.ts +2 -0
  170. package/dist/db/load-prisma-client.js +44 -0
  171. package/dist/db/prisma.d.ts +3 -0
  172. package/dist/db/prisma.js +67 -0
  173. package/dist/db/resolve-client-dir.d.ts +2 -0
  174. package/dist/db/resolve-client-dir.js +83 -0
  175. package/dist/docs/filter-public-api-spec.d.ts +2 -0
  176. package/dist/docs/filter-public-api-spec.js +52 -0
  177. package/dist/docs-bundle/404.html +41 -0
  178. package/dist/docs-bundle/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js +1 -0
  179. package/dist/docs-bundle/_astro/Search.astro_astro_type_script_index_0_lang.CavldbAs.js +1 -0
  180. package/dist/docs-bundle/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js +1 -0
  181. package/dist/docs-bundle/_astro/common.DTXA7sXM.css +3 -0
  182. package/dist/docs-bundle/_astro/ec.0vx5m.js +3 -0
  183. package/dist/docs-bundle/_astro/ec.w36nc.css +1 -0
  184. package/dist/docs-bundle/_astro/page.Dwipeu-R.js +1 -0
  185. package/dist/docs-bundle/_astro/print.ehPL0gv-.css +1 -0
  186. package/dist/docs-bundle/_astro/starlight-toc.CTCQXEge.js +1 -0
  187. package/dist/docs-bundle/_astro/ui-core.D-7Scnhs.js +2 -0
  188. package/dist/docs-bundle/favicon.svg +1 -0
  189. package/dist/docs-bundle/index.html +1 -0
  190. package/dist/docs-bundle/pagefind/fragment/en_18a36d7.pf_fragment +0 -0
  191. package/dist/docs-bundle/pagefind/fragment/en_1d9eb24.pf_fragment +0 -0
  192. package/dist/docs-bundle/pagefind/fragment/en_1ef320c.pf_fragment +0 -0
  193. package/dist/docs-bundle/pagefind/fragment/en_3099406.pf_fragment +0 -0
  194. package/dist/docs-bundle/pagefind/fragment/en_4a5b6fe.pf_fragment +0 -0
  195. package/dist/docs-bundle/pagefind/fragment/en_9aa652e.pf_fragment +0 -0
  196. package/dist/docs-bundle/pagefind/fragment/en_baccdf6.pf_fragment +0 -0
  197. package/dist/docs-bundle/pagefind/fragment/en_d81f3fe.pf_fragment +0 -0
  198. package/dist/docs-bundle/pagefind/fragment/en_d855550.pf_fragment +0 -0
  199. package/dist/docs-bundle/pagefind/fragment/en_e6d88d8.pf_fragment +0 -0
  200. package/dist/docs-bundle/pagefind/fragment/en_e9d3189.pf_fragment +0 -0
  201. package/dist/docs-bundle/pagefind/index/en_5feeed7.pf_index +0 -0
  202. package/dist/docs-bundle/pagefind/index/en_c6981f9.pf_index +0 -0
  203. package/dist/docs-bundle/pagefind/index/en_ce4f6bf.pf_index +0 -0
  204. package/dist/docs-bundle/pagefind/pagefind-component-ui.css +1509 -0
  205. package/dist/docs-bundle/pagefind/pagefind-component-ui.js +56 -0
  206. package/dist/docs-bundle/pagefind/pagefind-entry.json +1 -0
  207. package/dist/docs-bundle/pagefind/pagefind-highlight.js +1070 -0
  208. package/dist/docs-bundle/pagefind/pagefind-modular-ui.css +214 -0
  209. package/dist/docs-bundle/pagefind/pagefind-modular-ui.js +8 -0
  210. package/dist/docs-bundle/pagefind/pagefind-ui.css +1 -0
  211. package/dist/docs-bundle/pagefind/pagefind-ui.js +2 -0
  212. package/dist/docs-bundle/pagefind/pagefind-worker.js +6 -0
  213. package/dist/docs-bundle/pagefind/pagefind.en_1e94abd852.pf_meta +0 -0
  214. package/dist/docs-bundle/pagefind/pagefind.js +6 -0
  215. package/dist/docs-bundle/pagefind/wasm.en.pagefind +0 -0
  216. package/dist/docs-bundle/pagefind/wasm.unknown.pagefind +0 -0
  217. package/dist/docs-bundle/v1/index.html +86 -0
  218. package/dist/docs-bundle/v1/using-velora/build-a-headless-frontend/index.html +413 -0
  219. package/dist/docs-bundle/v1/using-velora/content-model/index.html +858 -0
  220. package/dist/docs-bundle/v1/using-velora/editing/index.html +748 -0
  221. package/dist/docs-bundle/v1/using-velora/getting-started/index.html +588 -0
  222. package/dist/docs-bundle/v1/using-velora/headless-api/index.html +1620 -0
  223. package/dist/docs-bundle/v1/using-velora/index.html +215 -0
  224. package/dist/docs-bundle/v1/using-velora/installation/index.html +886 -0
  225. package/dist/docs-bundle/v1/using-velora/installing-plugins/index.html +836 -0
  226. package/dist/docs-bundle/v1/using-velora/media/index.html +468 -0
  227. package/dist/docs-bundle/v1/using-velora/users-and-roles/index.html +561 -0
  228. package/dist/graphql/field-count-rule.d.ts +5 -0
  229. package/dist/graphql/field-count-rule.js +59 -0
  230. package/dist/graphql/schema.d.ts +12 -0
  231. package/dist/graphql/schema.js +330 -0
  232. package/dist/index.d.ts +1 -0
  233. package/dist/index.js +180 -0
  234. package/dist/lib.d.ts +2 -0
  235. package/dist/lib.js +39 -0
  236. package/dist/marketplace/install-bundle-members.d.ts +33 -0
  237. package/dist/marketplace/install-bundle-members.js +137 -0
  238. package/dist/marketplace/install-from-marketplace.d.ts +60 -0
  239. package/dist/marketplace/install-from-marketplace.js +581 -0
  240. package/dist/marketplace/license-revalidation-job.d.ts +38 -0
  241. package/dist/marketplace/license-revalidation-job.js +139 -0
  242. package/dist/marketplace/marketplace-client.d.ts +43 -0
  243. package/dist/marketplace/marketplace-client.js +228 -0
  244. package/dist/marketplace/revalidate-license.d.ts +39 -0
  245. package/dist/marketplace/revalidate-license.js +103 -0
  246. package/dist/media/media-service.d.ts +36 -0
  247. package/dist/media/media-service.js +162 -0
  248. package/dist/plugins/boot-plugin-loader.d.ts +32 -0
  249. package/dist/plugins/boot-plugin-loader.js +57 -0
  250. package/dist/plugins/hook-dispatcher.d.ts +13 -0
  251. package/dist/plugins/hook-dispatcher.js +68 -0
  252. package/dist/plugins/install-built-package.d.ts +36 -0
  253. package/dist/plugins/install-built-package.js +199 -0
  254. package/dist/plugins/install-package-tarball.d.ts +25 -0
  255. package/dist/plugins/install-package-tarball.js +73 -0
  256. package/dist/plugins/plugin-context.d.ts +5 -0
  257. package/dist/plugins/plugin-context.js +26 -0
  258. package/dist/plugins/plugin-package-store.d.ts +31 -0
  259. package/dist/plugins/plugin-package-store.js +64 -0
  260. package/dist/plugins/plugin-registry-service.d.ts +227 -0
  261. package/dist/plugins/plugin-registry-service.js +636 -0
  262. package/dist/plugins/seed-template-content.d.ts +23 -0
  263. package/dist/plugins/seed-template-content.js +198 -0
  264. package/dist/plugins/server-serializers.d.ts +3 -0
  265. package/dist/plugins/server-serializers.js +16 -0
  266. package/dist/plugins/simple-storage-api.d.ts +9 -0
  267. package/dist/plugins/simple-storage-api.js +26 -0
  268. package/dist/plugins/snapshot-groups.d.ts +16 -0
  269. package/dist/plugins/snapshot-groups.js +81 -0
  270. package/dist/plugins/structured-query-builder.d.ts +21 -0
  271. package/dist/plugins/structured-query-builder.js +60 -0
  272. package/dist/plugins/structured-snapshot-service.d.ts +16 -0
  273. package/dist/plugins/structured-snapshot-service.js +286 -0
  274. package/dist/plugins/structured-storage-api.d.ts +22 -0
  275. package/dist/plugins/structured-storage-api.js +91 -0
  276. package/dist/plugins/structured-storage-transaction.d.ts +22 -0
  277. package/dist/plugins/structured-storage-transaction.js +63 -0
  278. package/dist/plugins/structured-table-naming.d.ts +1 -0
  279. package/dist/plugins/structured-table-naming.js +18 -0
  280. package/dist/plugins/trusted-keys.d.ts +11 -0
  281. package/dist/plugins/trusted-keys.js +67 -0
  282. package/dist/plugins/trusted-signing-keys.d.ts +27 -0
  283. package/dist/plugins/trusted-signing-keys.js +40 -0
  284. package/dist/plugins/uninstall-bundle.d.ts +29 -0
  285. package/dist/plugins/uninstall-bundle.js +150 -0
  286. package/dist/plugins/verify-package.d.ts +36 -0
  287. package/dist/plugins/verify-package.js +121 -0
  288. package/dist/routes/admin-static.d.ts +8 -0
  289. package/dist/routes/admin-static.js +196 -0
  290. package/dist/routes/api-keys.d.ts +2 -0
  291. package/dist/routes/api-keys.js +95 -0
  292. package/dist/routes/api-playground.d.ts +2 -0
  293. package/dist/routes/api-playground.js +21 -0
  294. package/dist/routes/auth.d.ts +2 -0
  295. package/dist/routes/auth.js +102 -0
  296. package/dist/routes/content.d.ts +3 -0
  297. package/dist/routes/content.js +471 -0
  298. package/dist/routes/data-types.d.ts +3 -0
  299. package/dist/routes/data-types.js +100 -0
  300. package/dist/routes/docs-static.d.ts +8 -0
  301. package/dist/routes/docs-static.js +137 -0
  302. package/dist/routes/document-types.d.ts +3 -0
  303. package/dist/routes/document-types.js +149 -0
  304. package/dist/routes/headless-bridge.d.ts +10 -0
  305. package/dist/routes/headless-bridge.js +207 -0
  306. package/dist/routes/health.d.ts +2 -0
  307. package/dist/routes/health.js +15 -0
  308. package/dist/routes/marketplace-proxy.d.ts +8 -0
  309. package/dist/routes/marketplace-proxy.js +135 -0
  310. package/dist/routes/media.d.ts +6 -0
  311. package/dist/routes/media.js +93 -0
  312. package/dist/routes/plugin-api-routes.d.ts +12 -0
  313. package/dist/routes/plugin-api-routes.js +224 -0
  314. package/dist/routes/plugin-assets.d.ts +8 -0
  315. package/dist/routes/plugin-assets.js +51 -0
  316. package/dist/routes/plugin-management.d.ts +7 -0
  317. package/dist/routes/plugin-management.js +525 -0
  318. package/dist/routes/plugin-storage.d.ts +8 -0
  319. package/dist/routes/plugin-storage.js +104 -0
  320. package/dist/routes/plugins.d.ts +2 -0
  321. package/dist/routes/plugins.js +36 -0
  322. package/dist/routes/public-api.d.ts +9 -0
  323. package/dist/routes/public-api.js +545 -0
  324. package/dist/routes/published-item.d.ts +57 -0
  325. package/dist/routes/published-item.js +236 -0
  326. package/dist/routes/setup.d.ts +9 -0
  327. package/dist/routes/setup.js +477 -0
  328. package/dist/routes/snapshots.d.ts +3 -0
  329. package/dist/routes/snapshots.js +65 -0
  330. package/dist/routes/templates.d.ts +3 -0
  331. package/dist/routes/templates.js +37 -0
  332. package/dist/routes/themes.d.ts +2 -0
  333. package/dist/routes/themes.js +30 -0
  334. package/dist/routes/users.d.ts +2 -0
  335. package/dist/routes/users.js +113 -0
  336. package/dist/sandbox/register-local-plugin.d.ts +36 -0
  337. package/dist/sandbox/register-local-plugin.js +108 -0
  338. package/dist/sandbox/seed-sandbox.d.ts +20 -0
  339. package/dist/sandbox/seed-sandbox.js +122 -0
  340. package/dist/scripts/copy-admin-bundle.d.ts +1 -0
  341. package/dist/scripts/copy-admin-bundle.js +24 -0
  342. package/dist/scripts/copy-docs-bundle.d.ts +1 -0
  343. package/dist/scripts/copy-docs-bundle.js +22 -0
  344. package/dist/scripts/create-api-key.d.ts +5 -0
  345. package/dist/scripts/create-api-key.js +56 -0
  346. package/dist/scripts/generate-prisma-clients.d.ts +6 -0
  347. package/dist/scripts/generate-prisma-clients.js +125 -0
  348. package/dist/scripts/pack-bundled-templates.d.ts +1 -0
  349. package/dist/scripts/pack-bundled-templates.js +87 -0
  350. package/dist/setup/build-connection-string.d.ts +19 -0
  351. package/dist/setup/build-connection-string.js +14 -0
  352. package/dist/setup/bundled-templates.d.ts +19 -0
  353. package/dist/setup/bundled-templates.js +121 -0
  354. package/dist/setup/ensure-prisma-client.d.ts +3 -0
  355. package/dist/setup/ensure-prisma-client.js +23 -0
  356. package/dist/setup/install-starter.d.ts +14 -0
  357. package/dist/setup/install-starter.js +76 -0
  358. package/dist/setup/normalize-database-url.d.ts +2 -0
  359. package/dist/setup/normalize-database-url.js +35 -0
  360. package/dist/setup/persist-env-config.d.ts +2 -0
  361. package/dist/setup/persist-env-config.js +63 -0
  362. package/dist/setup/provision-database.d.ts +2 -0
  363. package/dist/setup/provision-database.js +89 -0
  364. package/dist/setup-app.d.ts +4 -0
  365. package/dist/setup-app.js +43 -0
  366. package/package.json +96 -0
  367. package/prisma/schema.prisma +303 -0
@@ -0,0 +1,413 @@
1
+ <!DOCTYPE html><html lang="en" dir="ltr" data-theme="dark" data-has-toc data-has-sidebar class="astro-ewhz5xxr"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>Build a headless frontend | Velora</title><link rel="shortcut icon" href="/docs/favicon.svg" type="image/svg+xml"/><meta name="generator" content="Astro v7.2.10"/><meta name="generator" content="Starlight v0.41.5"/><meta property="og:title" content="Build a headless frontend"/><meta property="og:type" content="article"/><meta property="og:locale" content="en"/><meta property="og:description" content="Build hello-website — a plain Node site on Velora's headless API, one file, no dependencies — line by line, running against the public demo."/><meta property="og:site_name" content="Velora"/><meta name="twitter:card" content="summary_large_image"/><meta name="description" content="Build hello-website — a plain Node site on Velora's headless API, one file, no dependencies — line by line, running against the public demo."/><script>
2
+ window.StarlightThemeProvider = (() => {
3
+ const storedTheme =
4
+ typeof localStorage !== 'undefined' && localStorage.getItem('starlight-theme');
5
+ const theme =
6
+ storedTheme ||
7
+ (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
8
+ document.documentElement.dataset.theme = theme === 'light' ? 'light' : 'dark';
9
+ return {
10
+ updatePickers(theme = storedTheme || 'auto') {
11
+ document.querySelectorAll('starlight-theme-select').forEach((picker) => {
12
+ const select = picker.querySelector('select');
13
+ if (select) select.value = theme;
14
+ /** @type {HTMLTemplateElement | null} */
15
+ const tmpl = document.querySelector(`#theme-icons`);
16
+ const newIcon = tmpl && tmpl.content.querySelector('.' + theme);
17
+ if (newIcon) {
18
+ const oldIcon = picker.querySelector('svg.label-icon');
19
+ if (oldIcon) {
20
+ oldIcon.replaceChildren(...newIcon.cloneNode(true).childNodes);
21
+ }
22
+ }
23
+ });
24
+ },
25
+ };
26
+ })();
27
+ </script><template id="theme-icons"><svg aria-hidden="true" class="light astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M5 12a1 1 0 0 0-1-1H3a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Zm.64 5-.71.71a1 1 0 0 0 0 1.41 1 1 0 0 0 1.41 0l.71-.71A1 1 0 0 0 5.64 17ZM12 5a1 1 0 0 0 1-1V3a1 1 0 0 0-2 0v1a1 1 0 0 0 1 1Zm5.66 2.34a1 1 0 0 0 .7-.29l.71-.71a1 1 0 1 0-1.41-1.41l-.66.71a1 1 0 0 0 0 1.41 1 1 0 0 0 .66.29Zm-12-.29a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.71-.71a1.004 1.004 0 1 0-1.43 1.41l.73.71ZM21 11h-1a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2Zm-2.64 6A1 1 0 0 0 17 18.36l.71.71a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.76-.66ZM12 6.5a5.5 5.5 0 1 0 5.5 5.5A5.51 5.51 0 0 0 12 6.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0 3.5a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1a1 1 0 0 0-1-1Z"/></svg><svg aria-hidden="true" class="dark astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.64 13a1 1 0 0 0-1.05-.14 8.049 8.049 0 0 1-3.37.73 8.15 8.15 0 0 1-8.14-8.1 8.59 8.59 0 0 1 .25-2A1 1 0 0 0 8 2.36a10.14 10.14 0 1 0 14 11.69 1 1 0 0 0-.36-1.05Zm-9.5 6.69A8.14 8.14 0 0 1 7.08 5.22v.27a10.15 10.15 0 0 0 10.14 10.14 9.784 9.784 0 0 0 2.1-.22 8.11 8.11 0 0 1-7.18 4.32v-.04Z"/></svg><svg aria-hidden="true" class="auto astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg></template><link rel="stylesheet" href="/docs/_astro/print.ehPL0gv-.css" media="print"><link rel="stylesheet" href="/docs/_astro/common.DTXA7sXM.css"><script type="module" src="/docs/_astro/page.Dwipeu-R.js"></script></head><body class="astro-ewhz5xxr"><a class="sl-skip-link astro-zufphfvp" href="#_top">Skip to content</a><div class="page sl-flex astro-5auw3bkx"><header class="header astro-5auw3bkx"><div class="header astro-6ytusalw"><div class="title-wrapper sl-flex astro-6ytusalw"><a href="/docs/" class="site-title sl-flex astro-nu4hftpf"><span class="astro-nu4hftpf" translate="no">Velora</span></a></div><div class="sl-flex print:hidden astro-6ytusalw"><site-search class="astro-6ytusalw astro-zwj6qr56" data-translations="{&quot;placeholder&quot;:&quot;Search&quot;}"><button data-open-modal disabled aria-label="Search" aria-keyshortcuts="Control+K" class="astro-zwj6qr56"><svg aria-hidden="true" class="astro-zwj6qr56 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.71 20.29 18 16.61A9 9 0 1 0 16.61 18l3.68 3.68a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.39ZM11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z"/></svg><span class="sl-hidden md:sl-block astro-zwj6qr56" aria-hidden="true">Search</span><kbd class="sl-hidden md:sl-flex astro-zwj6qr56" style="display: none;"><kbd class="astro-zwj6qr56">Ctrl</kbd><kbd class="astro-zwj6qr56">K</kbd></kbd></button><dialog style="padding:0" aria-label="Search" class="astro-zwj6qr56"><div class="dialog-frame sl-flex astro-zwj6qr56"><button data-close-modal class="sl-flex md:sl-hidden astro-zwj6qr56">Cancel</button><div class="search-container astro-zwj6qr56"><div id="starlight__search" class="astro-zwj6qr56"></div></div></div></dialog></site-search><script>
28
+ (() => {
29
+ const openBtn = document.querySelector('button[data-open-modal]');
30
+ const shortcut = openBtn?.querySelector('kbd');
31
+ if (!openBtn || !(shortcut instanceof HTMLElement)) return;
32
+ const platformKey = shortcut.querySelector('kbd');
33
+ if (platformKey && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
34
+ platformKey.textContent = '⌘';
35
+ openBtn.setAttribute('aria-keyshortcuts', 'Meta+K');
36
+ }
37
+ shortcut.style.display = '';
38
+ })();
39
+ </script><script type="module" src="/docs/_astro/Search.astro_astro_type_script_index_0_lang.CavldbAs.js"></script></div><div class="sl-hidden md:sl-flex print:hidden right-group astro-6ytusalw"><div class="sl-flex social-icons astro-6ytusalw"></div><starlight-theme-select><label style="--sl-select-width: 6.25em" class="astro-7uz3f5wr"><span class="sr-only astro-7uz3f5wr">Select theme</span><svg aria-hidden="true" class="icon label-icon astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg><select autocomplete="off" class="astro-7uz3f5wr"><option value="dark" class="astro-7uz3f5wr">Dark</option><option value="light" class="astro-7uz3f5wr">Light</option><option value="auto" selected class="astro-7uz3f5wr">Auto</option></select><svg aria-hidden="true" class="icon caret astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg></label></starlight-theme-select><script>
40
+ StarlightThemeProvider.updatePickers();
41
+ </script><script type="module">var e=`starlight-theme`,t=e=>e===`auto`||e===`dark`||e===`light`?e:`auto`,n=()=>t(typeof localStorage<`u`&&localStorage.getItem(e));function r(t){typeof localStorage<`u`&&localStorage.setItem(e,t===`light`||t===`dark`?t:``)}var i=()=>matchMedia(`(prefers-color-scheme: light)`).matches?`light`:`dark`;function a(e){StarlightThemeProvider.updatePickers(e),document.documentElement.dataset.theme=e===`auto`?i():e,r(e)}matchMedia(`(prefers-color-scheme: light)`).addEventListener(`change`,()=>{n()===`auto`&&a(`auto`)});var o=class extends HTMLElement{constructor(){super(),a(n()),this.querySelector(`select`)?.addEventListener(`change`,e=>{e.currentTarget instanceof HTMLSelectElement&&a(t(e.currentTarget.value))})}};customElements.define(`starlight-theme-select`,o);</script><script type="module">var e=class extends HTMLElement{constructor(){super();let e=this.querySelector(`select`);e&&(e.addEventListener(`change`,e=>{e.currentTarget instanceof HTMLSelectElement&&(window.location.pathname=e.currentTarget.value)}),window.addEventListener(`pageshow`,t=>{if(!t.persisted)return;let n=e.querySelector(`option[selected]`)?.index;n!==e.selectedIndex&&(e.selectedIndex=n??0)}))}};customElements.define(`starlight-lang-select`,e);</script></div></div></header><nav class="sidebar print:hidden astro-5auw3bkx" aria-label="Main"><starlight-menu-button class="print:hidden astro-gnkm6dw5"><button aria-expanded="false" aria-label="Menu" aria-controls="starlight__sidebar" class="sl-flex md:sl-hidden astro-gnkm6dw5"><svg aria-hidden="true" class="open-menu astro-gnkm6dw5 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M3 8h18a1 1 0 1 0 0-2H3a1 1 0 0 0 0 2Zm18 8H3a1 1 0 0 0 0 2h18a1 1 0 0 0 0-2Zm0-5H3a1 1 0 0 0 0 2h18a1 1 0 0 0 0-2Z"/></svg><svg aria-hidden="true" class="close-menu astro-gnkm6dw5 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="m13.41 12 6.3-6.29a1.004 1.004 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1.004 1.004 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 0 1.42.998.998 0 0 0 1.42 0l6.29-6.3 6.29 6.3a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.42L13.41 12Z"/></svg></button></starlight-menu-button><script type="module">var e=class extends HTMLElement{btn=this.querySelector(`button`);constructor(){super(),this.btn.addEventListener(`click`,()=>this.toggleExpanded());let e=this.closest(`nav`);e&&e.addEventListener(`keyup`,e=>this.closeOnEscape(e)),matchMedia(`(min-width: 50em)`).addEventListener(`change`,()=>this.setExpanded(!1))}setExpanded(e){this.setAttribute(`aria-expanded`,String(e)),document.body.toggleAttribute(`data-mobile-menu-expanded`,e),document.querySelectorAll(`.main-frame, .sl-skip-link`).forEach(t=>{t.toggleAttribute(`inert`,e)})}toggleExpanded(){this.setExpanded(this.getAttribute(`aria-expanded`)!==`true`)}closeOnEscape(e){e.code===`Escape`&&(this.setExpanded(!1),this.btn.focus())}};customElements.define(`starlight-menu-button`,e);</script><div id="starlight__sidebar" class="sidebar-pane astro-5auw3bkx"><div class="sidebar-content sl-flex astro-5auw3bkx"><sl-sidebar-state-persist data-hash="03jp7ll" class="astro-pcs3l5kp"><script aria-hidden="true">
42
+ (() => {
43
+ try {
44
+ if (!matchMedia('(min-width: 50em)').matches) return;
45
+ /** @type {HTMLElement | null} */
46
+ const target = document.querySelector('sl-sidebar-state-persist');
47
+ const state = JSON.parse(sessionStorage.getItem('sl-sidebar-state') || '0');
48
+ if (!target || !state || target.dataset.hash !== state.hash) return;
49
+ window._starlightScrollRestore = state.scroll;
50
+ customElements.define(
51
+ 'sl-sidebar-restore',
52
+ class SidebarRestore extends HTMLElement {
53
+ connectedCallback() {
54
+ try {
55
+ const idx = parseInt(this.dataset.index || '');
56
+ const details = this.closest('details');
57
+ if (details && typeof state.open[idx] === 'boolean') details.open = state.open[idx];
58
+ } catch {}
59
+ }
60
+ }
61
+ );
62
+ } catch {}
63
+ })();
64
+ </script><ul class="top-level astro-ck7l566j"><li class="astro-ck7l566j"><details open class="astro-ck7l566j"><summary class="astro-ck7l566j"><span class="group-label astro-ck7l566j"><span class="large astro-ck7l566j">Using Velora</span></span><svg aria-hidden="true" class="caret astro-ck7l566j astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.25rem;"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg></summary><sl-sidebar-restore data-index="0"></sl-sidebar-restore><ul class="astro-ck7l566j"><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/" class="astro-ck7l566j"><span class="astro-ck7l566j">Using Velora</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/getting-started/" class="astro-ck7l566j"><span class="astro-ck7l566j">Getting started</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/installation/" class="astro-ck7l566j"><span class="astro-ck7l566j">Installation</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/content-model/" class="astro-ck7l566j"><span class="astro-ck7l566j">Document types</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/editing/" class="astro-ck7l566j"><span class="astro-ck7l566j">Content editing</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/media/" class="astro-ck7l566j"><span class="astro-ck7l566j">Media</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/users-and-roles/" class="astro-ck7l566j"><span class="astro-ck7l566j">Users and roles</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/installing-plugins/" class="astro-ck7l566j"><span class="astro-ck7l566j">Installing plugins</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/headless-api/" class="astro-ck7l566j"><span class="astro-ck7l566j">Headless API</span></a></li><li class="astro-ck7l566j"><a href="/docs/v1/using-velora/build-a-headless-frontend/" aria-current="page" class="astro-ck7l566j"><span class="astro-ck7l566j">Build a headless frontend</span></a></li></ul></details></li></ul><script aria-hidden="true">
65
+ (() => {
66
+ const scroller = document.getElementById('starlight__sidebar');
67
+ if (!window._starlightScrollRestore || !scroller) return;
68
+ scroller.scrollTop = window._starlightScrollRestore;
69
+ delete window._starlightScrollRestore;
70
+ })();
71
+ </script></sl-sidebar-state-persist><div class="md:sl-hidden"><div class="mobile-preferences sl-flex astro-yfet6llj"><div class="social-icons astro-yfet6llj"></div><starlight-theme-select><label style="--sl-select-width: 6.25em" class="astro-7uz3f5wr"><span class="sr-only astro-7uz3f5wr">Select theme</span><svg aria-hidden="true" class="icon label-icon astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg><select autocomplete="off" class="astro-7uz3f5wr"><option value="dark" class="astro-7uz3f5wr">Dark</option><option value="light" class="astro-7uz3f5wr">Light</option><option value="auto" selected class="astro-7uz3f5wr">Auto</option></select><svg aria-hidden="true" class="icon caret astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg></label></starlight-theme-select><script>
72
+ StarlightThemeProvider.updatePickers();
73
+ </script></div></div></div></div></nav><div class="main-frame astro-5auw3bkx"><script type="module">var e=document.getElementById(`starlight__sidebar`),t=e?.querySelector(`sl-sidebar-state-persist`),n=`sl-sidebar-state`,r=()=>{let r=[],i=t?.dataset.hash||``;try{let e=sessionStorage.getItem(n),t=JSON.parse(e||`{}`);Array.isArray(t.open)&&t.hash===i&&(r=t.open)}catch{}return{hash:i,open:r,scroll:e?.scrollTop||0}},i=e=>{try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}},a=()=>i(r()),o=(e,t)=>{let n=r();n.open[t]=e,i(n)};t?.addEventListener(`click`,e=>{if(!(e.target instanceof Element))return;let t=e.target.closest(`summary`)?.closest(`details`);if(!t)return;let n=t.querySelector(`sl-sidebar-restore`),r=parseInt(n?.dataset.index||``);isNaN(r)||o(!t.open,r)}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&a()});</script><div class="lg:sl-flex astro-perbdhd2"><aside class="right-sidebar-container print:hidden astro-perbdhd2"><div class="right-sidebar astro-perbdhd2"><div class="lg:sl-hidden astro-epm5tkrc"><mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-bnvebtr4"><nav aria-labelledby="starlight__on-this-page--mobile" class="astro-bnvebtr4"><details id="starlight__mobile-toc" class="astro-bnvebtr4"><summary id="starlight__on-this-page--mobile" class="sl-flex astro-bnvebtr4"><span class="toggle sl-flex astro-bnvebtr4">On this page<svg aria-hidden="true" class="caret astro-bnvebtr4 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1rem;"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg></span><span class="display-current astro-bnvebtr4"></span></summary><div class="dropdown astro-bnvebtr4"><ul class="isMobile astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#run-it-first" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Run it first</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-the-site-reads" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What the site reads</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#1-load-env-without-a-dependency" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">1. Load .env without a dependency</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#2-one-function-for-the-whole-api-client" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">2. One function for the whole API client</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#3-html-helpers" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">3. HTML helpers</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#4-the-home-page-two-requests-one-grouped-list" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">4. The home page: two requests, one grouped list</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#5-the-item-page-fields-from-one-call" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">5. The item page: fields from one call</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#6-the-server" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">6. The server</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-it-renders" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What it renders</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#point-it-at-your-own-instance" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Point it at your own instance</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#make-it-say-hello-world" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Make it say hello world</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#where-to-go-next" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Where to go next</span></a></li></ul></div></details></nav></mobile-starlight-toc><script type="module" src="/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js"></script></div><div class="right-sidebar-panel sl-hidden lg:sl-block astro-epm5tkrc"><div class="sl-container astro-epm5tkrc"><starlight-toc data-min-h="2" data-max-h="3"><nav aria-labelledby="starlight__on-this-page"><h2 id="starlight__on-this-page">On this page</h2><ul class="astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#run-it-first" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Run it first</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-the-site-reads" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What the site reads</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#1-load-env-without-a-dependency" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">1. Load .env without a dependency</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#2-one-function-for-the-whole-api-client" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">2. One function for the whole API client</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#3-html-helpers" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">3. HTML helpers</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#4-the-home-page-two-requests-one-grouped-list" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">4. The home page: two requests, one grouped list</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#5-the-item-page-fields-from-one-call" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">5. The item page: fields from one call</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#6-the-server" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">6. The server</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-it-renders" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What it renders</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#point-it-at-your-own-instance" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Point it at your own instance</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#make-it-say-hello-world" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Make it say hello world</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#where-to-go-next" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Where to go next</span></a></li></ul></nav></starlight-toc><script type="module" src="/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js"></script></div></div></div></aside><div class="main-pane astro-perbdhd2"><main data-pagefind-body class="astro-ewhz5xxr" lang="en" dir="ltr"><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><h1 id="_top" class="astro-oef2efwn">Build a headless frontend</h1></div></div><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><div class="sl-markdown-content"><p>This tutorial builds a working website on top of Velora’s <a href="/docs/v1/using-velora/headless-api/">headless
74
+ API</a>: one file, plain Node, no framework
75
+ and no npm dependencies. It fetches content <strong>server-side</strong>, so the API
76
+ key never reaches a browser, lists every published item grouped by
77
+ document type, and renders any item’s fields when you click it.</p>
78
+ <p>The finished file is <code dir="auto">examples/hello-website/server.js</code> in the public
79
+ starter repository,
80
+ <a href="https://github.com/velora-cms/velora-starter">velora-cms/velora-starter</a>.
81
+ Every code block below is a slice of that same file, in file order — paste
82
+ them one after another and you have it. You can also clone the repository
83
+ and read ahead at any point.</p>
84
+ <p>It runs against the public demo instance, so you need no Velora of your
85
+ own to finish it. Node 18 or newer is the only requirement (the code uses
86
+ the built-in <code dir="auto">fetch</code>).</p>
87
+ <div class="sl-heading-wrapper level-h2"><h2 id="run-it-first">Run it first</h2><a class="sl-anchor-link" href="#run-it-first"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Run it first”</span></a></div>
88
+ <p>Cloning the starter and running the example takes about a minute and gives
89
+ you the finished thing to compare against:</p>
90
+ <div class="expressive-code"><link rel="stylesheet" href="/docs/_astro/ec.w36nc.css"><script type="module" src="/docs/_astro/ec.0vx5m.js"></script><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">git</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">clone</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">--depth</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#F78C6C;--1:#AA0982">1</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">https://github.com/velora-cms/velora-starter</span></div></div><div class="ec-line"><div class="code"><span style="--0:#C5E478;--1:#3B61B0">cd</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">velora-starter/examples/hello-website</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="git clone --depth 1 https://github.com/velora-cms/velora-startercd velora-starter/examples/hello-website"><div></div></button></div></figure></div>
91
+ <p>You now have four files: <code dir="auto">server.js</code> (the whole site), <code dir="auto">package.json</code>
92
+ (name and a <code dir="auto">start</code> script — no dependencies), <code dir="auto">.env.example</code>, and a
93
+ <code dir="auto">README.md</code> covering the same ground as this tutorial.</p>
94
+ <p>The site reads its configuration from <code dir="auto">.env</code>. Create one pointing at the
95
+ public demo:</p>
96
+ <div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">cat</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">></span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">.env</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">&#x3C;&#x3C;</span><span style="--0:#D9F5DD;--1:#111111">'ENV'</span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#984E4D">VELORA_API_URL=https://demo.velora-cms.com</span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#984E4D">VELORA_API_KEY=vk_4ac0a6b78b953a13b8646e6379fcbb6bee1a269af253a14eebce668572019328</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D9F5DD;--1:#111111">ENV</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="cat > .env <<&#x27;ENV&#x27;VELORA_API_URL=https://demo.velora-cms.comVELORA_API_KEY=vk_4ac0a6b78b953a13b8646e6379fcbb6bee1a269af253a14eebce668572019328ENV"><div></div></button></div></figure></div>
97
+ <p>That key is intentionally public: it belongs to the demo instance, it is
98
+ rate-limited, and it is rotated whenever the demo is reseeded. (It is a
99
+ normal API key — see <a href="/docs/v1/using-velora/headless-api/#what-a-key-can-do">What a key can
100
+ do</a> — created with the
101
+ default read scope, so Velora itself refuses writes with it; the demo’s
102
+ reverse proxy also rejects every non-<code dir="auto">GET</code> request except GraphQL
103
+ queries before Velora sees them, as a second layer.)</p>
104
+ <p>Start the server:</p>
105
+ <div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">node</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">server.js</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="node server.js"><div></div></button></div></figure></div>
106
+ <p>It prints one line and stays running:</p>
107
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="plaintext"><code><div class="ec-line"><div class="code"><span style="--0:#d6deeb;--1:#403f53">Hello-website running at http://localhost:4600</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="Hello-website running at http://localhost:4600"><div></div></button></div></figure></div>
108
+ <p>Open that URL and you will see the demo’s content — a heading per document
109
+ type, and every published item beneath it. The rest of this page explains
110
+ how the file gets there, block by block.</p>
111
+ <div class="sl-heading-wrapper level-h2"><h2 id="what-the-site-reads">What the site reads</h2><a class="sl-anchor-link" href="#what-the-site-reads"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “What the site reads”</span></a></div>
112
+ <p>Three environment variables, all read at startup:</p>
113
+
114
+
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+
123
+
124
+
125
+
126
+
127
+
128
+
129
+
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+ <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">VELORA_API_URL</code></td><td>string (URL)</td><td>Origin of the Velora instance. Every request is resolved against it as <code dir="auto">&#x3C;origin>/api/v1/…</code>. No default — the site refuses to render without it.</td><td><code dir="auto">https://demo.velora-cms.com</code></td></tr><tr><td><code dir="auto">VELORA_API_KEY</code></td><td>string</td><td>An API key, sent as <code dir="auto">Authorization: Bearer &#x3C;key></code>. Minted in the admin under <strong>Settings → API Keys</strong>. No default.</td><td><code dir="auto">vk_4ac0a6b78b953a13b8646e6379fcbb6bee1a269af253a14eebce668572019328</code></td></tr><tr><td><code dir="auto">PORT</code></td><td>integer</td><td>Port this site listens on. Default <code dir="auto">4600</code>.</td><td><code dir="auto">4600</code></td></tr></tbody></table>
143
+ <p>Real shell exports win over the <code dir="auto">.env</code> file, so
144
+ <code dir="auto">PORT=5000 node server.js</code> overrides the file without editing it.</p>
145
+ <div class="sl-heading-wrapper level-h2"><h2 id="1-load-env-without-a-dependency">1. Load <code dir="auto">.env</code> without a dependency</h2><a class="sl-anchor-link" href="#1-load-env-without-a-dependency"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “1. Load .env without a dependency”</span></a></div>
146
+ <p>The file opens by pulling <code dir="auto">KEY=value</code> pairs out of <code dir="auto">.env</code> into
147
+ <code dir="auto">process.env</code>. Ten lines replace a dotenv dependency, and the <code dir="auto">!(m[1] in process.env)</code> check is what makes a real shell export take precedence:</p>
148
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">http</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#82AAFF;--1:#3B61B0">require</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">node:http</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">fs</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#82AAFF;--1:#3B61B0">require</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">node:fs</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">path</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#82AAFF;--1:#3B61B0">require</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">node:path</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code">
149
+ </div></div><div class="ec-line"><div class="code"><span style="--0:#919F9F;--1:#5D6376">// --- tiny .env loader (real shell exports win over the file) ------------</span></div></div><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">try</span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">lines</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#7FDBCA;--1:#096E72">fs</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">readFileSync</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">path</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">join</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">__dirname</span></span><span style="--0:#C792EA;--1:#8844AE">, </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">.env</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#C792EA;--1:#8844AE">, </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">utf8</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">split</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">/</span><span style="--0:#82AAFF;--1:#3B61B0">\r</span><span style="--0:#7FDBCA;--1:#096E72">?</span><span style="--0:#82AAFF;--1:#3B61B0">\n</span><span style="--0:#D9F5DD;--1:#111111">/</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">for</span><span style="--0:#D6DEEB;--1:#403F53"> (</span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">line</span><span style="--0:#C792EA;--1:#8844AE"> </span><span style="--0:#7FDBCA;--1:#096E72">of</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> </span><span style="--0:#D7DBE0">lines</span><span style="--0:#D6DEEB">) {</span></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">m</span><span style="--0:#C792EA;--1:#8844AE"> =</span><span style="--0:#5CA7E4;--1:#3A688F"> </span><span style="--0:#D9F5DD;--1:#111111">/</span><span style="--0:#C792EA;--1:#8844AE">^</span><span style="--0:#82AAFF;--1:#3B61B0">\s</span><span style="--0:#7FDBCA;--1:#096E72">*</span><span style="--0:#5CA7E4;--1:#3A688F">(</span><span style="--0:#82AAFF;--1:#3B61B0">[A-Z0-9_]</span><span style="--0:#7FDBCA;--1:#096E72">+</span><span style="--0:#5CA7E4;--1:#3A688F">)</span><span style="--0:#82AAFF;--1:#3B61B0">\s</span><span style="--0:#7FDBCA;--1:#096E72">*</span><span style="--0:#5CA7E4;--1:#3A688F">=</span><span style="--0:#82AAFF;--1:#3B61B0">\s</span><span style="--0:#7FDBCA;--1:#096E72">*</span><span style="--0:#5CA7E4;--1:#3A688F">(</span><span style="--0:#82AAFF;--1:#3B61B0">.</span><span style="--0:#7FDBCA;--1:#096E72">*</span><span style="--0:#5CA7E4;--1:#3A688F">)</span><span style="--0:#82AAFF;--1:#3B61B0">\s</span><span style="--0:#7FDBCA;--1:#096E72">*</span><span style="--0:#C792EA;--1:#8844AE">$</span><span style="--0:#D9F5DD;--1:#111111">/</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">exec</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">line</span><span style="--0:#D6DEEB">);</span></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">if</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> (</span><span style="--0:#D7DBE0">m</span><span style="--0:#D6DEEB"> </span></span><span style="--0:#C792EA;--1:#8844AE">&#x26;&#x26;</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">!</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">m</span><span style="--0:#D6DEEB">[</span></span><span style="--0:#F78C6C;--1:#AA0982">1</span><span style="--0:#D6DEEB;--1:#403F53">] </span><span style="--0:#7FDBCA;--1:#096E72">in</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">process</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">env</span><span style="--0:#D6DEEB;--1:#403F53">)) </span><span style="--0:#7FDBCA;--1:#096E72">process</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">env</span><span style="--1:#403F53"><span style="--0:#D6DEEB">[</span><span style="--0:#D7DBE0">m</span><span style="--0:#D6DEEB">[</span></span><span style="--0:#F78C6C;--1:#AA0982">1</span><span style="--0:#D6DEEB;--1:#403F53">]] </span><span style="--0:#C792EA;--1:#8844AE">=</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> </span><span style="--0:#D7DBE0">m</span><span style="--0:#D6DEEB">[</span></span><span style="--0:#F78C6C;--1:#AA0982">2</span><span style="--0:#D6DEEB;--1:#403F53">];</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">} </span><span style="--0:#C792EA;--1:#8844AE">catch</span><span style="--0:#D6DEEB;--1:#403F53"> { </span><span style="--0:#919F9F;--1:#5F636F">/* no .env file — env vars only */</span><span style="--0:#D6DEEB;--1:#403F53"> }</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="const http = require(&#x22;node:http&#x22;);const fs = require(&#x22;node:fs&#x22;);const path = require(&#x22;node:path&#x22;);// --- tiny .env loader (real shell exports win over the file) ------------try { const lines = fs.readFileSync(path.join(__dirname, &#x22;.env&#x22;), &#x22;utf8&#x22;).split(/\r?\n/); for (const line of lines) { const m = /^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/.exec(line); if (m &#x26;&#x26; !(m[1] in process.env)) process.env[m[1]] = m[2]; }} catch { /* no .env file — env vars only */ }"><div></div></button></div></figure></div>
150
+ <p>A missing <code dir="auto">.env</code> is swallowed on purpose: the site still starts when
151
+ configuration comes from the environment instead, which is how you would
152
+ run it on a host that injects secrets rather than mounting files.</p>
153
+ <p>Next, the three values become constants:</p>
154
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">API_URL</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#7FDBCA;--1:#096E72">process</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#FAF39F;--1:#111111">env</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">VELORA_API_URL</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">API_KEY</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#7FDBCA;--1:#096E72">process</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#FAF39F;--1:#111111">env</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">VELORA_API_KEY</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">PORT</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#82AAFF;--1:#3B61B0">Number</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">process</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#FAF39F;--1:#111111">env</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">PORT</span><span style="--0:#C792EA;--1:#8844AE"> || </span><span style="--0:#F78C6C;--1:#AA0982">4600</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="const API_URL = process.env.VELORA_API_URL;const API_KEY = process.env.VELORA_API_KEY;const PORT = Number(process.env.PORT || 4600);"><div></div></button></div></figure></div>
155
+ <p><code dir="auto">API_URL</code> and <code dir="auto">API_KEY</code> are deliberately left undefined when unset rather
156
+ than defaulted — the request handler in step 6 turns that into a readable
157
+ error page instead of a confusing 401 from a server you did not mean to
158
+ call.</p>
159
+ <div class="sl-heading-wrapper level-h2"><h2 id="2-one-function-for-the-whole-api-client">2. One function for the whole API client</h2><a class="sl-anchor-link" href="#2-one-function-for-the-whole-api-client"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “2. One function for the whole API client”</span></a></div>
160
+ <p>Talking to Velora needs no SDK. Every route takes the same bearer header
161
+ and answers JSON, so the entire client is six lines:</p>
162
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">async</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">function</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">velora</span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--0:#D7DBE0;--1:#403F53">apiPath</span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">res</span><span style="--0:#C792EA;--1:#8844AE"> = await </span><span style="--0:#82AAFF;--1:#3B61B0">fetch</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">new</span><span style="--0:#C792EA;--1:#8844AE"> </span><span style="--0:#82AAFF;--1:#3B61B0">URL</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">apiPath</span></span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--0:#C792EA;--1:#8844AE"> </span><span style="--0:#82AAFF;--1:#3B61B0">API_URL</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--0:#C792EA;--1:#8844AE"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#C792EA;--1:#8844AE"> </span></span><span style="--0:#C792EA;--1:#8844AE">headers: { authorization: </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">Bearer </span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">API_KEY</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#C792EA;--1:#8844AE"> }</span><span style="--0:#889FB2;--1:#4D667B">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#C792EA;--1:#8844AE"> </span></span><span style="--0:#C792EA;--1:#8844AE">}</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">if</span><span style="--0:#D6DEEB;--1:#403F53"> (</span><span style="--0:#C792EA;--1:#8844AE">!</span><span style="--0:#7FDBCA;--1:#096E72">res</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">ok</span><span style="--0:#D6DEEB;--1:#403F53">) </span><span style="--0:#C792EA;--1:#8844AE">throw</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">new</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">Error</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#D7DBE0;--1:#403F53">apiPath</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0"> -> HTTP </span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#7FDBCA;--1:#096E72">res</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">status</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">return</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">res</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">json</span><span style="--0:#D6DEEB;--1:#403F53">();</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="async function velora(apiPath) { const res = await fetch(new URL(apiPath, API_URL), { headers: { authorization: &#x60;Bearer ${API_KEY}&#x60; }, }); if (!res.ok) throw new Error(&#x60;${apiPath} -> HTTP ${res.status}&#x60;); return res.json();}"><div></div></button></div></figure></div>
163
+ <p><code dir="auto">new URL(apiPath, API_URL)</code> joins the path to the configured origin, so
164
+ callers below write <code dir="auto">/api/v1/content</code> and never string-concatenate. A
165
+ non-2xx response throws with its status attached — a <code dir="auto">401</code> here means the
166
+ key is missing, wrong or revoked; a <code dir="auto">404</code> on <code dir="auto">/api/v1/…</code> usually means the
167
+ instance was not started with <code dir="auto">VELORA_HEADLESS=true</code>, so the routes do not
168
+ exist at all.</p>
169
+ <p>Because this runs in Node, <code dir="auto">API_KEY</code> stays on the server. Nothing in the
170
+ HTML this site emits contains the key.</p>
171
+ <div class="sl-heading-wrapper level-h2"><h2 id="3-html-helpers">3. HTML helpers</h2><a class="sl-anchor-link" href="#3-html-helpers"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “3. HTML helpers”</span></a></div>
172
+ <p>Three small helpers do all the rendering. <code dir="auto">esc</code> escapes every value that
173
+ came from the API — content is data, and data goes through escaping before
174
+ it becomes markup:</p>
175
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--0:#D7DBE0;--1:#403F53">s</span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#C792EA;--1:#8844AE"> =></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">String</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">s</span><span style="--0:#D6DEEB">)</span></span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">replace</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">/</span><span style="--0:#82AAFF;--1:#3B61B0">[&#x26;&#x3C;>"']</span><span style="--0:#D9F5DD;--1:#111111">/</span><span style="--0:#5CA7E4;--1:#3A688F">g</span><span style="--0:#C792EA;--1:#8844AE">, </span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--0:#D7DBE0;--1:#403F53">c</span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#C792EA;--1:#8844AE"> => </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x26;#</span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#7FDBCA;--1:#096E72">c</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">charCodeAt</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#F78C6C;--1:#AA0982">0</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">;</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code">
176
+ </div></div><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">function</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">page</span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--1:#403F53"><span style="--0:#D7DBE0">title</span><span style="--0:#D6DEEB">, </span><span style="--0:#D7DBE0">body</span></span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">return</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;!doctype html>&#x3C;meta charset="utf-8"></span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;meta name="viewport" content="width=device-width, initial-scale=1"></span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;title></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">title</span><span style="--0:#D6DEEB">)</span></span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/title></span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;style></span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">body { font-family: system-ui, sans-serif; max-width: 42rem;</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">margin-inline: auto; padding: 2rem 1rem; line-height: 1.6; }</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">h1 { border-block-end: 2px solid #ddd; padding-block-end: .5rem; }</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">a { color: #0369a1; }</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">dt { font-weight: 600; margin-block-start: 1rem; }</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">dd { margin-inline-start: 0; }</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">pre { background: #f5f5f4; padding: 1rem; overflow-x: auto; }</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">small { color: #78716c; }</span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/style></span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;body></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#D7DBE0;--1:#403F53">body</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/body></span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code">
177
+ </div></div><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">function</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">renderValue</span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--0:#D7DBE0;--1:#403F53">v</span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">if</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> (</span><span style="--0:#D7DBE0">v</span><span style="--0:#D6DEEB"> </span></span><span style="--0:#C792EA;--1:#8844AE">===</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#FF6A83;--1:#A24848">null</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">||</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> </span><span style="--0:#D7DBE0">v</span><span style="--0:#D6DEEB"> </span></span><span style="--0:#C792EA;--1:#8844AE">===</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">undefined</span><span style="--0:#D6DEEB;--1:#403F53">) </span><span style="--0:#C792EA;--1:#8844AE">return</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">&#x3C;small>(empty)&#x3C;/small></span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">if</span><span style="--0:#D6DEEB;--1:#403F53"> (</span><span style="--0:#7FDBCA;--1:#096E72">typeof</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> </span><span style="--0:#D7DBE0">v</span><span style="--0:#D6DEEB"> </span></span><span style="--0:#C792EA;--1:#8844AE">===</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">object</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">) </span><span style="--0:#C792EA;--1:#8844AE">return</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;pre></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#82AAFF;--1:#3B61B0">JSON</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">stringify</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">v</span></span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--0:#E2817F;--1:#B23834"> </span><span style="--0:#FF6A83;--1:#A24848">null</span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--0:#E2817F;--1:#B23834"> </span><span style="--0:#F78C6C;--1:#AA0982">2</span><span style="--0:#D6DEEB;--1:#403F53">))</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/pre></span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">return</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">v</span><span style="--0:#D6DEEB">);</span></span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="const esc = (s) => String(s).replace(/[&#x26;<>&#x22;&#x27;]/g, (c) => &#x60;&#x26;#${c.charCodeAt(0)};&#x60;);function page(title, body) { return &#x60;<!doctype html><meta charset=&#x22;utf-8&#x22;><meta name=&#x22;viewport&#x22; content=&#x22;width=device-width, initial-scale=1&#x22;><title>${esc(title)}</title><style> body { font-family: system-ui, sans-serif; max-width: 42rem; margin-inline: auto; padding: 2rem 1rem; line-height: 1.6; } h1 { border-block-end: 2px solid #ddd; padding-block-end: .5rem; } a { color: #0369a1; } dt { font-weight: 600; margin-block-start: 1rem; } dd { margin-inline-start: 0; } pre { background: #f5f5f4; padding: 1rem; overflow-x: auto; } small { color: #78716c; }</style><body>${body}</body>&#x60;;}function renderValue(v) { if (v === null || v === undefined) return &#x22;<small>(empty)</small>&#x22;; if (typeof v === &#x22;object&#x22;) return &#x60;<pre>${esc(JSON.stringify(v, null, 2))}</pre>&#x60;; return esc(v);}"><div></div></button></div></figure></div>
178
+ <p><code dir="auto">page</code> wraps a body in a complete document with a small stylesheet — note
179
+ <code dir="auto">margin-inline</code>, <code dir="auto">padding-block-end</code> and <code dir="auto">margin-inline-start</code> rather than
180
+ their left/right equivalents, so the layout mirrors correctly in a
181
+ right-to-left locale. <code dir="auto">renderValue</code> is the honest part: a field’s value can
182
+ be a string, a number, or a nested object (rich text arrives as a document
183
+ object, not HTML), so anything structural is pretty-printed as JSON rather
184
+ than guessed at.</p>
185
+ <div class="sl-heading-wrapper level-h2"><h2 id="4-the-home-page-two-requests-one-grouped-list">4. The home page: two requests, one grouped list</h2><a class="sl-anchor-link" href="#4-the-home-page-two-requests-one-grouped-list"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “4. The home page: two requests, one grouped list”</span></a></div>
186
+ <p>The home page needs two things — the list of published content, and the
187
+ names of the document types those items belong to, since content rows
188
+ carry only a <code dir="auto">documentTypeId</code>. Both are fetched in parallel:</p>
189
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">async</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">function</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">home</span><span style="--0:#D9F5DD;--1:#111111">()</span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const [</span><span style="--0:#82AAFF;--1:#3B61B0">types</span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--0:#C792EA;--1:#8844AE"> </span><span style="--0:#82AAFF;--1:#3B61B0">content</span><span style="--0:#C792EA;--1:#8844AE">] = await </span><span style="--0:#C5E478;--1:#3B61B0">Promise</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">all</span><span style="--0:#D6DEEB;--1:#403F53">([</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">velora</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/api/v1/document-types?limit=100</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#889FB2;--1:#4D667B">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">velora</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/api/v1/content?limit=100</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#889FB2;--1:#4D667B">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">]);</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">typeName</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#7FDBCA;--1:#096E72">new</span><span style="--0:#C792EA;--1:#8844AE"> </span><span style="--0:#82AAFF;--1:#3B61B0">Map</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">types</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#FAF39F;--1:#111111">items</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">map</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--0:#D7DBE0;--1:#403F53">t</span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#C792EA;--1:#8844AE"> =></span><span style="--0:#D6DEEB;--1:#403F53"> [</span><span style="--0:#7FDBCA;--1:#096E72">t</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">id</span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">t</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">name</span><span style="--0:#D6DEEB;--1:#403F53">]));</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">groups</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#7FDBCA;--1:#096E72">new</span><span style="--0:#C792EA;--1:#8844AE"> </span><span style="--0:#82AAFF;--1:#3B61B0">Map</span><span style="--0:#D6DEEB;--1:#403F53">();</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">for</span><span style="--0:#D6DEEB;--1:#403F53"> (</span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">item</span><span style="--0:#C792EA;--1:#8844AE"> </span><span style="--0:#7FDBCA;--1:#096E72">of</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">content</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">items</span><span style="--0:#D6DEEB;--1:#403F53">) {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">name</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#7FDBCA;--1:#096E72">typeName</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">get</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">item</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">documentTypeId</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#C792EA;--1:#8844AE"> || </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">Unknown type</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">if</span><span style="--0:#D6DEEB;--1:#403F53"> (</span><span style="--0:#C792EA;--1:#8844AE">!</span><span style="--0:#7FDBCA;--1:#096E72">groups</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">has</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">name</span><span style="--0:#D6DEEB">)) </span></span><span style="--0:#7FDBCA;--1:#096E72">groups</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">set</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">name</span></span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--0:#D6DEEB;--1:#403F53"> []);</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">groups</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">get</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">name</span><span style="--0:#D6DEEB">)</span></span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">push</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">item</span><span style="--0:#D6DEEB">);</span></span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="async function home() { const [types, content] = await Promise.all([ velora(&#x22;/api/v1/document-types?limit=100&#x22;), velora(&#x22;/api/v1/content?limit=100&#x22;), ]); const typeName = new Map(types.items.map((t) => [t.id, t.name])); const groups = new Map(); for (const item of content.items) { const name = typeName.get(item.documentTypeId) || &#x22;Unknown type&#x22;; if (!groups.has(name)) groups.set(name, []); groups.get(name).push(item); }"><div></div></button></div></figure></div>
190
+ <p>After those two calls, <code dir="auto">groups</code> maps a type name like <code dir="auto">Post</code> to the items
191
+ of that type. The two responses are worth knowing by shape:</p>
192
+
193
+
194
+
195
+
196
+
197
+
198
+
199
+
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+
209
+
210
+
211
+
212
+
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+
221
+
222
+
223
+
224
+
225
+
226
+
227
+
228
+
229
+
230
+
231
+
232
+
233
+
234
+
235
+
236
+
237
+
238
+
239
+
240
+
241
+
242
+
243
+
244
+
245
+
246
+
247
+
248
+
249
+
250
+
251
+
252
+
253
+
254
+
255
+
256
+
257
+ <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">GET /api/v1/document-types</code></td><td>request</td><td>Every document type, with its fields.</td><td><code dir="auto">/api/v1/document-types?limit=100</code></td></tr><tr><td><code dir="auto">GET /api/v1/content</code></td><td>request</td><td>Every <strong>published</strong> node, in tree order, without field values.</td><td><code dir="auto">/api/v1/content?limit=100</code></td></tr><tr><td><code dir="auto">items</code></td><td>array</td><td>The page of rows for either call.</td><td><code dir="auto">[{ "id": "…", "name": "Post" }]</code></td></tr><tr><td><code dir="auto">nextCursor</code></td><td>string (uuid) or null</td><td>Cursor for the next page — <code dir="auto">null</code> when this was the last one.</td><td><code dir="auto">null</code></td></tr><tr><td><code dir="auto">items[].id</code></td><td>string (uuid)</td><td>Type id on the first call, node id on the second.</td><td><code dir="auto">f0491ccf-0511-43a7-862b-fdf9dce55f3c</code></td></tr><tr><td><code dir="auto">items[].name</code></td><td>string</td><td>Document types only: the display name this page groups by.</td><td><code dir="auto">Post</code></td></tr><tr><td><code dir="auto">items[].documentTypeId</code></td><td>string (uuid)</td><td>Content rows only: which type the node is. Joined against the map above.</td><td><code dir="auto">f0491ccf-0511-43a7-862b-fdf9dce55f3c</code></td></tr><tr><td><code dir="auto">items[].path</code></td><td>string</td><td>Content rows only: the node’s place in the tree, used here as its label.</td><td><code dir="auto">/jane-doe/hello-velora</code></td></tr><tr><td><code dir="auto">items[].locale</code></td><td>string</td><td>Content rows only: which translation the row is.</td><td><code dir="auto">en</code></td></tr></tbody></table>
258
+ <p>Both calls pass <code dir="auto">limit=100</code>, the practical maximum (the API’s ceiling is
259
+ 100 per page). A bigger site pages with <code dir="auto">nextCursor</code> — see <a href="/docs/v1/using-velora/headless-api/#cursor-pagination">Cursor
260
+ pagination</a>; this example
261
+ deliberately stops at one page to stay short.</p>
262
+ <p>The rest of <code dir="auto">home()</code> turns those groups into markup:</p>
263
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">let </span><span style="--0:#D7DBE0;--1:#403F53">body</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;h1>Hello, Velora&#x3C;/h1></span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;p>Everything below is live, published content served by the headless</span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">API at &#x3C;code></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#82AAFF;--1:#3B61B0">API_URL</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/code>. Publish something in the admin and</span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">refresh — no rebuild.&#x3C;/p></span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">for</span><span style="--0:#D6DEEB;--1:#403F53"> (</span><span style="--0:#C792EA;--1:#8844AE">const [</span><span style="--0:#82AAFF;--1:#3B61B0">name</span><span style="--0:#C792EA;--1:#8844AE">, </span><span style="--0:#82AAFF;--1:#3B61B0">items</span><span style="--0:#C792EA;--1:#8844AE">] </span><span style="--0:#7FDBCA;--1:#096E72">of</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> </span><span style="--0:#D7DBE0">groups</span><span style="--0:#D6DEEB">) {</span></span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--1:#403F53"> </span></span><span style="--0:#D7DBE0;--1:#403F53">body</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">+=</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;h2></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">name</span><span style="--0:#D6DEEB">)</span></span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/h2>&#x3C;ul></span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">+</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">items</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">map</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--0:#D7DBE0;--1:#403F53">i</span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">=></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;li>&#x3C;a href="/item?id=</span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">i</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">id</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">"></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">i</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">path</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/a></span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#ECC48D;--1:#3B61B0"> </span></span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;small>(</span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">i</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">locale</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">)&#x3C;/small>&#x3C;/li></span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">join</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">""</span><span style="--0:#D6DEEB;--1:#403F53">) </span><span style="--0:#C792EA;--1:#8844AE">+</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">&#x3C;/ul></span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">if</span><span style="--0:#D6DEEB;--1:#403F53"> (</span><span style="--0:#7FDBCA;--1:#096E72">content</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#FAF39F;--1:#111111">items</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">length</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">===</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#F78C6C;--1:#AA0982">0</span><span style="--0:#D6DEEB;--1:#403F53">) {</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--1:#403F53"> </span></span><span style="--0:#D7DBE0;--1:#403F53">body</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">+=</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">&#x3C;p>No published content yet — create and publish an item in the Velora admin, then refresh.&#x3C;/p></span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">;</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">return</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">page</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">Hello, Velora</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--1:#403F53"><span style="--0:#D6DEEB">, </span><span style="--0:#D7DBE0">body</span><span style="--0:#D6DEEB">);</span></span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code=" let body = &#x60;<h1>Hello, Velora</h1><p>Everything below is live, published content served by the headlessAPI at <code>${esc(API_URL)}</code>. Publish something in the admin andrefresh — no rebuild.</p>&#x60;; for (const [name, items] of groups) { body += &#x60;<h2>${esc(name)}</h2><ul>&#x60; + items.map((i) => &#x60;<li><a href=&#x22;/item?id=${esc(i.id)}&#x22;>${esc(i.path)}</a> <small>(${esc(i.locale)})</small></li>&#x60;).join(&#x22;&#x22;) + &#x22;</ul>&#x22;; } if (content.items.length === 0) { body += &#x22;<p>No published content yet — create and publish an item in the Velora admin, then refresh.</p>&#x22;; } return page(&#x22;Hello, Velora&#x22;, body);}"><div></div></button></div></figure></div>
264
+ <p>Each item links to <code dir="auto">/item?id=&#x3C;id></code>, which is the second route. The
265
+ empty-state branch matters more than it looks: a fresh instance has no
266
+ published content, and “nothing here yet” is a much better first
267
+ impression than a blank page.</p>
268
+ <div class="sl-heading-wrapper level-h2"><h2 id="5-the-item-page-fields-from-one-call">5. The item page: fields from one call</h2><a class="sl-anchor-link" href="#5-the-item-page-fields-from-one-call"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “5. The item page: fields from one call”</span></a></div>
269
+ <p>Clicking through fetches the full item — the one call that returns field
270
+ values:</p>
271
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span style="--0:#C792EA;--1:#8844AE">async</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">function</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">item</span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--0:#D7DBE0;--1:#403F53">id</span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">it</span><span style="--0:#C792EA;--1:#8844AE"> = await </span><span style="--0:#82AAFF;--1:#3B61B0">velora</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">/api/v1/content/</span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">encodeURIComponent</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">id</span><span style="--0:#D6DEEB">)</span></span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">fields</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#7FDBCA;--1:#096E72">Object</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">entries</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">it</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">data</span><span style="--0:#D6DEEB;--1:#403F53">)</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">map</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--1:#403F53"><span style="--0:#D6DEEB">[</span><span style="--0:#D7DBE0">k</span></span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> </span><span style="--0:#D7DBE0">v</span><span style="--0:#D6DEEB">]</span></span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">=></span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;dt></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">k</span><span style="--0:#D6DEEB">)</span></span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/dt>&#x3C;dd></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">renderValue</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">v</span><span style="--0:#D6DEEB">)</span></span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/dd></span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">)</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">join</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">""</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">return</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">page</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">id</span></span><span style="--0:#889FB2;--1:#4D667B">,</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;p>&#x3C;a href="/">&#x26;#8592; all content&#x3C;/a>&#x3C;/p></span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;h1>Content item&#x3C;/h1></span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;p>&#x3C;small>version </span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">it</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">version</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0"> · locale </span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">it</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">locale</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0"> ·</span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">published </span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">it</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">publishedAt</span><span style="--0:#E2817F;--1:#B23834"> </span><span style="--0:#C792EA;--1:#8844AE">??</span><span style="--0:#E2817F;--1:#B23834"> </span><span style="--0:#D6DEEB;--1:#403F53">"</span><span style="--0:#ECC48D;--1:#984E4D">—</span><span style="--0:#D6DEEB;--1:#403F53">"</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/small>&#x3C;/p></span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;dl></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#D7DBE0;--1:#403F53">fields</span><span style="--0:#E2817F;--1:#B23834"> </span><span style="--0:#C792EA;--1:#8844AE">||</span><span style="--0:#E2817F;--1:#B23834"> </span><span style="--0:#D6DEEB;--1:#403F53">"</span><span style="--0:#ECC48D;--1:#984E4D">&#x3C;dd>&#x3C;small>(this item has no field data)&#x3C;/small>&#x3C;/dd></span><span style="--0:#D6DEEB;--1:#403F53">"</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/dl></span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="async function item(id) { const it = await velora(&#x60;/api/v1/content/${encodeURIComponent(id)}&#x60;); const fields = Object.entries(it.data) .map(([k, v]) => &#x60;<dt>${esc(k)}</dt><dd>${renderValue(v)}</dd>&#x60;) .join(&#x22;&#x22;); return page(id, &#x60;<p><a href=&#x22;/&#x22;>&#x26;#8592; all content</a></p><h1>Content item</h1><p><small>version ${esc(it.version)} · locale ${esc(it.locale)} ·published ${esc(it.publishedAt ?? &#x22;—&#x22;)}</small></p><dl>${fields || &#x22;<dd><small>(this item has no field data)</small></dd>&#x22;}</dl>&#x60;);}"><div></div></button></div></figure></div>
272
+ <p>That renders a definition list of every field, plus the item’s version and
273
+ publish time. The properties this function touches:</p>
274
+
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+
309
+ <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">it.data</code></td><td>object</td><td>Field values keyed by field <strong>id</strong> — the keys are whatever the document type defines.</td><td><code dir="auto">{"title":"Hello, Velora"}</code></td></tr><tr><td><code dir="auto">it.version</code></td><td>positive integer</td><td>Which immutable version is currently published. Every save mints a new one.</td><td><code dir="auto">3</code></td></tr><tr><td><code dir="auto">it.locale</code></td><td>string</td><td>The translation served.</td><td><code dir="auto">en</code></td></tr><tr><td><code dir="auto">it.publishedAt</code></td><td>ISO datetime string or null</td><td>When it was published; <code dir="auto">null</code> only for a draft read with a preview token.</td><td><code dir="auto">2026-08-25T15:36:10.252Z</code></td></tr></tbody></table>
310
+ <p>Keying on field id is why the page prints <code dir="auto">title</code>, <code dir="auto">body</code>, <code dir="auto">seo</code> rather
311
+ than the friendly labels: labels live on the document type, which the home
312
+ page already fetched. Joining the two is a natural next exercise.</p>
313
+ <div class="sl-heading-wrapper level-h2"><h2 id="6-the-server">6. The server</h2><a class="sl-anchor-link" href="#6-the-server"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “6. The server”</span></a></div>
314
+ <p>The last block wires the two routes together and turns anything thrown
315
+ above into a readable page:</p>
316
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span style="--0:#7FDBCA;--1:#096E72">http</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">createServer</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#C792EA;--1:#8844AE">async</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">(</span><span style="--1:#403F53"><span style="--0:#D7DBE0">req</span><span style="--0:#D6DEEB">, </span><span style="--0:#D7DBE0">res</span></span><span style="--0:#D9F5DD;--1:#111111">)</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">=></span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">try</span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">if</span><span style="--0:#D6DEEB;--1:#403F53"> (</span><span style="--0:#C792EA;--1:#8844AE">!</span><span style="--0:#82AAFF;--1:#3B61B0">API_URL</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">||</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">!</span><span style="--0:#82AAFF;--1:#3B61B0">API_KEY</span><span style="--0:#D6DEEB;--1:#403F53">) {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">throw</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">new</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">Error</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">VELORA_API_URL / VELORA_API_KEY are not set — copy .env.example to .env and fill them in (demo values are in the file's comments).</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">url</span><span style="--0:#C792EA;--1:#8844AE"> = </span><span style="--0:#7FDBCA;--1:#096E72">new</span><span style="--0:#C792EA;--1:#8844AE"> </span><span style="--0:#82AAFF;--1:#3B61B0">URL</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">req</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">url</span><span style="--0:#C792EA;--1:#8844AE">, </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">http://localhost:</span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">PORT</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">const </span><span style="--0:#82AAFF;--1:#3B61B0">html</span><span style="--0:#C792EA;--1:#8844AE"> =</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">url</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">pathname</span><span style="--0:#C792EA;--1:#8844AE"> === </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/item</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C792EA;--1:#8844AE"> &#x26;&#x26; </span><span style="--0:#7FDBCA;--1:#096E72">url</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#FAF39F;--1:#111111">searchParams</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">get</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">id</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">)</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">?</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">await</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">item</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">url</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#FAF39F;--1:#111111">searchParams</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">get</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">id</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">))</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#C792EA;--1:#8844AE">:</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">await</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">home</span><span style="--0:#D6DEEB;--1:#403F53">();</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">res</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">writeHead</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#F78C6C;--1:#AA0982">200</span><span style="--0:#D6DEEB;--1:#403F53">, { </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">content-type</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">text/html; charset=utf-8</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53"> });</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">res</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">end</span><span style="--1:#403F53"><span style="--0:#D6DEEB">(</span><span style="--0:#D7DBE0">html</span><span style="--0:#D6DEEB">);</span></span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">} </span><span style="--0:#C792EA;--1:#8844AE">catch</span><span style="--1:#403F53"><span style="--0:#D6DEEB"> (</span><span style="--0:#D7DBE0">err</span><span style="--0:#D6DEEB">) {</span></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">res</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">writeHead</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#F78C6C;--1:#AA0982">500</span><span style="--0:#D6DEEB;--1:#403F53">, { </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">content-type</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">text/html; charset=utf-8</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53"> });</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">res</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">end</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#82AAFF;--1:#3B61B0">page</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">Error</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">, </span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;h1>Something went wrong&#x3C;/h1>&#x3C;pre></span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">esc</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#7FDBCA;--1:#096E72">err</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#7FDBCA;--1:#096E72">message</span><span style="--0:#D6DEEB;--1:#403F53">)</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#ECC48D;--1:#3B61B0">&#x3C;/pre></span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">));</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">})</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">listen</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#82AAFF;--1:#3B61B0">PORT</span><span style="--0:#D6DEEB;--1:#403F53">, </span><span style="--0:#D9F5DD;--1:#111111">()</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">=></span><span style="--0:#D6DEEB;--1:#403F53"> {</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">console</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">log</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#ECC48D;--1:#3B61B0">Hello-website running at http://localhost:</span><span style="--0:#E2817F;--1:#B23834">${</span><span style="--0:#82AAFF;--1:#3B61B0">PORT</span><span style="--0:#E2817F;--1:#B23834">}</span><span style="--0:#D6DEEB;--1:#403F53">`</span><span style="--0:#D6DEEB;--1:#403F53">);</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">});</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="http.createServer(async (req, res) => { try { if (!API_URL || !API_KEY) { throw new Error(&#x22;VELORA_API_URL / VELORA_API_KEY are not set — copy .env.example to .env and fill them in (demo values are in the file&#x27;s comments).&#x22;); } const url = new URL(req.url, &#x60;http://localhost:${PORT}&#x60;); const html = url.pathname === &#x22;/item&#x22; &#x26;&#x26; url.searchParams.get(&#x22;id&#x22;) ? await item(url.searchParams.get(&#x22;id&#x22;)) : await home(); res.writeHead(200, { &#x22;content-type&#x22;: &#x22;text/html; charset=utf-8&#x22; }); res.end(html); } catch (err) { res.writeHead(500, { &#x22;content-type&#x22;: &#x22;text/html; charset=utf-8&#x22; }); res.end(page(&#x22;Error&#x22;, &#x60;<h1>Something went wrong</h1><pre>${esc(err.message)}</pre>&#x60;)); }}).listen(PORT, () => { console.log(&#x60;Hello-website running at http://localhost:${PORT}&#x60;);});"><div></div></button></div></figure></div>
317
+ <p>Any path other than <code dir="auto">/item?id=…</code> renders the home page, and a failed fetch
318
+ becomes a <code dir="auto">500</code> page showing its message — <code dir="auto">"/api/v1/content -> HTTP 401"</code>
319
+ when the key is wrong — instead of crashing the process. That is the whole
320
+ file: roughly 120 lines, and every line of it is above.</p>
321
+ <div class="sl-heading-wrapper level-h2"><h2 id="what-it-renders">What it renders</h2><a class="sl-anchor-link" href="#what-it-renders"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “What it renders”</span></a></div>
322
+ <p>With the demo <code dir="auto">.env</code> in place, starting the server and requesting the home
323
+ page returns HTML like this — a live response from a real run against
324
+ <code dir="auto">https://demo.velora-cms.com</code>, trimmed to the body:</p>
325
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="html"><code><div class="ec-line"><div class="code"><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">body</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">h1</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">Hello, Velora</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">h1</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">p</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">Everything below is live, published content served by the headless</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">API at </span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">code</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">https://demo.velora-cms.com</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">code</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">. Publish something in the admin and</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">refresh — no rebuild.</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">p</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">h2</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">Tag</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">h2</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">ul</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA"> </span></span><span style="--0:#C5E478;--1:#3B61B0">href</span><span style="--0:#7FDBCA;--1:#8844AE">=</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/item?id=2e9053b1-a300-493f-a48b-58440cdf3629</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#7FDBCA;--1:#8844AE">></span><span style="--0:#D6DEEB;--1:#403F53">/announcements</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">(en)</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA"> </span></span><span style="--0:#C5E478;--1:#3B61B0">href</span><span style="--0:#7FDBCA;--1:#8844AE">=</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/item?id=45dfb6ca-ffa7-482e-842b-f662c0c1034c</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#7FDBCA;--1:#8844AE">></span><span style="--0:#D6DEEB;--1:#403F53">/guides</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">(en)</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">ul</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">h2</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">Author</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">h2</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">ul</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA"> </span></span><span style="--0:#C5E478;--1:#3B61B0">href</span><span style="--0:#7FDBCA;--1:#8844AE">=</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/item?id=acc3aefb-6254-465a-b088-c9578b2794af</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#7FDBCA;--1:#8844AE">></span><span style="--0:#D6DEEB;--1:#403F53">/jane-doe</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">(en)</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">ul</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">h2</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">Post</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">h2</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">ul</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA"> </span></span><span style="--0:#C5E478;--1:#3B61B0">href</span><span style="--0:#7FDBCA;--1:#8844AE">=</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/item?id=afe5b000-d81e-4c51-b60a-1f5f04d6dacf</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#7FDBCA;--1:#8844AE">></span><span style="--0:#D6DEEB;--1:#403F53">/jane-doe/hello-velora</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">(en)</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA"> </span></span><span style="--0:#C5E478;--1:#3B61B0">href</span><span style="--0:#7FDBCA;--1:#8844AE">=</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/item?id=ce16029a-f5ef-4bbe-ad07-6eede30c9b8c</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#7FDBCA;--1:#8844AE">></span><span style="--0:#D6DEEB;--1:#403F53">/jane-doe/writing-your-first-post</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">(en)</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">li</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">ul</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">body</span><span style="--0:#7FDBCA">></span></span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="<body><h1>Hello, Velora</h1><p>Everything below is live, published content served by the headlessAPI at <code>https://demo.velora-cms.com</code>. Publish something in the admin andrefresh — no rebuild.</p><h2>Tag</h2><ul><li><a href=&#x22;/item?id=2e9053b1-a300-493f-a48b-58440cdf3629&#x22;>/announcements</a> <small>(en)</small></li><li><a href=&#x22;/item?id=45dfb6ca-ffa7-482e-842b-f662c0c1034c&#x22;>/guides</a> <small>(en)</small></li></ul><h2>Author</h2><ul><li><a href=&#x22;/item?id=acc3aefb-6254-465a-b088-c9578b2794af&#x22;>/jane-doe</a> <small>(en)</small></li></ul><h2>Post</h2><ul><li><a href=&#x22;/item?id=afe5b000-d81e-4c51-b60a-1f5f04d6dacf&#x22;>/jane-doe/hello-velora</a> <small>(en)</small></li><li><a href=&#x22;/item?id=ce16029a-f5ef-4bbe-ad07-6eede30c9b8c&#x22;>/jane-doe/writing-your-first-post</a> <small>(en)</small></li></ul></body>"><div></div></button></div></figure></div>
326
+ <p>In a browser that is three headings — <strong>Tag</strong>, <strong>Author</strong>, <strong>Post</strong> — with
327
+ the demo’s five published nodes listed under them by path.</p>
328
+ <p>Following the first Post link renders its fields — a live response from
329
+ the same run, trimmed to the body:</p>
330
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="html"><code><div class="ec-line"><div class="code"><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">body</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">p</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA"> </span></span><span style="--0:#C5E478;--1:#3B61B0">href</span><span style="--0:#7FDBCA;--1:#8844AE">=</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#7FDBCA;--1:#8844AE">></span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#8592;</span><span style="--0:#D6DEEB;--1:#403F53"> all content</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">a</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">p</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">h1</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">Content item</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">h1</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">p</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">version 3 · locale en ·</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">published 2026-08-25T15:36:10.252Z</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">small</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">p</span><span style="--0:#7FDBCA">></span></span></div></div><div class="ec-line"><div class="code"><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;</span><span style="--0:#CAECE6">dl</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">seo</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">pre</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">title</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">Hello, Velora — the capstone post</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">description</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">A published post proving the SEO plugin composes with the Blog starter on the deployed Velora demo.</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">pre</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">body</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">pre</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">type</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">doc</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">content</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: [</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">type</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">paragraph</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">content</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: [</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">text</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">This sample post was created by the Blog template. Edit or delete it freely.</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">type</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">text</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">]</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">},</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">type</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">paragraph</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">content</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: [</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">text</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">Updated live during the Session 168 capstone story.</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">type</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span><span style="--0:#D6DEEB;--1:#403F53">text</span><span style="--0:#82AAFF;--1:#3B61B0">&#x26;#34;</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">]</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">]</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">pre</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">tags</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">announcements</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">title</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">Hello, Velora</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">publishedDate</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">dt</span><span style="--0:#7FDBCA">>&#x3C;</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">></span></span><span style="--0:#D6DEEB;--1:#403F53">2026-08-13</span><span style="--1:#8844AE"><span style="--0:#7FDBCA">&#x3C;/</span><span style="--0:#CAECE6">dd</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">dl</span><span style="--0:#7FDBCA">>&#x3C;/</span><span style="--0:#CAECE6">body</span><span style="--0:#7FDBCA">></span></span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="<body><p><a href=&#x22;/&#x22;>&#x26;#8592; all content</a></p><h1>Content item</h1><p><small>version 3 · locale en ·published 2026-08-25T15:36:10.252Z</small></p><dl><dt>seo</dt><dd><pre>{ &#x26;#34;title&#x26;#34;: &#x26;#34;Hello, Velora — the capstone post&#x26;#34;, &#x26;#34;description&#x26;#34;: &#x26;#34;A published post proving the SEO plugin composes with the Blog starter on the deployed Velora demo.&#x26;#34;}</pre></dd><dt>body</dt><dd><pre>{ &#x26;#34;type&#x26;#34;: &#x26;#34;doc&#x26;#34;, &#x26;#34;content&#x26;#34;: [ { &#x26;#34;type&#x26;#34;: &#x26;#34;paragraph&#x26;#34;, &#x26;#34;content&#x26;#34;: [ { &#x26;#34;text&#x26;#34;: &#x26;#34;This sample post was created by the Blog template. Edit or delete it freely.&#x26;#34;, &#x26;#34;type&#x26;#34;: &#x26;#34;text&#x26;#34; } ] }, { &#x26;#34;type&#x26;#34;: &#x26;#34;paragraph&#x26;#34;, &#x26;#34;content&#x26;#34;: [ { &#x26;#34;text&#x26;#34;: &#x26;#34;Updated live during the Session 168 capstone story.&#x26;#34;, &#x26;#34;type&#x26;#34;: &#x26;#34;text&#x26;#34; } ] } ]}</pre></dd><dt>tags</dt><dd>announcements</dd><dt>title</dt><dd>Hello, Velora</dd><dt>publishedDate</dt><dd>2026-08-13</dd></dl></body>"><div></div></button></div></figure></div>
331
+ <p>The back-link at the top returns to the home page. Plain fields (<code dir="auto">title</code>,
332
+ <code dir="auto">tags</code>, <code dir="auto">publishedDate</code>) render as text; the structured ones (<code dir="auto">seo</code>, and
333
+ the rich-text <code dir="auto">body</code>, which stores TipTap’s JSON document format) render
334
+ as pretty-printed JSON, which is exactly the shape you would design real
335
+ markup against.</p>
336
+ <p>Stop the server with <code dir="auto">Ctrl-C</code> when you are done.</p>
337
+ <div class="sl-heading-wrapper level-h2"><h2 id="point-it-at-your-own-instance">Point it at your own instance</h2><a class="sl-anchor-link" href="#point-it-at-your-own-instance"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Point it at your own instance”</span></a></div>
338
+ <p>Three changes move the site off the demo.</p>
339
+ <p>First, the instance must serve the public API — it is a deploy-time
340
+ opt-in, so start your server with the variable set. Run this from the
341
+ repository root:</p>
342
+ <div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#C5E478;--1:#3B61B0">VELORA_HEADLESS</span><span style="--0:#C792EA;--1:#8844AE">=</span><span style="--0:#ECC48D;--1:#3B61B0">true</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">node</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">apps/server/dist/index.js</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="VELORA_HEADLESS=true node apps/server/dist/index.js"><div></div></button></div></figure></div>
343
+ <p>Without it, <code dir="auto">/api/v1/*</code> does not exist and every call from this site fails
344
+ with <code dir="auto">HTTP 404</code> rather than <code dir="auto">401</code>.</p>
345
+ <p>Second, mint a key: in the admin, <strong>Settings → API Keys → Create key</strong>.
346
+ The secret is shown once. Treat it as a secret — keys are created
347
+ read-only by default, and a key minted with the <strong>Read and write</strong> option
348
+ can also create and publish content (<a href="/docs/v1/using-velora/headless-api/#what-a-key-can-do">What a key can
349
+ do</a>), and
350
+ <a href="/docs/v1/using-velora/users-and-roles/">Users and roles</a> covers rotating and
351
+ revoking one.</p>
352
+ <p>Third, point <code dir="auto">.env</code> at your origin and that key, then restart:</p>
353
+ <div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">cat</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">></span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">.env</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#7FDBCA;--1:#096E72">&#x3C;&#x3C;</span><span style="--0:#D9F5DD;--1:#111111">'ENV'</span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#984E4D">VELORA_API_URL=http://localhost:3000</span></div></div><div class="ec-line"><div class="code"><span style="--0:#ECC48D;--1:#984E4D">VELORA_API_KEY=vk_your_key_here</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D9F5DD;--1:#111111">ENV</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="cat > .env <<&#x27;ENV&#x27;VELORA_API_URL=http://localhost:3000VELORA_API_KEY=vk_your_key_hereENV"><div></div></button></div></figure></div>
354
+ <p>Restarting <code dir="auto">node server.js</code> now shows <em>your</em> content — or the empty-state
355
+ line, if nothing is published yet.</p>
356
+ <div class="sl-heading-wrapper level-h2"><h2 id="make-it-say-hello-world">Make it say hello world</h2><a class="sl-anchor-link" href="#make-it-say-hello-world"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Make it say hello world”</span></a></div>
357
+ <p>With the site pointed at your own instance, three steps in the admin put
358
+ something on it:</p>
359
+ <ol>
360
+ <li><strong>Document types → Create.</strong> Name it <code dir="auto">Page</code>, add a Text field <code dir="auto">title</code>
361
+ and a Rich text field <code dir="auto">body</code>. Field ids become the keys in <code dir="auto">data</code>, so
362
+ <code dir="auto">title</code> here is <code dir="auto">data.title</code> in the API response.</li>
363
+ <li><strong>Content → Create → Page.</strong> Type “Hello world” into <code dir="auto">title</code>, write
364
+ something in <code dir="auto">body</code>, and <strong>Save</strong>.</li>
365
+ <li><strong>Publish.</strong> Only published content appears on the public API — a saved
366
+ draft is invisible to this site, deliberately.</li>
367
+ </ol>
368
+ <p>Refresh the browser. Your page is listed under <strong>Page</strong>; clicking it shows
369
+ <code dir="auto">title</code> as text and <code dir="auto">body</code> as the rich-text document object. There is no
370
+ rebuild and no cache to clear: the next request re-fetches from the API,
371
+ which is the whole point of rendering server-side.</p>
372
+ <div class="sl-heading-wrapper level-h2"><h2 id="where-to-go-next">Where to go next</h2><a class="sl-anchor-link" href="#where-to-go-next"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Where to go next”</span></a></div>
373
+ <p>The site above uses four endpoints. Two more make it a real website:</p>
374
+
375
+
376
+
377
+
378
+
379
+
380
+
381
+
382
+
383
+
384
+
385
+
386
+
387
+
388
+
389
+
390
+
391
+
392
+
393
+
394
+
395
+
396
+
397
+
398
+
399
+
400
+
401
+
402
+
403
+ <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">GET /api/v1/content/by-path</code></td><td>request</td><td>Resolves a URL path straight to an item, so <code dir="auto">/about</code> on your site can map to <code dir="auto">/about</code> in the content tree instead of an opaque id.</td><td><code dir="auto">/api/v1/content/by-path?path=/about</code></td></tr><tr><td><code dir="auto">GET /api/v1/content/:id/children</code></td><td>request</td><td>One level of the tree, paginated — navigation menus and section index pages.</td><td><code dir="auto">/api/v1/content/acc3aefb-…/children?limit=20</code></td></tr><tr><td><code dir="auto">nextCursor</code></td><td>string (uuid) or null</td><td>Feed it back as <code dir="auto">?cursor=…</code> to page past the first 100 items.</td><td><code dir="auto">45dfb6ca-ffa7-482e-842b-f662c0c1034c</code></td></tr></tbody></table>
404
+ <ul>
405
+ <li><a href="/docs/v1/using-velora/headless-api/">Headless API</a> — the complete reference:
406
+ every route, parameter, response property, error body and rate limit,
407
+ plus the GraphQL surface if you would rather fetch a page and its
408
+ children in one round trip.</li>
409
+ <li><a href="/docs/v1/using-velora/content-model/">Content model</a> — designing the
410
+ document types this site reads.</li>
411
+ <li><a href="https://github.com/velora-cms/velora-starter">velora-cms/velora-starter</a>
412
+ — the finished <code dir="auto">examples/hello-website</code>, plus issues and discussions.</li>
413
+ </ul></div><footer class="sl-flex astro-gjkl2a5w"><div class="meta sl-flex astro-gjkl2a5w"></div><div class="pagination-links print:hidden astro-3tj6ibx2" dir="ltr"><a href="/docs/v1/using-velora/headless-api/" rel="prev" class="astro-3tj6ibx2"><svg aria-hidden="true" class="astro-3tj6ibx2 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17 11H9.41l3.3-3.29a1.004 1.004 0 1 0-1.42-1.42l-5 5a1 1 0 0 0-.21.33 1 1 0 0 0 0 .76 1 1 0 0 0 .21.33l5 5a1.002 1.002 0 0 0 1.639-.325 1 1 0 0 0-.219-1.095L9.41 13H17a1 1 0 0 0 0-2Z"/></svg><span class="astro-3tj6ibx2">Previous<br class="astro-3tj6ibx2"><span class="link-title astro-3tj6ibx2">Headless API</span></span></a></div></footer><div class="velora-bundle-stamp astro-llmj63sw">Velora 0.9.0 &middot; bundled documentation</div></div></div></main></div></div></div></div></body></html>