@rimelight/cms 0.0.1 → 0.0.3

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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +16 -14
  3. package/src/admin/api/media-file.ts +39 -0
  4. package/src/admin/api/media.ts +199 -0
  5. package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
  6. package/src/admin/layouts/DefaultParentLayout.astro +50 -0
  7. package/src/admin/pages/assets.astro +424 -0
  8. package/src/admin/pages/byline-schema.astro +62 -0
  9. package/src/admin/pages/bylines.astro +16 -0
  10. package/src/admin/pages/categories.astro +21 -0
  11. package/src/admin/pages/content-types.astro +55 -0
  12. package/src/admin/pages/dashboard.astro +61 -0
  13. package/src/admin/pages/pages-edit.astro +595 -0
  14. package/src/admin/pages/pages-index.astro +146 -0
  15. package/src/admin/pages/pages-new.astro +130 -0
  16. package/src/admin/pages/pages-preview.astro +111 -0
  17. package/src/admin/pages/pages-review.astro +236 -0
  18. package/src/admin/pages/settings.astro +203 -0
  19. package/src/admin/pages/tags.astro +21 -0
  20. package/src/admin/pages/templates-edit.astro +234 -0
  21. package/src/admin/pages/templates-index.astro +95 -0
  22. package/src/admin/pages/templates-new.astro +193 -0
  23. package/src/admin/pages/users.astro +74 -0
  24. package/src/admin/pages/versions.astro +74 -0
  25. package/src/astro/BlockRenderer.astro +44 -96
  26. package/src/astro/PageRenderer.astro +9 -2
  27. package/src/astro/blocks/CalloutBlock.astro +7 -7
  28. package/src/astro/blocks/CardBlock.astro +5 -5
  29. package/src/astro/blocks/CodeBlock.astro +2 -2
  30. package/src/astro/blocks/FileTreeBlock.astro +7 -7
  31. package/src/astro/blocks/ImageBlock.astro +2 -2
  32. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  33. package/src/astro/blocks/ParagraphBlock.astro +3 -3
  34. package/src/astro/blocks/SectionBlock.astro +6 -6
  35. package/src/astro/blocks/StepItemBlock.astro +3 -3
  36. package/src/astro/blocks/StepsBlock.astro +1 -1
  37. package/src/astro/blocks/TableBlock.astro +6 -6
  38. package/src/astro/blocks/TabsBlock.astro +9 -9
  39. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  40. package/src/astro/blocks/index.ts +35 -0
  41. package/src/client/components/RimelightBlock.tsx +19 -19
  42. package/src/client/components/RimelightBlockPicker.tsx +9 -13
  43. package/src/client/components/RimelightBylinesManager.tsx +438 -0
  44. package/src/client/components/RimelightEditor.tsx +3 -3
  45. package/src/client/components/RimelightPreview.tsx +37 -45
  46. package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
  47. package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
  48. package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
  49. package/src/client/components/RimelightTemplateEditor.tsx +6 -8
  50. package/src/client/components/editors/CalloutEditor.tsx +2 -4
  51. package/src/client/components/editors/CardsEditor.tsx +2 -4
  52. package/src/client/components/editors/CodeEditor.tsx +1 -3
  53. package/src/client/components/editors/FileTreeEditor.tsx +2 -4
  54. package/src/client/components/editors/ImageEditor.tsx +3 -3
  55. package/src/client/components/editors/ParagraphEditor.tsx +2 -2
  56. package/src/client/components/editors/SceneEditor.tsx +6 -12
  57. package/src/client/components/editors/ScriptEditor.tsx +8 -16
  58. package/src/client/components/editors/SectionEditor.tsx +4 -4
  59. package/src/client/components/editors/StepItemEditor.tsx +2 -2
  60. package/src/client/components/editors/StepsEditor.tsx +2 -4
  61. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  62. package/src/client/components/editors/TableEditor.tsx +5 -5
  63. package/src/client/components/editors/TabsEditor.tsx +2 -4
  64. package/src/client/components/index.ts +3 -0
  65. package/src/client/loader.ts +3 -0
  66. package/src/core/diff.ts +145 -0
  67. package/src/core/excerpt.ts +159 -0
  68. package/src/core/preview.ts +87 -0
  69. package/src/core/registry.ts +0 -39
  70. package/src/core/types/blocks.ts +83 -6
  71. package/src/core/types/bylines.ts +14 -0
  72. package/src/core/types/media.ts +13 -0
  73. package/src/core/types/taxonomies.ts +20 -0
  74. package/src/cron/scheduled.ts +57 -0
  75. package/src/docs/agent.ts +116 -0
  76. package/src/docs/config.ts +25 -0
  77. package/src/docs/index.ts +5 -0
  78. package/src/docs/routing.ts +104 -0
  79. package/src/docs/sidebar.ts +251 -0
  80. package/src/docs/toc.ts +47 -0
  81. package/src/env.d.ts +13 -0
  82. package/src/index.ts +40 -6
  83. package/src/integration.ts +196 -0
  84. package/src/loader/live.ts +254 -0
  85. package/src/markdown/index.ts +0 -1
  86. package/src/mcp/index.ts +228 -0
  87. package/src/schema/bylines.ts +23 -0
  88. package/src/schema/index.ts +3 -0
  89. package/src/schema/page_versions.ts +2 -0
  90. package/src/schema/pages.ts +2 -0
  91. package/src/schema/site_settings.ts +41 -0
  92. package/src/schema/taxonomies.ts +49 -0
  93. package/src/services/bylines.ts +74 -0
  94. package/src/services/search-indexer.ts +61 -0
  95. package/src/services/site-settings.ts +108 -0
  96. package/src/services/taxonomies.ts +276 -0
  97. package/src/storage/index.ts +253 -0
  98. package/src/astro/blocks/DialogueBlock.astro +0 -23
  99. package/src/astro/blocks/SceneBlock.astro +0 -65
  100. package/src/astro/blocks/ScriptBlock.astro +0 -59
  101. package/src/core/template-sync.ts +0 -75
  102. package/src/markdown/parser.ts +0 -347
  103. package/src/migration.ts +0 -155
  104. package/src/search/sync.ts +0 -32
