@we8/cms 0.1.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 (304) hide show
  1. package/README.md +78 -0
  2. package/dist/auth/auth.d.ts +87 -0
  3. package/dist/auth/auth.d.ts.map +1 -0
  4. package/dist/auth/auth.js +90 -0
  5. package/dist/auth/auth.js.map +1 -0
  6. package/dist/auth/routes.admin.d.ts +22 -0
  7. package/dist/auth/routes.admin.d.ts.map +1 -0
  8. package/dist/auth/routes.admin.js +106 -0
  9. package/dist/auth/routes.admin.js.map +1 -0
  10. package/dist/auth/schema.d.ts +1395 -0
  11. package/dist/auth/schema.d.ts.map +1 -0
  12. package/dist/auth/schema.js +76 -0
  13. package/dist/auth/schema.js.map +1 -0
  14. package/dist/env.d.ts +64 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +48 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +170 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/keys/middleware.d.ts +54 -0
  23. package/dist/keys/middleware.d.ts.map +1 -0
  24. package/dist/keys/middleware.js +84 -0
  25. package/dist/keys/middleware.js.map +1 -0
  26. package/dist/keys/routes.admin.d.ts +16 -0
  27. package/dist/keys/routes.admin.d.ts.map +1 -0
  28. package/dist/keys/routes.admin.js +34 -0
  29. package/dist/keys/routes.admin.js.map +1 -0
  30. package/dist/keys/service.d.ts +36 -0
  31. package/dist/keys/service.d.ts.map +1 -0
  32. package/dist/keys/service.js +120 -0
  33. package/dist/keys/service.js.map +1 -0
  34. package/dist/lib/bots.d.ts +24 -0
  35. package/dist/lib/bots.d.ts.map +1 -0
  36. package/dist/lib/bots.js +79 -0
  37. package/dist/lib/bots.js.map +1 -0
  38. package/dist/lib/email.d.ts +62 -0
  39. package/dist/lib/email.d.ts.map +1 -0
  40. package/dist/lib/email.js +64 -0
  41. package/dist/lib/email.js.map +1 -0
  42. package/dist/lib/errors.d.ts +12 -0
  43. package/dist/lib/errors.d.ts.map +1 -0
  44. package/dist/lib/errors.js +23 -0
  45. package/dist/lib/errors.js.map +1 -0
  46. package/dist/lib/notify.d.ts +20 -0
  47. package/dist/lib/notify.d.ts.map +1 -0
  48. package/dist/lib/notify.js +60 -0
  49. package/dist/lib/notify.js.map +1 -0
  50. package/dist/lib/retention.d.ts +15 -0
  51. package/dist/lib/retention.d.ts.map +1 -0
  52. package/dist/lib/retention.js +40 -0
  53. package/dist/lib/retention.js.map +1 -0
  54. package/dist/lib/sanitize.d.ts +14 -0
  55. package/dist/lib/sanitize.d.ts.map +1 -0
  56. package/dist/lib/sanitize.js +145 -0
  57. package/dist/lib/sanitize.js.map +1 -0
  58. package/dist/lib/turnstile.d.ts +7 -0
  59. package/dist/lib/turnstile.d.ts.map +1 -0
  60. package/dist/lib/turnstile.js +32 -0
  61. package/dist/lib/turnstile.js.map +1 -0
  62. package/dist/lib/validate.d.ts +11 -0
  63. package/dist/lib/validate.d.ts.map +1 -0
  64. package/dist/lib/validate.js +22 -0
  65. package/dist/lib/validate.js.map +1 -0
  66. package/dist/lib/word-count.d.ts +7 -0
  67. package/dist/lib/word-count.d.ts.map +1 -0
  68. package/dist/lib/word-count.js +14 -0
  69. package/dist/lib/word-count.js.map +1 -0
  70. package/dist/middleware/auth.d.ts +46 -0
  71. package/dist/middleware/auth.d.ts.map +1 -0
  72. package/dist/middleware/auth.js +60 -0
  73. package/dist/middleware/auth.js.map +1 -0
  74. package/dist/middleware/cors.d.ts +33 -0
  75. package/dist/middleware/cors.d.ts.map +1 -0
  76. package/dist/middleware/cors.js +55 -0
  77. package/dist/middleware/cors.js.map +1 -0
  78. package/dist/middleware/module-gate.d.ts +11 -0
  79. package/dist/middleware/module-gate.d.ts.map +1 -0
  80. package/dist/middleware/module-gate.js +18 -0
  81. package/dist/middleware/module-gate.js.map +1 -0
  82. package/dist/middleware/rate-limit.d.ts +13 -0
  83. package/dist/middleware/rate-limit.d.ts.map +1 -0
  84. package/dist/middleware/rate-limit.js +30 -0
  85. package/dist/middleware/rate-limit.js.map +1 -0
  86. package/dist/middleware/request-id.d.ts +8 -0
  87. package/dist/middleware/request-id.d.ts.map +1 -0
  88. package/dist/middleware/request-id.js +11 -0
  89. package/dist/middleware/request-id.js.map +1 -0
  90. package/dist/modules/aeo/checks.d.ts +41 -0
  91. package/dist/modules/aeo/checks.d.ts.map +1 -0
  92. package/dist/modules/aeo/checks.js +127 -0
  93. package/dist/modules/aeo/checks.js.map +1 -0
  94. package/dist/modules/aeo/llms.d.ts +24 -0
  95. package/dist/modules/aeo/llms.d.ts.map +1 -0
  96. package/dist/modules/aeo/llms.js +92 -0
  97. package/dist/modules/aeo/llms.js.map +1 -0
  98. package/dist/modules/aeo/routes.admin.d.ts +12 -0
  99. package/dist/modules/aeo/routes.admin.d.ts.map +1 -0
  100. package/dist/modules/aeo/routes.admin.js +37 -0
  101. package/dist/modules/aeo/routes.admin.js.map +1 -0
  102. package/dist/modules/analytics/db.d.ts +24 -0
  103. package/dist/modules/analytics/db.d.ts.map +1 -0
  104. package/dist/modules/analytics/db.js +24 -0
  105. package/dist/modules/analytics/db.js.map +1 -0
  106. package/dist/modules/analytics/routes.admin.d.ts +6 -0
  107. package/dist/modules/analytics/routes.admin.d.ts.map +1 -0
  108. package/dist/modules/analytics/routes.admin.js +21 -0
  109. package/dist/modules/analytics/routes.admin.js.map +1 -0
  110. package/dist/modules/analytics/routes.public.d.ts +6 -0
  111. package/dist/modules/analytics/routes.public.d.ts.map +1 -0
  112. package/dist/modules/analytics/routes.public.js +33 -0
  113. package/dist/modules/analytics/routes.public.js.map +1 -0
  114. package/dist/modules/analytics/service.d.ts +16 -0
  115. package/dist/modules/analytics/service.d.ts.map +1 -0
  116. package/dist/modules/analytics/service.js +148 -0
  117. package/dist/modules/analytics/service.js.map +1 -0
  118. package/dist/modules/authors/routes.admin.d.ts +6 -0
  119. package/dist/modules/authors/routes.admin.d.ts.map +1 -0
  120. package/dist/modules/authors/routes.admin.js +29 -0
  121. package/dist/modules/authors/routes.admin.js.map +1 -0
  122. package/dist/modules/authors/routes.public.d.ts +6 -0
  123. package/dist/modules/authors/routes.public.d.ts.map +1 -0
  124. package/dist/modules/authors/routes.public.js +11 -0
  125. package/dist/modules/authors/routes.public.js.map +1 -0
  126. package/dist/modules/authors/service.d.ts +26 -0
  127. package/dist/modules/authors/service.d.ts.map +1 -0
  128. package/dist/modules/authors/service.js +116 -0
  129. package/dist/modules/authors/service.js.map +1 -0
  130. package/dist/modules/content/db.d.ts +127 -0
  131. package/dist/modules/content/db.d.ts.map +1 -0
  132. package/dist/modules/content/db.js +203 -0
  133. package/dist/modules/content/db.js.map +1 -0
  134. package/dist/modules/content/publish.d.ts +38 -0
  135. package/dist/modules/content/publish.d.ts.map +1 -0
  136. package/dist/modules/content/publish.js +184 -0
  137. package/dist/modules/content/publish.js.map +1 -0
  138. package/dist/modules/content/routes.admin.d.ts +8 -0
  139. package/dist/modules/content/routes.admin.d.ts.map +1 -0
  140. package/dist/modules/content/routes.admin.js +122 -0
  141. package/dist/modules/content/routes.admin.js.map +1 -0
  142. package/dist/modules/content/routes.public.d.ts +6 -0
  143. package/dist/modules/content/routes.public.d.ts.map +1 -0
  144. package/dist/modules/content/routes.public.js +56 -0
  145. package/dist/modules/content/routes.public.js.map +1 -0
  146. package/dist/modules/content/service.d.ts +137 -0
  147. package/dist/modules/content/service.d.ts.map +1 -0
  148. package/dist/modules/content/service.js +348 -0
  149. package/dist/modules/content/service.js.map +1 -0
  150. package/dist/modules/forms/db.d.ts +42 -0
  151. package/dist/modules/forms/db.d.ts.map +1 -0
  152. package/dist/modules/forms/db.js +62 -0
  153. package/dist/modules/forms/db.js.map +1 -0
  154. package/dist/modules/forms/forms-routes.admin.d.ts +6 -0
  155. package/dist/modules/forms/forms-routes.admin.d.ts.map +1 -0
  156. package/dist/modules/forms/forms-routes.admin.js +37 -0
  157. package/dist/modules/forms/forms-routes.admin.js.map +1 -0
  158. package/dist/modules/forms/forms-service.d.ts +18 -0
  159. package/dist/modules/forms/forms-service.d.ts.map +1 -0
  160. package/dist/modules/forms/forms-service.js +121 -0
  161. package/dist/modules/forms/forms-service.js.map +1 -0
  162. package/dist/modules/forms/routes.public.d.ts +6 -0
  163. package/dist/modules/forms/routes.public.d.ts.map +1 -0
  164. package/dist/modules/forms/routes.public.js +41 -0
  165. package/dist/modules/forms/routes.public.js.map +1 -0
  166. package/dist/modules/forms/submissions-routes.admin.d.ts +6 -0
  167. package/dist/modules/forms/submissions-routes.admin.d.ts.map +1 -0
  168. package/dist/modules/forms/submissions-routes.admin.js +46 -0
  169. package/dist/modules/forms/submissions-routes.admin.js.map +1 -0
  170. package/dist/modules/forms/submissions-service.d.ts +45 -0
  171. package/dist/modules/forms/submissions-service.d.ts.map +1 -0
  172. package/dist/modules/forms/submissions-service.js +174 -0
  173. package/dist/modules/forms/submissions-service.js.map +1 -0
  174. package/dist/modules/media/db.d.ts +15 -0
  175. package/dist/modules/media/db.d.ts.map +1 -0
  176. package/dist/modules/media/db.js +24 -0
  177. package/dist/modules/media/db.js.map +1 -0
  178. package/dist/modules/media/routes.admin.d.ts +6 -0
  179. package/dist/modules/media/routes.admin.d.ts.map +1 -0
  180. package/dist/modules/media/routes.admin.js +30 -0
  181. package/dist/modules/media/routes.admin.js.map +1 -0
  182. package/dist/modules/media/routes.public.d.ts +6 -0
  183. package/dist/modules/media/routes.public.d.ts.map +1 -0
  184. package/dist/modules/media/routes.public.js +35 -0
  185. package/dist/modules/media/routes.public.js.map +1 -0
  186. package/dist/modules/media/service.d.ts +32 -0
  187. package/dist/modules/media/service.d.ts.map +1 -0
  188. package/dist/modules/media/service.js +99 -0
  189. package/dist/modules/media/service.js.map +1 -0
  190. package/dist/modules/openapi/spec.d.ts +21 -0
  191. package/dist/modules/openapi/spec.d.ts.map +1 -0
  192. package/dist/modules/openapi/spec.js +468 -0
  193. package/dist/modules/openapi/spec.js.map +1 -0
  194. package/dist/modules/seo/jsonld.d.ts +25 -0
  195. package/dist/modules/seo/jsonld.d.ts.map +1 -0
  196. package/dist/modules/seo/jsonld.js +67 -0
  197. package/dist/modules/seo/jsonld.js.map +1 -0
  198. package/dist/modules/seo/routes.public.d.ts +6 -0
  199. package/dist/modules/seo/routes.public.d.ts.map +1 -0
  200. package/dist/modules/seo/routes.public.js +39 -0
  201. package/dist/modules/seo/routes.public.js.map +1 -0
  202. package/dist/modules/seo/sitemap.d.ts +3 -0
  203. package/dist/modules/seo/sitemap.d.ts.map +1 -0
  204. package/dist/modules/seo/sitemap.js +39 -0
  205. package/dist/modules/seo/sitemap.js.map +1 -0
  206. package/dist/modules/settings/routes.admin.d.ts +12 -0
  207. package/dist/modules/settings/routes.admin.d.ts.map +1 -0
  208. package/dist/modules/settings/routes.admin.js +27 -0
  209. package/dist/modules/settings/routes.admin.js.map +1 -0
  210. package/dist/modules/site-documents/routes.admin.d.ts +10 -0
  211. package/dist/modules/site-documents/routes.admin.d.ts.map +1 -0
  212. package/dist/modules/site-documents/routes.admin.js +29 -0
  213. package/dist/modules/site-documents/routes.admin.js.map +1 -0
  214. package/dist/modules/site-documents/routes.public.d.ts +6 -0
  215. package/dist/modules/site-documents/routes.public.d.ts.map +1 -0
  216. package/dist/modules/site-documents/routes.public.js +16 -0
  217. package/dist/modules/site-documents/routes.public.js.map +1 -0
  218. package/dist/modules/site-documents/service.d.ts +11 -0
  219. package/dist/modules/site-documents/service.d.ts.map +1 -0
  220. package/dist/modules/site-documents/service.js +42 -0
  221. package/dist/modules/site-documents/service.js.map +1 -0
  222. package/dist/modules/webhooks/dispatch.d.ts +14 -0
  223. package/dist/modules/webhooks/dispatch.d.ts.map +1 -0
  224. package/dist/modules/webhooks/dispatch.js +84 -0
  225. package/dist/modules/webhooks/dispatch.js.map +1 -0
  226. package/dist/shared/constants.d.ts +79 -0
  227. package/dist/shared/constants.d.ts.map +1 -0
  228. package/dist/shared/constants.js +89 -0
  229. package/dist/shared/constants.js.map +1 -0
  230. package/dist/shared/envelope.d.ts +37 -0
  231. package/dist/shared/envelope.d.ts.map +1 -0
  232. package/dist/shared/envelope.js +46 -0
  233. package/dist/shared/envelope.js.map +1 -0
  234. package/dist/shared/hash.d.ts +2 -0
  235. package/dist/shared/hash.d.ts.map +1 -0
  236. package/dist/shared/hash.js +8 -0
  237. package/dist/shared/hash.js.map +1 -0
  238. package/dist/shared/index.d.ts +13 -0
  239. package/dist/shared/index.d.ts.map +1 -0
  240. package/dist/shared/index.js +13 -0
  241. package/dist/shared/index.js.map +1 -0
  242. package/dist/shared/schemas/aeo.d.ts +182 -0
  243. package/dist/shared/schemas/aeo.d.ts.map +1 -0
  244. package/dist/shared/schemas/aeo.js +43 -0
  245. package/dist/shared/schemas/aeo.js.map +1 -0
  246. package/dist/shared/schemas/analytics.d.ts +277 -0
  247. package/dist/shared/schemas/analytics.d.ts.map +1 -0
  248. package/dist/shared/schemas/analytics.js +74 -0
  249. package/dist/shared/schemas/analytics.js.map +1 -0
  250. package/dist/shared/schemas/api-key.d.ts +77 -0
  251. package/dist/shared/schemas/api-key.d.ts.map +1 -0
  252. package/dist/shared/schemas/api-key.js +27 -0
  253. package/dist/shared/schemas/api-key.js.map +1 -0
  254. package/dist/shared/schemas/author.d.ts +147 -0
  255. package/dist/shared/schemas/author.d.ts.map +1 -0
  256. package/dist/shared/schemas/author.js +27 -0
  257. package/dist/shared/schemas/author.js.map +1 -0
  258. package/dist/shared/schemas/form.d.ts +341 -0
  259. package/dist/shared/schemas/form.d.ts.map +1 -0
  260. package/dist/shared/schemas/form.js +117 -0
  261. package/dist/shared/schemas/form.js.map +1 -0
  262. package/dist/shared/schemas/index.d.ts +12 -0
  263. package/dist/shared/schemas/index.d.ts.map +1 -0
  264. package/dist/shared/schemas/index.js +12 -0
  265. package/dist/shared/schemas/index.js.map +1 -0
  266. package/dist/shared/schemas/media.d.ts +42 -0
  267. package/dist/shared/schemas/media.d.ts.map +1 -0
  268. package/dist/shared/schemas/media.js +16 -0
  269. package/dist/shared/schemas/media.js.map +1 -0
  270. package/dist/shared/schemas/pagination.d.ts +34 -0
  271. package/dist/shared/schemas/pagination.d.ts.map +1 -0
  272. package/dist/shared/schemas/pagination.js +21 -0
  273. package/dist/shared/schemas/pagination.js.map +1 -0
  274. package/dist/shared/schemas/post.d.ts +862 -0
  275. package/dist/shared/schemas/post.d.ts.map +1 -0
  276. package/dist/shared/schemas/post.js +187 -0
  277. package/dist/shared/schemas/post.js.map +1 -0
  278. package/dist/shared/schemas/settings.d.ts +839 -0
  279. package/dist/shared/schemas/settings.d.ts.map +1 -0
  280. package/dist/shared/schemas/settings.js +202 -0
  281. package/dist/shared/schemas/settings.js.map +1 -0
  282. package/dist/shared/schemas/site-document.d.ts +38 -0
  283. package/dist/shared/schemas/site-document.d.ts.map +1 -0
  284. package/dist/shared/schemas/site-document.js +21 -0
  285. package/dist/shared/schemas/site-document.js.map +1 -0
  286. package/dist/shared/schemas/webhook.d.ts +39 -0
  287. package/dist/shared/schemas/webhook.d.ts.map +1 -0
  288. package/dist/shared/schemas/webhook.js +20 -0
  289. package/dist/shared/schemas/webhook.js.map +1 -0
  290. package/dist/spa.d.ts +9 -0
  291. package/dist/spa.d.ts.map +1 -0
  292. package/dist/spa.js +57 -0
  293. package/dist/spa.js.map +1 -0
  294. package/migrations/0001_settings.sql +39 -0
  295. package/migrations/0002_content.sql +135 -0
  296. package/migrations/0003_forms.sql +35 -0
  297. package/migrations/0004_analytics.sql +42 -0
  298. package/migrations/0005_media.sql +17 -0
  299. package/migrations/0006_site_documents.sql +18 -0
  300. package/migrations/0007_auth.sql +81 -0
  301. package/migrations/0008_api_keys.sql +20 -0
  302. package/package.json +68 -0
  303. package/scripts/bundle-admin.mjs +54 -0
  304. package/seed/local.sql +63 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/shared/schemas/settings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,202 @@