@@ -0,0 +1,254 @@
1
+ import { and, desc, eq, isNotNull, isNull, lte, sql, type SQL } from "drizzle-orm"
2
+ import { pages } from "../schema/pages.ts"
3
+ import { createPageExcerpt } from "../core/excerpt.ts"
4
+ import type { BaseBlock } from "../core/types/blocks.ts"
5
+ import type { Localized, PageType } from "../core/types/pages.ts"
6
+
7
+ import { taxonomyTerms, pageTaxonomyTerms } from "../schema/taxonomies.ts"
8
+
9
+ export interface CmsLoaderOptions {
10
+ type?: PageType | undefined
11
+ db?: any
12
+ locale?: string | undefined
13
+ includeDrafts?: boolean | undefined
14
+ includeScheduled?: boolean | undefined
15
+ taxonomy?:
16
+ | {
17
+ category?: string | undefined
18
+ tag?: string | undefined
19
+ [key: string]: string | undefined
20
+ }
21
+ | undefined
22
+ }
23
+
24
+ export interface CmsPageData {
25
+ id: string
26
+ slug: string
27
+ type: PageType
28
+ title: Localized
29
+ resolvedTitle: string
30
+ description?: Localized
31
+ resolvedDescription: string
32
+ excerpt: string
33
+ tags: Localized[]
34
+ authorIds: string[]
35
+ blocks: BaseBlock[]
36
+ properties: Record<string, any>
37
+ publishedVersionId: string | null
38
+ postedAt: Date | null
39
+ createdAt: Date
40
+ updatedAt: Date | null
41
+ }
42
+
43
+ export interface CmsEntry {
44
+ id: string
45
+ data: CmsPageData
46
+ }
47
+
48
+ export interface CacheHint {
49
+ tags?: string[]
50
+ lastModified?: Date
51
+ maxAge?: number
52
+ }
53
+
54
+ function resolveLocalized(val: unknown, locale: string = "en"): string {
55
+ if (!val) return ""
56
+ if (typeof val === "string") return val
57
+ if (typeof val === "object" && val !== null) {
58
+ const rec = val as Record<string, string>
59
+ return rec[locale] || rec.en || Object.values(rec)[0] || ""
60
+ }
61
+ return typeof val === "number" ? String(val) : ""
62
+ }
63
+
64
+ function transformPageToEntry(rawPage: any, locale: string = "en"): CmsEntry {
65
+ let contentObj = rawPage.content
66
+ if (typeof contentObj === "string") {
67
+ try {
68
+ contentObj = JSON.parse(contentObj)
69
+ } catch {
70
+ contentObj = { blocks: [], properties: {} }
71
+ }
72
+ } else if (!contentObj || typeof contentObj !== "object") {
73
+ contentObj = { blocks: [], properties: {} }
74
+ }
75
+
76
+ const blocks: BaseBlock[] = Array.isArray(contentObj.blocks) ? contentObj.blocks : []
77
+ const properties: Record<string, any> = contentObj.properties || {}
78
+
79
+ const resolvedTitle = resolveLocalized(rawPage.title, locale)
80
+ const resolvedDescription = resolveLocalized(rawPage.description, locale)
81
+ const excerpt = createPageExcerpt(rawPage, { locale, maxLength: 160 })
82
+
83
+ return {
84
+ id: rawPage.slug || rawPage.id,
85
+ data: {
86
+ id: rawPage.id,
87
+ slug: rawPage.slug,
88
+ type: rawPage.type,
89
+ title: rawPage.title,
90
+ resolvedTitle,
91
+ description: rawPage.description,
92
+ resolvedDescription,
93
+ excerpt,
94
+ tags: rawPage.tags || [],
95
+ authorIds: rawPage.authorIds || [],
96
+ blocks,
97
+ properties,
98
+ publishedVersionId: rawPage.publishedVersionId,
99
+ postedAt: rawPage.postedAt ? new Date(rawPage.postedAt) : null,
100
+ createdAt: new Date(rawPage.createdAt),
101
+ updatedAt: rawPage.updatedAt ? new Date(rawPage.updatedAt) : null
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Direct querying helper for CMS collections.
108
+ */
109
+ export async function getCmsCollection(
110
+ db: any,
111
+ options: CmsLoaderOptions & {
112
+ limit?: number | undefined
113
+ offset?: number | undefined
114
+ where?: SQL | undefined
115
+ } = {}
116
+ ): Promise<{ entries: CmsEntry[]; cacheHint: CacheHint }> {
117
+ const {
118
+ type,
119
+ locale = "en",
120
+ includeDrafts = false,
121
+ includeScheduled = false,
122
+ limit,
123
+ offset,
124
+ where: customWhere
125
+ } = options
126
+
127
+ if (!db) {
128
+ return { entries: [], cacheHint: { tags: ["cms"] } }
129
+ }
130
+
131
+ const conditions: SQL[] = [isNull(pages.deletedAt)]
132
+
133
+ if (type) {
134
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
135
+ conditions.push(eq(pages.type, type as PageType))
136
+ }
137
+
138
+ if (!includeDrafts) {
139
+ conditions.push(isNotNull(pages.publishedVersionId))
140
+ }
141
+
142
+ if (!includeScheduled) {
143
+ conditions.push(lte(pages.postedAt, new Date()))
144
+ }
145
+
146
+ if (customWhere) {
147
+ conditions.push(customWhere)
148
+ }
149
+
150
+ if (options.taxonomy) {
151
+ for (const [taxName, termSlug] of Object.entries(options.taxonomy)) {
152
+ if (termSlug) {
153
+ conditions.push(
154
+ sql`${pages.id} IN (
155
+ SELECT ${pageTaxonomyTerms.pageId} FROM ${pageTaxonomyTerms}
156
+ INNER JOIN ${taxonomyTerms} ON ${taxonomyTerms.id} = ${pageTaxonomyTerms.termId}
157
+ WHERE ${taxonomyTerms.taxonomy} = ${taxName} AND ${taxonomyTerms.slug} = ${termSlug}
158
+ )`
159
+ )
160
+ }
161
+ }
162
+ }
163
+
164
+ let query = db
165
+ .select()
166
+ .from(pages)
167
+ .where(and(...conditions))
168
+ .orderBy(desc(pages.postedAt))
169
+
170
+ if (typeof limit === "number") {
171
+ query = query.limit(limit)
172
+ }
173
+
174
+ if (typeof offset === "number") {
175
+ query = query.offset(offset)
176
+ }
177
+
178
+ const rows = await query
179
+ const entries = rows.map((r: any) => transformPageToEntry(r, locale))
180
+
181
+ let lastModified = new Date(0)
182
+ for (const entry of entries) {
183
+ if (entry.data.updatedAt && entry.data.updatedAt > lastModified) {
184
+ lastModified = entry.data.updatedAt
185
+ }
186
+ }
187
+
188
+ const tags = ["cms"]
189
+ if (type) tags.push(`cms:${type}`)
190
+
191
+ return {
192
+ entries,
193
+ cacheHint: {
194
+ tags,
195
+ lastModified: lastModified.getTime() > 0 ? lastModified : new Date()
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Direct querying helper for a single CMS entry by slug or id.
202
+ */
203
+ export async function getCmsEntry(
204
+ db: any,
205
+ slugOrId: string,
206
+ options: CmsLoaderOptions = {}
207
+ ): Promise<{ entry: CmsEntry | null; cacheHint: CacheHint }> {
208
+ const { entries, cacheHint } = await getCmsCollection(db, {
209
+ ...options,
210
+ limit: 1,
211
+ where: eq(pages.slug, slugOrId)
212
+ })
213
+
214
+ const entry = entries[0] || null
215
+ if (entry) {
216
+ cacheHint.tags?.push(`page:${entry.data.slug}`)
217
+ }
218
+
219
+ return { entry, cacheHint }
220
+ }
221
+
222
+ /**
223
+ * Astro Live Loader for CMS collections. Use inside src/live.config.ts with defineLiveCollection({
224
+ * loader: rimelightCmsLoader({ type: 'blog' }) })
225
+ */
226
+ export function rimelightCmsLoader(loaderOptions: CmsLoaderOptions = {}) {
227
+ return {
228
+ name: "rimelight-cms-loader",
229
+ loadCollection: async (context: {
230
+ filter?: { limit?: number; offset?: number }
231
+ collection?: string
232
+ }) => {
233
+ const db = loaderOptions.db || (globalThis as any).db
234
+ const limit = typeof context.filter?.limit === "number" ? context.filter.limit : undefined
235
+ const offset = typeof context.filter?.offset === "number" ? context.filter.offset : undefined
236
+
237
+ return getCmsCollection(db, {
238
+ ...loaderOptions,
239
+ limit,
240
+ offset
241
+ })
242
+ },
243
+ loadEntry: async (context: { filter: { id: string }; collection?: string }) => {
244
+ const db = loaderOptions.db || (globalThis as any).db
245
+ const { entry, cacheHint } = await getCmsEntry(db, context.filter.id, loaderOptions)
246
+ if (!entry) return undefined
247
+ return {
248
+ id: entry.id,
249
+ data: entry.data,
250
+ cacheHint
251
+ }
252
+ }
253
+ }
254
+ }
@@ -1,2 +1 @@
1
1
  export * from "./serializer.ts"
2
- export * from "./parser.ts"
@@ -0,0 +1,228 @@
1
+ import { and, desc, eq, isNotNull, isNull } from "drizzle-orm"
2
+ import { pages } from "../schema/pages.ts"
3
+ import { pageDrafts } from "../schema/page_drafts.ts"
4
+ import { getSiteSettings, updateSiteSettings } from "../services/site-settings.ts"
5
+ import type { PageType } from "../core/types/pages.ts"
6
+ import type { BaseBlock } from "../core/types/blocks.ts"
7
+
8
+ export const CMS_MCP_TOOLS = [
9
+ {
10
+ name: "cms_list_pages",
11
+ description: "List CMS pages with optional filters for type, draft status, and pagination.",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ type: {
16
+ type: "string",
17
+ description: "Filter by page type (e.g. 'blog', 'docs', 'landing')"
18
+ },
19
+ includeDrafts: { type: "boolean", description: "Whether to include unpublished drafts" },
20
+ limit: { type: "number", description: "Maximum number of pages to return (default 20)" }
21
+ }
22
+ }
23
+ },
24
+ {
25
+ name: "cms_get_page",
26
+ description:
27
+ "Get full details, block tree, and properties for a specific CMS page by slug or id.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ slugOrId: { type: "string", description: "Page slug or UUID" }
32
+ },
33
+ required: ["slugOrId"]
34
+ }
35
+ },
36
+ {
37
+ name: "cms_create_draft",
38
+ description: "Create or update a draft version for a CMS page.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ pageId: { type: "string", description: "Page UUID" },
43
+ userId: { type: "string", description: "User ID creating the draft" },
44
+ blocks: { type: "array", description: "Array of structured CMS blocks" },
45
+ properties: { type: "object", description: "Page properties object" }
46
+ },
47
+ required: ["pageId", "userId", "blocks"]
48
+ }
49
+ },
50
+ {
51
+ name: "cms_get_site_settings",
52
+ description: "Retrieve current dynamic site settings including branding and SEO defaults.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {}
56
+ }
57
+ },
58
+ {
59
+ name: "cms_update_site_settings",
60
+ description: "Update dynamic site settings (name, description, branding, SEO).",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ name: { type: "string" },
65
+ description: { type: "string" },
66
+ author: { type: "string" },
67
+ email: { type: "string" },
68
+ branding: { type: "object" },
69
+ seo: { type: "object" }
70
+ }
71
+ }
72
+ }
73
+ ]
74
+
75
+ /**
76
+ * Handles Model Context Protocol (MCP) JSON-RPC requests for CMS tools.
77
+ */
78
+ export async function handleCmsMcpRequest(
79
+ body: {
80
+ jsonrpc?: string
81
+ id?: string | number
82
+ method: string
83
+ params?: any
84
+ },
85
+ db: any
86
+ ): Promise<any> {
87
+ const { id = 1, method, params = {} } = body
88
+
89
+ if (method === "tools/list") {
90
+ return {
91
+ jsonrpc: "2.0",
92
+ id,
93
+ result: {
94
+ tools: CMS_MCP_TOOLS
95
+ }
96
+ }
97
+ }
98
+
99
+ if (method === "tools/call") {
100
+ const { name, arguments: args = {} } = params
101
+
102
+ switch (name) {
103
+ case "cms_list_pages": {
104
+ const conditions = [isNull(pages.deletedAt)]
105
+ if (args.type) {
106
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
107
+ conditions.push(eq(pages.type, args.type as PageType))
108
+ }
109
+ if (!args.includeDrafts) {
110
+ conditions.push(isNotNull(pages.publishedVersionId))
111
+ }
112
+
113
+ const queryLimit = typeof args.limit === "number" ? args.limit : 20
114
+ const rows = await db
115
+ .select({
116
+ id: pages.id,
117
+ slug: pages.slug,
118
+ type: pages.type,
119
+ title: pages.title,
120
+ postedAt: pages.postedAt,
121
+ publishedVersionId: pages.publishedVersionId
122
+ })
123
+ .from(pages)
124
+ .where(and(...conditions))
125
+ .orderBy(desc(pages.postedAt))
126
+ .limit(queryLimit)
127
+
128
+ return {
129
+ jsonrpc: "2.0",
130
+ id,
131
+ result: {
132
+ content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
133
+ }
134
+ }
135
+ }
136
+
137
+ case "cms_get_page": {
138
+ const row = await db
139
+ .select()
140
+ .from(pages)
141
+ .where(and(isNull(pages.deletedAt), eq(pages.slug, args.slugOrId)))
142
+ .limit(1)
143
+
144
+ const page = row[0] || null
145
+ return {
146
+ jsonrpc: "2.0",
147
+ id,
148
+ result: {
149
+ content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
150
+ }
151
+ }
152
+ }
153
+
154
+ case "cms_create_draft": {
155
+ const contentPayload = {
156
+ blocks: args.blocks as BaseBlock[],
157
+ properties: args.properties || {}
158
+ }
159
+
160
+ const draft = await db
161
+ .insert(pageDrafts)
162
+ .values({
163
+ pageId: args.pageId,
164
+ updatedBy: args.userId,
165
+ content: contentPayload
166
+ })
167
+ .onConflictDoUpdate({
168
+ target: [pageDrafts.pageId],
169
+ set: {
170
+ content: contentPayload,
171
+ updatedBy: args.userId,
172
+ updatedAt: new Date()
173
+ }
174
+ })
175
+ .returning()
176
+
177
+ return {
178
+ jsonrpc: "2.0",
179
+ id,
180
+ result: {
181
+ content: [{ type: "text", text: JSON.stringify(draft[0], null, 2) }]
182
+ }
183
+ }
184
+ }
185
+
186
+ case "cms_get_site_settings": {
187
+ const settings = await getSiteSettings(db)
188
+ return {
189
+ jsonrpc: "2.0",
190
+ id,
191
+ result: {
192
+ content: [{ type: "text", text: JSON.stringify(settings, null, 2) }]
193
+ }
194
+ }
195
+ }
196
+
197
+ case "cms_update_site_settings": {
198
+ const updated = await updateSiteSettings(db, args)
199
+ return {
200
+ jsonrpc: "2.0",
201
+ id,
202
+ result: {
203
+ content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
204
+ }
205
+ }
206
+ }
207
+
208
+ default:
209
+ return {
210
+ jsonrpc: "2.0",
211
+ id,
212
+ error: {
213
+ code: -32601,
214
+ message: `Method or tool '${name}' not found`
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ return {
221
+ jsonrpc: "2.0",
222
+ id,
223
+ error: {
224
+ code: -32601,
225
+ message: `Unknown method: ${method}`
226
+ }
227
+ }
228
+ }
@@ -0,0 +1,23 @@
1
+ import { pgTable, uuid, text, jsonb, timestamp, uniqueIndex } from "drizzle-orm/pg-core"
2
+
3
+ export const bylines = pgTable(
4
+ "bylines",
5
+ {
6
+ id: uuid("id").primaryKey().defaultRandom(),
7
+ name: text("name").notNull(),
8
+ slug: text("slug").notNull(),
9
+ websiteUrl: text("website_url"),
10
+ bio: text("bio"),
11
+ avatar: text("avatar"),
12
+ userId: text("user_id"),
13
+ socials: jsonb("socials").$type<Record<string, string>>().default({}),
14
+ metadata: jsonb("metadata").$type<Record<string, any>>().default({}),
15
+ createdAt: timestamp("created_at").defaultNow().notNull(),
16
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
17
+ deletedAt: timestamp("deleted_at")
18
+ },
19
+ (table) => [uniqueIndex("bylines_slug_idx").on(table.slug)]
20
+ )
21
+
22
+ export type Byline = typeof bylines.$inferSelect
23
+ export type NewByline = typeof bylines.$inferInsert
@@ -6,3 +6,6 @@ export * from "./page_version_comments.ts"
6
6
  export * from "./page_version_approvals.ts"
7
7
  export * from "./page_templates.ts"
8
8
  export * from "./search.ts"
9
+ export * from "./site_settings.ts"
10
+ export * from "./taxonomies.ts"
11
+ export * from "./bylines.ts"
@@ -2,6 +2,7 @@ import { integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "dri
2
2
  import type { BaseBlock } from "../core/types/blocks"
3
3
  import type { Localized, PageType } from "../core/types/pages"
4
4
  import type { PageVersionStatus } from "../core/types/versioning"
5
+ import type { BylineCredit } from "../core/types/bylines"
5
6
 
6
7
  export const pageVersions = pgTable(
7
8
  "page_versions",
@@ -16,6 +17,7 @@ export const pageVersions = pgTable(
16
17
  description: jsonb("description").$type<Localized>(),
17
18
  tags: jsonb("tags").$type<Localized[]>().default([]).notNull(),
18
19
  authorIds: jsonb("author_ids").$type<string[]>().default([]),
20
+ bylines: jsonb("bylines").$type<BylineCredit[]>().default([]),
19
21
  content: jsonb("content")
20
22
  .$type<{
21
23
  blocks: BaseBlock[]
@@ -2,6 +2,7 @@ import { sql } from "drizzle-orm"
2
2
  import { integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"
3
3
  import type { BaseBlock } from "../core/types/blocks"
4
4
  import type { Localized, PageType } from "../core/types/pages"
5
+ import type { BylineCredit } from "../core/types/bylines"
5
6
  import { pageVersions } from "./page_versions.ts"
6
7
  import { pageTemplates } from "./page_templates.ts"
7
8
 
@@ -17,6 +18,7 @@ export const pages = pgTable(
17
18
  description: jsonb("description").$type<Localized>(),
18
19
  tags: jsonb("tags").$type<Localized[]>().default([]).notNull(),
19
20
  authorIds: jsonb("author_ids").$type<string[]>().default([]),
21
+ bylines: jsonb("bylines").$type<BylineCredit[]>().default([]),
20
22
  /**
21
23
  * Denormalized read-cache of the active published version content (NULL if unpublished draft)
22
24
  */
@@ -0,0 +1,41 @@
1
+ import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"
2
+
3
+ export interface SiteSettingsBranding {
4
+ logo: {
5
+ alt: string
6
+ }
7
+ favicon: {
8
+ svg: string
9
+ }
10
+ colors: {
11
+ themeColor: string
12
+ backgroundColor: string
13
+ }
14
+ }
15
+
16
+ export interface SiteSettingsSeo {
17
+ titleTemplate: string
18
+ ogImageFallback: string
19
+ maxDescriptionLength: number
20
+ authorHandle?: string
21
+ }
22
+
23
+ export const siteSettings = pgTable("site_settings", {
24
+ id: text("id").primaryKey().default("default"),
25
+ name: text("name").notNull(),
26
+ description: text("description").notNull(),
27
+ url: text("url").notNull(),
28
+ ogImage: text("og_image").notNull(),
29
+ author: text("author").notNull(),
30
+ email: text("email").notNull().default(""),
31
+ branding: jsonb("branding").$type<SiteSettingsBranding>().notNull(),
32
+ seo: jsonb("seo").$type<SiteSettingsSeo>().notNull(),
33
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
34
+ updatedAt: timestamp("updated_at", { withTimezone: true })
35
+ .defaultNow()
36
+ .$onUpdate(() => new Date())
37
+ .notNull()
38
+ })
39
+
40
+ export type SiteSettings = typeof siteSettings.$inferSelect
41
+ export type InsertSiteSettings = typeof siteSettings.$inferInsert
@@ -0,0 +1,49 @@
1
+ import {
2
+ foreignKey,
3
+ integer,
4
+ jsonb,
5
+ pgTable,
6
+ text,
7
+ timestamp,
8
+ uniqueIndex,
9
+ uuid
10
+ } from "drizzle-orm/pg-core"
11
+ import { pages } from "./pages.ts"
12
+ import type { Localized } from "../core/types/pages"
13
+
14
+ export const taxonomyTerms = pgTable(
15
+ "taxonomy_terms",
16
+ {
17
+ id: uuid("id").defaultRandom().notNull().primaryKey(),
18
+ taxonomy: text("taxonomy").notNull(), // "category" | "tag" | custom (e.g. "genre")
19
+ slug: text("slug").notNull(),
20
+ label: jsonb("label").$type<Localized | string>().notNull(),
21
+ description: jsonb("description").$type<Localized | string>(),
22
+ parentId: uuid("parent_id"),
23
+ displayOrder: integer("display_order").default(0).notNull(),
24
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
25
+ updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(() => new Date())
26
+ },
27
+ (table) => [
28
+ uniqueIndex("taxonomy_terms_tax_slug_idx").on(table.taxonomy, table.slug),
29
+ foreignKey({
30
+ columns: [table.parentId],
31
+ foreignColumns: [table.id],
32
+ name: "taxonomy_terms_parent_id_fk"
33
+ }).onDelete("cascade")
34
+ ]
35
+ )
36
+
37
+ export const pageTaxonomyTerms = pgTable(
38
+ "page_taxonomy_terms",
39
+ {
40
+ pageId: uuid("page_id")
41
+ .notNull()
42
+ .references(() => pages.id, { onDelete: "cascade" }),
43
+ termId: uuid("term_id")
44
+ .notNull()
45
+ .references(() => taxonomyTerms.id, { onDelete: "cascade" }),
46
+ assignedAt: timestamp("assigned_at", { withTimezone: true }).defaultNow().notNull()
47
+ },
48
+ (table) => [uniqueIndex("page_taxonomy_terms_page_term_idx").on(table.pageId, table.termId)]
49
+ )