1
+ import { z } from 'zod';
2
+ import { POST_TYPES } from '../constants.js';
3
+ import { webhookConfigSchema } from './webhook.js';
4
+ export const moduleFlagsSchema = z.object({
5
+ content: z.boolean().default(true),
6
+ forms: z.boolean().default(true),
7
+ analytics: z.boolean().default(true),
8
+ media: z.boolean().default(true),
9
+ seo: z.boolean().default(true),
10
+ });
11
+ export const retentionSettingsSchema = z.object({
12
+ visits: z.number().int().min(1).max(3650).default(90),
13
+ consent: z.number().int().min(1).max(3650).default(365),
14
+ });
15
+ export const turnstileSettingsSchema = z.object({
16
+ enabled: z.boolean().default(false),
17
+ sitekey: z.string().optional(),
18
+ });
19
+ /** schema.org Organization, minimal shape. Used as JSON-LD `publisher` on post detail responses. */
20
+ export const publisherSettingsSchema = z.object({
21
+ name: z.string().min(1),
22
+ logoUrl: z.string().url().optional(),
23
+ });
24
+ const pathPrefixSchema = z
25
+ .string()
26
+ .min(1)
27
+ .max(100)
28
+ .regex(/^\/[a-z0-9\-/]*$/, 'path must start with / and be lowercase kebab segments');
29
+ /**
30
+ * Per-type URL sections: a PARTIAL map from post type to the path prefix the
31
+ * site serves that section at, e.g. `{ blog: "/insights", news: "/newsroom" }`.
32
+ * Types absent from the map fall back to `postsPathPrefix`, the universal
33
+ * prefix, so an empty map behaves exactly as a site with one section does. All
34
+ * fields optional, so the object replaces wholesale on update. Explicit
35
+ * optional keys instead of z.record: a record with an enum key types as a FULL
36
+ * Record while parsing partially, a lie the resolver would trip on.
37
+ */
38
+ export const contentPathsSchema = z.object({
39
+ blog: pathPrefixSchema.optional(),
40
+ article: pathPrefixSchema.optional(),
41
+ news: pathPrefixSchema.optional(),
42
+ research: pathPrefixSchema.optional(),
43
+ whitepaper: pathPrefixSchema.optional(),
44
+ 'case-study': pathPrefixSchema.optional(),
45
+ });
46
+ /**
47
+ * THE resolution rule, defined once: a type's own path, else the legacy
48
+ * universal prefix. Sitemap, JSON-LD, the public site config, and the astro
49
+ * postUrl helper must all route through this so they can never disagree.
50
+ */
51
+ export function resolveContentPath(type, settings) {
52
+ return settings.contentPaths?.[type] ?? settings.postsPathPrefix;
53
+ }
54
+ /** The fully-resolved map served in the public site config (every type has a path). */
55
+ export function resolveAllContentPaths(settings) {
56
+ return Object.fromEntries(POST_TYPES.map((type) => [type, resolveContentPath(type, settings)]));
57
+ }
58
+ /**
59
+ * Site-level SEO FALLBACKS: a template's own values always win, and these fill
60
+ * the gaps when it omits one (the code-first merge lives in @we8/astro, not
61
+ * here). titleTemplate uses "%s" as the page-title placeholder, e.g.
62
+ * "%s | Acme Co". discourageSearchEngines is advertised in the public site
63
+ * config so a template can emit a disallow robots.txt and a noindex tag; this
64
+ * package only stores and serves it.
65
+ */
66
+ export const seoSettingsSchema = z.object({
67
+ defaultTitle: z.string().max(120).optional(),
68
+ titleTemplate: z.string().max(160).optional(),
69
+ metaDescription: z.string().max(300).optional(),
70
+ ogTitle: z.string().max(120).optional(),
71
+ ogDescription: z.string().max(300).optional(),
72
+ ogImageUrl: z.string().url().optional(),
73
+ twitterHandle: z.string().max(30).optional(),
74
+ discourageSearchEngines: z.boolean().default(false),
75
+ });
76
+ /**
77
+ * A flat optional scalar, not a nested object: there is exactly one value to
78
+ * configure, so nesting would only reproduce the all-or-nothing footgun the
79
+ * update-variant comment below exists to avoid, for no benefit.
80
+ * ""-accepted-then-normalized-to-null on read, the same clear protocol the
81
+ * post content fields use, since JSON can express "" but not undefined.
82
+ */
83
+ const gaMeasurementIdSchema = z
84
+ .string()
85
+ .regex(/^G-[A-Z0-9]{4,}$/, 'must look like a GA4 measurement ID (G-XXXXXXX)')
86
+ .or(z.literal(''))
87
+ .optional();
88
+ /**
89
+ * Brand asset slots: media URLs for the minimal icon set (favicon.ico,
90
+ * icon.svg, apple-touch-icon, 192 and 512 plus maskable) and the display logos.
91
+ * Every slot is optional and the object defaults to `{}`, so a site owner who
92
+ * never opens the Brand screen still gets a fully-formed object rather than
93
+ * undefined, and consumers never need optional chaining. The OG default is not
94
+ * duplicated here; `seo.ogImageUrl` already covers it. Generating the icon set
95
+ * from one source image is out of scope, since it drags in image resizing;
96
+ * each slot is an already-correctly-sized upload.
97
+ */
98
+ export const brandSettingsSchema = z.object({
99
+ logoUrl: z.string().url().optional(),
100
+ logoDarkUrl: z.string().url().optional(),
101
+ iconSvgUrl: z.string().url().optional(),
102
+ faviconIcoUrl: z.string().url().optional(),
103
+ appleTouchIconUrl: z.string().url().optional(),
104
+ icon192Url: z.string().url().optional(),
105
+ icon512Url: z.string().url().optional(),
106
+ icon512MaskUrl: z.string().url().optional(),
107
+ });
108
+ /**
109
+ * AEO settings (0.1 headline feature). `siteSummary` is the one sentence an
110
+ * answer engine should be able to lift about the site as a whole; it heads the
111
+ * generated llms.txt and is the site-level counterpart of a post's
112
+ * `answerSummary`. The generated document can be turned off entirely, and the
113
+ * post cap bounds it so a large site does not publish a megabyte of index.
114
+ */
115
+ export const aeoSettingsSchema = z.object({
116
+ siteSummary: z.string().max(1000).optional(),
117
+ llmsTxtEnabled: z.boolean().default(true),
118
+ llmsTxtMaxPosts: z.number().int().min(1).max(500).default(100),
119
+ });
120
+ export const siteSettingsSchema = z.object({
121
+ allowedOrigins: z.array(z.string()).default([]),
122
+ modules: moduleFlagsSchema.default({
123
+ content: true,
124
+ forms: true,
125
+ analytics: true,
126
+ media: true,
127
+ seo: true,
128
+ }),
129
+ webhooks: z.array(webhookConfigSchema).default([]),
130
+ // Capped: a form notification pings a handful of site admins, not a mailing
131
+ // list, and the bound belongs at the source rather than at the sender.
132
+ notifyEmails: z.array(z.string().email()).max(10).default([]),
133
+ retentionDays: retentionSettingsSchema.default({ visits: 90, consent: 365 }),
134
+ turnstile: turnstileSettingsSchema.default({ enabled: false }),
135
+ sitemapStatic: z
136
+ .array(z.object({
137
+ path: z.string(),
138
+ changefreq: z.string().optional(),
139
+ priority: z.number().min(0).max(1).optional(),
140
+ }))
141
+ .default([]),
142
+ /**
143
+ * Where the frontend serves individual posts, e.g. "/insights" for
144
+ * SITE_URL + "/insights/:slug". Needed to build absolute sitemap <loc> URLs
145
+ * and the JSON-LD `mainEntityOfPage`.
146
+ */
147
+ postsPathPrefix: z.string().default('/insights'),
148
+ contentPaths: contentPathsSchema.default({}),
149
+ publisher: publisherSettingsSchema.optional(),
150
+ seo: seoSettingsSchema.default({}),
151
+ aeo: aeoSettingsSchema.default({ llmsTxtEnabled: true, llmsTxtMaxPosts: 100 }),
152
+ gaMeasurementId: gaMeasurementIdSchema,
153
+ brand: brandSettingsSchema.default({}),
154
+ });
155
+ /**
156
+ * Update-input variants of the nested settings objects with every field
157
+ * REQUIRED and no defaults. The write path (setSettings) shallow-merges the
158
+ * patch at the TOP level and re-parses the result through
159
+ * siteSettingsSchema, whose per-field .default()s would otherwise turn a
160
+ * partial nested object into a silent reset of the omitted siblings:
161
+ * PATCH {modules: {content: false}} would flip the other four flags back
162
+ * to true, {turnstile: {sitekey}} would silently disable turnstile
163
+ * (enabled defaults false), {retentionDays: {visits: 30}} would reset
164
+ * consent to 365. Nested objects are therefore all-or-nothing on update:
165
+ * a partial one is a 400, never a silent surprise. Arrays (webhooks,
166
+ * sitemapStatic, allowedOrigins, notifyEmails) replace wholesale, which
167
+ * is the intuitive PATCH semantic for lists, and publisher already
168
+ * requires its one non-optional field.
169
+ */
170
+ const moduleFlagsUpdateSchema = z.object({
171
+ content: z.boolean(),
172
+ forms: z.boolean(),
173
+ analytics: z.boolean(),
174
+ media: z.boolean(),
175
+ seo: z.boolean(),
176
+ });
177
+ const retentionUpdateSchema = z.object({
178
+ visits: z.number().int().min(1).max(3650),
179
+ consent: z.number().int().min(1).max(3650),
180
+ });
181
+ const turnstileUpdateSchema = z.object({
182
+ enabled: z.boolean(),
183
+ sitekey: z.string().optional(),
184
+ });
185
+ /** All-required for the same reason as the three above: two of its fields default. */
186
+ const aeoUpdateSchema = z.object({
187
+ siteSummary: z.string().max(1000).optional(),
188
+ llmsTxtEnabled: z.boolean(),
189
+ llmsTxtMaxPosts: z.number().int().min(1).max(500),
190
+ });
191
+ export const siteSettingsUpdateInputSchema = siteSettingsSchema.partial().extend({
192
+ modules: moduleFlagsUpdateSchema.optional(),
193
+ retentionDays: retentionUpdateSchema.optional(),
194
+ turnstile: turnstileUpdateSchema.optional(),
195
+ aeo: aeoUpdateSchema.optional(),
196
+ // seo and brand need no update variant: every field in both is optional,
197
+ // so the object replaces wholesale when present (the nested-update
198
+ // convention; omitted optional fields mean cleared, discourageSearchEngines
199
+ // re-fills its false default). Their screens always send the complete
200
+ // object.
201
+ });
202
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../../src/shared/schemas/settings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ export declare const siteDocumentKindSchema: z.ZodEnum<["design-md", "llms-txt"]>;
3
+ export declare const siteDocumentRecordSchema: z.ZodObject<{
4
+ kind: z.ZodEnum<["design-md", "llms-txt"]>;
5
+ content: z.ZodString;
6
+ /**
7
+ * AEO (0.1): the same answer-oriented summary posts carry, on the site's
8
+ * standing documents. A document is often the page an answer engine lands on
9
+ * first, and the body of a design.md or an llms.txt is a long structured
10
+ * text with no quotable opening; this gives it one.
11
+ */
12
+ answerSummary: z.ZodNullable<z.ZodString>;
13
+ updatedAt: z.ZodNullable<z.ZodString>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ updatedAt: string | null;
16
+ content: string;
17
+ answerSummary: string | null;
18
+ kind: "design-md" | "llms-txt";
19
+ }, {
20
+ updatedAt: string | null;
21
+ content: string;
22
+ answerSummary: string | null;
23
+ kind: "design-md" | "llms-txt";
24
+ }>;
25
+ export declare const siteDocumentUpdateInputSchema: z.ZodObject<{
26
+ content: z.ZodString;
27
+ /** "" clears, matching the clear protocol every other optional string uses. */
28
+ answerSummary: z.ZodOptional<z.ZodString>;
29
+ }, "strip", z.ZodTypeAny, {
30
+ content: string;
31
+ answerSummary?: string | undefined;
32
+ }, {
33
+ content: string;
34
+ answerSummary?: string | undefined;
35
+ }>;
36
+ export type SiteDocumentRecord = z.infer<typeof siteDocumentRecordSchema>;
37
+ export type SiteDocumentUpdateInput = z.infer<typeof siteDocumentUpdateInputSchema>;
38
+ //# sourceMappingURL=site-document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-document.d.ts","sourceRoot":"","sources":["../../../src/shared/schemas/site-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,sBAAsB,sCAA8B,CAAC;AAElE,eAAO,MAAM,wBAAwB;;;IAGnC;;;;;OAKG;;;;;;;;;;;;;EAGH,CAAC;AAEH,eAAO,MAAM,6BAA6B;;IAExC,+EAA+E;;;;;;;;EAE/E,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import { SITE_DOCUMENT_KINDS } from '../constants.js';
3
+ export const siteDocumentKindSchema = z.enum(SITE_DOCUMENT_KINDS);
4
+ export const siteDocumentRecordSchema = z.object({
5
+ kind: siteDocumentKindSchema,
6
+ content: z.string(),
7
+ /**
8
+ * AEO (0.1): the same answer-oriented summary posts carry, on the site's
9
+ * standing documents. A document is often the page an answer engine lands on
10
+ * first, and the body of a design.md or an llms.txt is a long structured
11
+ * text with no quotable opening; this gives it one.
12
+ */
13
+ answerSummary: z.string().nullable(),
14
+ updatedAt: z.string().nullable(),
15
+ });
16
+ export const siteDocumentUpdateInputSchema = z.object({
17
+ content: z.string().max(100_000),
18
+ /** "" clears, matching the clear protocol every other optional string uses. */
19
+ answerSummary: z.string().max(600).optional(),
20
+ });
21
+ //# sourceMappingURL=site-document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-document.js","sourceRoot":"","sources":["../../../src/shared/schemas/site-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,IAAI,EAAE,sBAAsB;IAC5B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB;;;;;OAKG;IACH,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC;IAChC,+EAA+E;IAC/E,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ export declare const webhookEventNameSchema: z.ZodEnum<["post.published", "post.unpublished", "form.submission.created"]>;
3
+ export declare const webhookConfigSchema: z.ZodObject<{
4
+ url: z.ZodString;
5
+ events: z.ZodArray<z.ZodEnum<["post.published", "post.unpublished", "form.submission.created"]>, "many">;
6
+ secret: z.ZodString;
7
+ }, "strip", z.ZodTypeAny, {
8
+ url: string;
9
+ events: ("post.published" | "post.unpublished" | "form.submission.created")[];
10
+ secret: string;
11
+ }, {
12
+ url: string;
13
+ events: ("post.published" | "post.unpublished" | "form.submission.created")[];
14
+ secret: string;
15
+ }>;
16
+ /**
17
+ * What a receiver gets. `site` is the site's public URL, which is the only
18
+ * identity a standalone install has (managed we8 sends a tenant id here; there
19
+ * is no tenant to name in this one).
20
+ */
21
+ export declare const webhookEnvelopeSchema: z.ZodObject<{
22
+ event: z.ZodEnum<["post.published", "post.unpublished", "form.submission.created"]>;
23
+ site: z.ZodString;
24
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
25
+ timestamp: z.ZodString;
26
+ }, "strip", z.ZodTypeAny, {
27
+ event: "post.published" | "post.unpublished" | "form.submission.created";
28
+ site: string;
29
+ data: Record<string, unknown>;
30
+ timestamp: string;
31
+ }, {
32
+ event: "post.published" | "post.unpublished" | "form.submission.created";
33
+ site: string;
34
+ data: Record<string, unknown>;
35
+ timestamp: string;
36
+ }>;
37
+ export type WebhookConfig = z.infer<typeof webhookConfigSchema>;
38
+ export type WebhookEnvelope = z.infer<typeof webhookEnvelopeSchema>;
39
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/shared/schemas/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,sBAAsB,8EAAyB,CAAC;AAE7D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;EAI9B,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;EAKhC,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { WEBHOOK_EVENTS } from '../constants.js';
3
+ export const webhookEventNameSchema = z.enum(WEBHOOK_EVENTS);
4
+ export const webhookConfigSchema = z.object({
5
+ url: z.string().url(),
6
+ events: z.array(webhookEventNameSchema).min(1),
7
+ secret: z.string().min(16),
8
+ });
9
+ /**
10
+ * What a receiver gets. `site` is the site's public URL, which is the only
11
+ * identity a standalone install has (managed we8 sends a tenant id here; there
12
+ * is no tenant to name in this one).
13
+ */
14
+ export const webhookEnvelopeSchema = z.object({
15
+ event: webhookEventNameSchema,
16
+ site: z.string(),
17
+ data: z.record(z.string(), z.unknown()),
18
+ timestamp: z.string(),
19
+ });
20
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../../src/shared/schemas/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAE7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACrB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;CAC3B,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,KAAK,EAAE,sBAAsB;IAC7B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IACvC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC,CAAC"}
package/dist/spa.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Bindings } from './env.js';
2
+ export declare function isApiPath(pathname: string): boolean;
3
+ export declare function wantsHtml(request: Request): boolean;
4
+ /**
5
+ * The SPA shell for a client-side route, or null when this request is not one
6
+ * (or when no admin bundle is deployed at all).
7
+ */
8
+ export declare function serveSpaShell(request: Request, env: Bindings): Promise<Response | null>;
9
+ //# sourceMappingURL=spa.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spa.d.ts","sourceRoot":"","sources":["../src/spa.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AA4BzC,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAInD;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEnD;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAsB7F"}
package/dist/spa.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Serving the admin portal out of this same worker.
3
+ *
4
+ * The portal is a single-page application, so every one of its routes has to
5
+ * answer with the same index.html and let the client-side router take over.
6
+ * Cloudflare's asset serving handles the files that really exist; what it
7
+ * cannot know is which of the remaining paths are the SPA's own routes and
8
+ * which are this worker's API. This does: the API prefixes are checked first,
9
+ * and only a document request that is none of them falls through to the shell.
10
+ *
11
+ * Two conditions keep the fallback narrow. It answers GET only, and only when
12
+ * the request actually asked for HTML, so a fetch for a missing JSON file gets
13
+ * the API's 404 envelope rather than a page of markup that would fail to parse
14
+ * somewhere far from the cause.
15
+ *
16
+ * The whole thing is optional. A deployment that binds no assets, which is the
17
+ * API-only mode a headless install runs in, has no ASSETS binding, and this
18
+ * returns null so the normal 404 stands.
19
+ */
20
+ /** Path prefixes this worker owns. Anything under them is never the SPA. */
21
+ const API_PREFIXES = ['/v1/', '/api/'];
22
+ /** Exact paths this worker owns that carry no trailing segment. */
23
+ const API_PATHS = ['/v1', '/api', '/llms.txt'];
24
+ export function isApiPath(pathname) {
25
+ return (API_PATHS.includes(pathname) || API_PREFIXES.some((prefix) => pathname.startsWith(prefix)));
26
+ }
27
+ export function wantsHtml(request) {
28
+ return request.method === 'GET' && (request.headers.get('Accept') ?? '').includes('text/html');
29
+ }
30
+ /**
31
+ * The SPA shell for a client-side route, or null when this request is not one
32
+ * (or when no admin bundle is deployed at all).
33
+ */
34
+ export async function serveSpaShell(request, env) {
35
+ const assets = env.ASSETS;
36
+ if (!assets)
37
+ return null;
38
+ const url = new URL(request.url);
39
+ if (isApiPath(url.pathname) || !wantsHtml(request))
40
+ return null;
41
+ const shell = await assets.fetch(new Request(new URL('/index.html', url), { method: 'GET' }));
42
+ // An assets binding with no bundle behind it answers 404 for index.html too,
43
+ // which is the API-only case: fall through rather than serving that 404 as
44
+ // though it were the admin.
45
+ if (!shell.ok)
46
+ return null;
47
+ return new Response(shell.body, {
48
+ status: 200,
49
+ headers: {
50
+ 'Content-Type': 'text/html; charset=UTF-8',
51
+ // The shell names hashed asset files, so it must never be the stale copy
52
+ // pointing at a bundle that no longer exists.
53
+ 'Cache-Control': 'no-cache',
54
+ },
55
+ });
56
+ }
57
+ //# sourceMappingURL=spa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spa.js","sourceRoot":"","sources":["../src/spa.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA4E;AAC5E,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEvC,mEAAmE;AACnE,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;AAE/C,MAAM,UAAU,SAAS,CAAC,QAAgB;IACxC,OAAO,CACL,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAC3F,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAgB;IACxC,OAAO,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACjG,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgB,EAAE,GAAa;IACjE,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IAC1B,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAEhE,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC,aAAa,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAC9F,6EAA6E;IAC7E,2EAA2E;IAC3E,4BAA4B;IAC5B,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAE3B,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE;QAC9B,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACP,cAAc,EAAE,0BAA0B;YAC1C,yEAAyE;YACzE,8CAA8C;YAC9C,eAAe,EAAE,UAAU;SAC5B;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,39 @@
1
+ -- 0001: site settings plus the two operational tables every later module
2
+ -- leans on (rate limiting and the webhook delivery log).
3
+ --
4
+ -- This sequence is a fresh single-site schema, not a replay of managed we8's
5
+ -- migration history: every later ALTER there is folded into the CREATE here,
6
+ -- so a self-hosted database starts at the shape the code actually reads.
7
+ --
8
+ -- Id strategy: operational tables use INTEGER PRIMARY KEY AUTOINCREMENT (cheap,
9
+ -- nothing outside the database references them); public-facing entities use
10
+ -- TEXT ids the application generates with crypto.randomUUID().
11
+
12
+ CREATE TABLE site_settings (
13
+ key TEXT PRIMARY KEY,
14
+ value TEXT NOT NULL, -- JSON, one row per top-level settings key
15
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
16
+ );
17
+
18
+ CREATE TABLE rate_events (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ bucket TEXT NOT NULL,
21
+ ip_hash TEXT NOT NULL,
22
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
23
+ );
24
+
25
+ CREATE INDEX idx_rate_events_bucket_ip_created
26
+ ON rate_events (bucket, ip_hash, created_at);
27
+
28
+ CREATE TABLE webhook_deliveries (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ event TEXT NOT NULL,
31
+ url TEXT NOT NULL,
32
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | delivered | failed
33
+ response_code INTEGER,
34
+ attempts INTEGER NOT NULL DEFAULT 0,
35
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
36
+ delivered_at TEXT
37
+ );
38
+
39
+ CREATE INDEX idx_webhook_deliveries_created ON webhook_deliveries (created_at DESC);
@@ -0,0 +1,135 @@
1
+ -- 0002: the CMS proper. posts / post_drafts / post_versions is the
2
+ -- draft-stage plus immutable-snapshot pattern: `posts` holds the working copy
3
+ -- and the mirror of the live version, `post_drafts` stages an edit to an
4
+ -- already-published post, and `post_versions` is insert-only history that
5
+ -- public reads are served from.
6
+ --
7
+ -- answer_summary and question_heading are the 0.1 AEO content fields. They sit
8
+ -- on all three content-carrying tables so they flow through the same
9
+ -- working-copy, draft, and publish machinery as every other content field,
10
+ -- with no separate code path.
11
+ --
12
+ -- Foreign keys are declared for documentation; D1 does not enforce them by
13
+ -- default, so delete cascades happen in the application layer.
14
+
15
+ CREATE TABLE authors (
16
+ id TEXT PRIMARY KEY,
17
+ slug TEXT NOT NULL UNIQUE,
18
+ name TEXT NOT NULL,
19
+ type TEXT NOT NULL DEFAULT 'team' CHECK (type IN ('team', 'guest', 'advisor', 'partner')),
20
+ role_title TEXT,
21
+ avatar_url TEXT,
22
+ bio TEXT,
23
+ links TEXT NOT NULL DEFAULT '[]', -- JSON {label,url}[]
24
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
25
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
26
+ );
27
+
28
+ CREATE INDEX idx_authors_slug ON authors (slug);
29
+
30
+ CREATE TABLE posts (
31
+ id TEXT PRIMARY KEY,
32
+ slug TEXT NOT NULL UNIQUE,
33
+ -- Structural, like slug: type picks the site section and the JSON-LD type,
34
+ -- so it is editable only while never-published and locked after.
35
+ type TEXT NOT NULL DEFAULT 'blog',
36
+ title TEXT NOT NULL,
37
+ excerpt TEXT,
38
+ content TEXT NOT NULL,
39
+ cover_image_url TEXT,
40
+ category TEXT,
41
+ tags TEXT NOT NULL DEFAULT '[]', -- JSON string[]
42
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
43
+ og_title TEXT,
44
+ og_description TEXT,
45
+ og_image_url TEXT,
46
+ answer_summary TEXT,
47
+ question_heading TEXT,
48
+ cta TEXT, -- JSON Cta, NULL means no CTA configured
49
+ current_version_id TEXT,
50
+ current_version_number INTEGER,
51
+ view_count INTEGER NOT NULL DEFAULT 0,
52
+ like_count INTEGER NOT NULL DEFAULT 0,
53
+ word_count INTEGER,
54
+ published_at TEXT,
55
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
56
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
57
+ );
58
+
59
+ CREATE INDEX idx_posts_slug ON posts (slug);
60
+ CREATE INDEX idx_posts_status ON posts (status);
61
+ CREATE INDEX idx_posts_type ON posts (type);
62
+ CREATE INDEX idx_posts_published_at ON posts (published_at DESC);
63
+
64
+ CREATE TABLE post_drafts (
65
+ post_id TEXT PRIMARY KEY REFERENCES posts (id),
66
+ title TEXT NOT NULL,
67
+ excerpt TEXT,
68
+ content TEXT NOT NULL,
69
+ cover_image_url TEXT,
70
+ category TEXT,
71
+ tags TEXT NOT NULL DEFAULT '[]',
72
+ og_title TEXT,
73
+ og_description TEXT,
74
+ og_image_url TEXT,
75
+ answer_summary TEXT,
76
+ question_heading TEXT,
77
+ cta TEXT,
78
+ author_ids TEXT NOT NULL DEFAULT '[]', -- JSON string[]
79
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
80
+ );
81
+
82
+ CREATE TABLE post_versions (
83
+ id TEXT PRIMARY KEY,
84
+ post_id TEXT NOT NULL REFERENCES posts (id),
85
+ version INTEGER NOT NULL,
86
+ title TEXT NOT NULL,
87
+ excerpt TEXT,
88
+ content TEXT NOT NULL,
89
+ cover_image_url TEXT,
90
+ category TEXT,
91
+ tags TEXT NOT NULL DEFAULT '[]',
92
+ og_title TEXT,
93
+ og_description TEXT,
94
+ og_image_url TEXT,
95
+ answer_summary TEXT,
96
+ question_heading TEXT,
97
+ cta TEXT,
98
+ author_ids TEXT NOT NULL DEFAULT '[]',
99
+ word_count INTEGER,
100
+ note TEXT,
101
+ published_at TEXT NOT NULL DEFAULT (datetime('now')),
102
+ published_by TEXT,
103
+ UNIQUE (post_id, version)
104
+ );
105
+
106
+ CREATE INDEX idx_post_versions_post_version ON post_versions (post_id, version DESC);
107
+
108
+ CREATE TABLE post_authors (
109
+ post_id TEXT NOT NULL REFERENCES posts (id),
110
+ author_id TEXT NOT NULL REFERENCES authors (id),
111
+ position INTEGER NOT NULL DEFAULT 0,
112
+ PRIMARY KEY (post_id, author_id)
113
+ );
114
+
115
+ CREATE INDEX idx_post_authors_post_position ON post_authors (post_id, position);
116
+ CREATE INDEX idx_post_authors_author ON post_authors (author_id);
117
+
118
+ -- version_number stamps which version was live when the view or like happened.
119
+ -- Nullable because a row can be recorded against a post that has never been
120
+ -- published, which has no version to attribute to.
121
+ CREATE TABLE post_views (
122
+ post_id TEXT NOT NULL REFERENCES posts (id),
123
+ session_id TEXT NOT NULL,
124
+ version_number INTEGER,
125
+ viewed_at TEXT NOT NULL DEFAULT (datetime('now')),
126
+ PRIMARY KEY (post_id, session_id)
127
+ );
128
+
129
+ CREATE TABLE post_likes (
130
+ post_id TEXT NOT NULL REFERENCES posts (id),
131
+ session_id TEXT NOT NULL,
132
+ version_number INTEGER,
133
+ liked_at TEXT NOT NULL DEFAULT (datetime('now')),
134
+ PRIMARY KEY (post_id, session_id)
135
+ );
@@ -0,0 +1,35 @@
1
+ -- 0003: forms and their submissions. A form is a first-class, named, typed
2
+ -- entity embedded by its stable `form_key`; the submissions table is the
3
+ -- inbox of record, which is why a form with submissions is archived rather
4
+ -- than deleted.
5
+
6
+ CREATE TABLE forms (
7
+ id TEXT PRIMARY KEY,
8
+ form_key TEXT NOT NULL UNIQUE,
9
+ name TEXT NOT NULL,
10
+ type TEXT NOT NULL DEFAULT 'custom',
11
+ settings TEXT NOT NULL DEFAULT '{}', -- JSON: {notifyEmails?, turnstileEnabled?, honeypotField?, successMessage?}
12
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
13
+ updated_at TEXT,
14
+ archived_at TEXT
15
+ );
16
+
17
+ CREATE TABLE submissions (
18
+ id TEXT PRIMARY KEY,
19
+ form_id TEXT NOT NULL REFERENCES forms (id),
20
+ submitted_at TEXT NOT NULL DEFAULT (datetime('now')),
21
+ payload TEXT NOT NULL, -- JSON: the full submitted body, capped by the application
22
+ email TEXT,
23
+ name TEXT,
24
+ status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new', 'reviewing', 'resolved', 'spam', 'archived')),
25
+ notes TEXT,
26
+ ip_hash TEXT,
27
+ user_agent TEXT,
28
+ source_page TEXT,
29
+ referrer TEXT
30
+ );
31
+
32
+ CREATE INDEX idx_submissions_submitted_at ON submissions (submitted_at DESC);
33
+ CREATE INDEX idx_submissions_status ON submissions (status);
34
+ CREATE INDEX idx_submissions_email ON submissions (email);
35
+ CREATE INDEX idx_submissions_form ON submissions (form_id);