@rimelight/cms 0.0.2 → 0.0.4

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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bylines-Csggkl-g.d.mts +405 -0
  3. package/dist/index.d.mts +724 -0
  4. package/dist/index.mjs +2382 -0
  5. package/dist/integration.d.mts +33 -0
  6. package/dist/integration.mjs +161 -0
  7. package/dist/schema/index.d.mts +2063 -0
  8. package/dist/schema/index.mjs +176 -0
  9. package/dist/schema/sqlite.d.mts +2132 -0
  10. package/dist/schema/sqlite.mjs +137 -0
  11. package/dist/storage/index.d.mts +59 -0
  12. package/dist/storage/index.mjs +133 -0
  13. package/package.json +36 -18
  14. package/src/admin/api/media-file.ts +39 -0
  15. package/src/admin/api/media.ts +224 -0
  16. package/src/admin/layouts/CMSDashboardLayout.astro +191 -0
  17. package/src/admin/layouts/DefaultParentLayout.astro +50 -0
  18. package/src/admin/pages/assets.astro +424 -0
  19. package/src/admin/pages/byline-schema.astro +62 -0
  20. package/src/admin/pages/bylines.astro +16 -0
  21. package/src/admin/pages/categories.astro +21 -0
  22. package/src/admin/pages/content-types.astro +55 -0
  23. package/src/admin/pages/dashboard.astro +61 -0
  24. package/src/admin/pages/pages-edit.astro +595 -0
  25. package/src/admin/pages/pages-index.astro +146 -0
  26. package/src/admin/pages/pages-new.astro +130 -0
  27. package/src/admin/pages/pages-preview.astro +111 -0
  28. package/src/admin/pages/pages-review.astro +236 -0
  29. package/src/admin/pages/settings.astro +203 -0
  30. package/src/admin/pages/tags.astro +21 -0
  31. package/src/admin/pages/templates-edit.astro +234 -0
  32. package/src/admin/pages/templates-index.astro +95 -0
  33. package/src/admin/pages/templates-new.astro +193 -0
  34. package/src/admin/pages/users.astro +74 -0
  35. package/src/admin/pages/versions.astro +74 -0
  36. package/src/astro/BlockRenderer.astro +44 -102
  37. package/src/astro/PageRenderer.astro +12 -5
  38. package/src/astro/blocks/CalloutBlock.astro +7 -7
  39. package/src/astro/blocks/CardBlock.astro +5 -5
  40. package/src/astro/blocks/CodeBlock.astro +2 -2
  41. package/src/astro/blocks/FileTreeBlock.astro +7 -7
  42. package/src/astro/blocks/ImageBlock.astro +2 -2
  43. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  44. package/src/astro/blocks/ParagraphBlock.astro +3 -3
  45. package/src/astro/blocks/SectionBlock.astro +6 -6
  46. package/src/astro/blocks/StepItemBlock.astro +3 -3
  47. package/src/astro/blocks/StepsBlock.astro +1 -1
  48. package/src/astro/blocks/TableBlock.astro +6 -6
  49. package/src/astro/blocks/TabsBlock.astro +9 -9
  50. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  51. package/src/astro/blocks/index.ts +35 -0
  52. package/src/client/components/RimelightBlock.tsx +19 -19
  53. package/src/client/components/RimelightBlockPicker.tsx +9 -13
  54. package/src/client/components/RimelightBylinesManager.tsx +438 -0
  55. package/src/client/components/RimelightEditor.tsx +3 -3
  56. package/src/client/components/RimelightPreview.tsx +37 -45
  57. package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
  58. package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
  59. package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
  60. package/src/client/components/RimelightTemplateEditor.tsx +6 -8
  61. package/src/client/components/editors/CalloutEditor.tsx +2 -4
  62. package/src/client/components/editors/CardsEditor.tsx +2 -4
  63. package/src/client/components/editors/CodeEditor.tsx +1 -3
  64. package/src/client/components/editors/FileTreeEditor.tsx +2 -4
  65. package/src/client/components/editors/ImageEditor.tsx +3 -3
  66. package/src/client/components/editors/ParagraphEditor.tsx +2 -2
  67. package/src/client/components/editors/SceneEditor.tsx +6 -12
  68. package/src/client/components/editors/ScriptEditor.tsx +8 -16
  69. package/src/client/components/editors/SectionEditor.tsx +4 -4
  70. package/src/client/components/editors/StepItemEditor.tsx +2 -2
  71. package/src/client/components/editors/StepsEditor.tsx +2 -4
  72. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  73. package/src/client/components/editors/TableEditor.tsx +5 -5
  74. package/src/client/components/editors/TabsEditor.tsx +2 -4
  75. package/src/client/components/index.ts +3 -0
  76. package/src/client/loader.ts +3 -0
  77. package/src/core/diff.ts +145 -0
  78. package/src/core/excerpt.ts +159 -0
  79. package/src/core/preview.ts +87 -0
  80. package/src/core/registry.ts +0 -86
  81. package/src/core/types/blocks.ts +6 -1
  82. package/src/core/types/bylines.ts +14 -0
  83. package/src/core/types/media.ts +13 -0
  84. package/src/core/types/taxonomies.ts +20 -0
  85. package/src/cron/scheduled.ts +57 -0
  86. package/src/docs/agent.ts +116 -0
  87. package/src/docs/config.ts +25 -0
  88. package/src/docs/index.ts +5 -0
  89. package/src/docs/routing.ts +104 -0
  90. package/src/docs/sidebar.ts +251 -0
  91. package/src/docs/toc.ts +47 -0
  92. package/src/env.d.ts +19 -0
  93. package/src/index.ts +57 -12
  94. package/src/integration.ts +250 -0
  95. package/src/loader/live.ts +254 -0
  96. package/src/markdown/index.ts +0 -1
  97. package/src/mcp/index.ts +228 -0
  98. package/src/schema/bylines.ts +23 -0
  99. package/src/schema/index.ts +3 -4
  100. package/src/schema/page_versions.ts +2 -0
  101. package/src/schema/pages.ts +2 -0
  102. package/src/schema/site_settings.ts +41 -0
  103. package/src/schema/sqlite.ts +344 -0
  104. package/src/schema/taxonomies.ts +49 -0
  105. package/src/services/bylines.ts +74 -0
  106. package/src/services/search-indexer.ts +61 -0
  107. package/src/services/site-settings.ts +108 -0
  108. package/src/services/taxonomies.ts +276 -0
  109. package/src/storage/index.ts +256 -0
  110. package/src/astro/blocks/ApiDocumentationBlock.astro +0 -350
  111. package/src/astro/blocks/DialogueBlock.astro +0 -23
  112. package/src/astro/blocks/LivePreviewBlock.astro +0 -27
  113. package/src/astro/blocks/SceneBlock.astro +0 -65
  114. package/src/astro/blocks/ScriptBlock.astro +0 -59
  115. package/src/core/template-sync.ts +0 -75
  116. package/src/markdown/parser.ts +0 -347
  117. package/src/migration-tools.ts +0 -334
  118. package/src/migration.ts +0 -155
  119. package/src/schema/component_dependencies.ts +0 -21
  120. package/src/schema/component_metadata.ts +0 -31
  121. package/src/schema/component_metadata_cache.ts +0 -15
  122. package/src/schema/component_versions.ts +0 -19
  123. package/src/search/sync.ts +0 -32
  124. package/src/services/component-metadata.ts +0 -134
@@ -0,0 +1,344 @@
1
+ import { sql } from "drizzle-orm"
2
+ import {
3
+ sqliteTable,
4
+ text,
5
+ integer,
6
+ uniqueIndex,
7
+ index,
8
+ type AnySQLiteColumn
9
+ } from "drizzle-orm/sqlite-core"
10
+ import type { BaseBlock } from "../core/types/blocks"
11
+ import type { Localized, PageType } from "../core/types/pages"
12
+ import type { BylineCredit } from "../core/types/bylines"
13
+ import type { PageVersionStatus } from "../core/types/versioning"
14
+ import type { TemplateApprovalRules, TemplateRolePermissions } from "../core/types/templates"
15
+
16
+ export interface SiteSettingsBranding {
17
+ logo: {
18
+ alt: string
19
+ }
20
+ favicon: {
21
+ svg: string
22
+ }
23
+ colors: {
24
+ themeColor: string
25
+ backgroundColor: string
26
+ }
27
+ }
28
+
29
+ export interface SiteSettingsSeo {
30
+ titleTemplate: string
31
+ ogImageFallback: string
32
+ maxDescriptionLength: number
33
+ authorHandle?: string
34
+ }
35
+
36
+ // 1. Page Templates
37
+ export const pageTemplates = sqliteTable("page_templates", {
38
+ id: text("id")
39
+ .$defaultFn(() => crypto.randomUUID())
40
+ .notNull()
41
+ .primaryKey(),
42
+ slug: text("slug").notNull().unique(),
43
+ title: text("title", { mode: "json" }).$type<Localized>().notNull(),
44
+ description: text("description", { mode: "json" }).$type<Localized>(),
45
+ pageType: text("page_type").$type<PageType>().notNull(),
46
+ version: integer("version").default(1).notNull(),
47
+ allowedRoles: text("allowed_roles", { mode: "json" })
48
+ .$type<string[]>()
49
+ .default(sql`'[]'`)
50
+ .notNull(),
51
+ rolePermissions: text("role_permissions", { mode: "json" })
52
+ .$type<TemplateRolePermissions>()
53
+ .default(sql`'{"whoCanCreate":[],"whoCanEdit":[],"whoCanReview":[],"whoCanView":[]}'`)
54
+ .notNull(),
55
+ requiredPermission: text("required_permission"),
56
+ approvalRules: text("approval_rules", { mode: "json" })
57
+ .$type<TemplateApprovalRules>()
58
+ .default(sql`'{"allowSelfApproval":true,"minApprovals":1}'`)
59
+ .notNull(),
60
+ defaultProperties: text("default_properties", { mode: "json" })
61
+ .default(sql`'{}'`)
62
+ .notNull(),
63
+ initialBlocks: text("initial_blocks", { mode: "json" })
64
+ .$type<BaseBlock[]>()
65
+ .default(sql`'[]'`)
66
+ .notNull(),
67
+ createdAt: integer("created_at", { mode: "timestamp" })
68
+ .$defaultFn(() => new Date())
69
+ .notNull(),
70
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => new Date())
71
+ })
72
+
73
+ // 2. Page Versions
74
+ export const pageVersions = sqliteTable(
75
+ "page_versions",
76
+ {
77
+ id: text("id")
78
+ .$defaultFn(() => crypto.randomUUID())
79
+ .notNull()
80
+ .primaryKey(),
81
+ pageId: text("page_id").notNull(),
82
+ versionNumber: integer("version_number").notNull(),
83
+ status: text("status").$type<PageVersionStatus>().default("pending").notNull(),
84
+ slug: text("slug").notNull(),
85
+ type: text("type").$type<PageType>().notNull(),
86
+ title: text("title", { mode: "json" }).$type<Localized>().notNull(),
87
+ description: text("description", { mode: "json" }).$type<Localized>(),
88
+ tags: text("tags", { mode: "json" })
89
+ .$type<Localized[]>()
90
+ .default(sql`'[]'`)
91
+ .notNull(),
92
+ authorIds: text("author_ids", { mode: "json" })
93
+ .$type<string[]>()
94
+ .default(sql`'[]'`),
95
+ bylines: text("bylines", { mode: "json" })
96
+ .$type<BylineCredit[]>()
97
+ .default(sql`'[]'`),
98
+ content: text("content", { mode: "json" })
99
+ .$type<{
100
+ blocks: BaseBlock[]
101
+ properties: Record<string, any>
102
+ }>()
103
+ .notNull(),
104
+ createdBy: text("created_by").notNull(),
105
+ approvedBy: text("approved_by", { mode: "json" })
106
+ .$type<string[]>()
107
+ .default(sql`'[]'`)
108
+ .notNull(),
109
+ approvedAt: integer("approved_at", { mode: "timestamp" }),
110
+ changeSummary: text("change_summary"),
111
+ createdAt: integer("created_at", { mode: "timestamp" })
112
+ .$defaultFn(() => new Date())
113
+ .notNull()
114
+ },
115
+ (table) => [
116
+ uniqueIndex("page_versions_page_id_version_idx").on(table.pageId, table.versionNumber)
117
+ ]
118
+ )
119
+
120
+ // 3. Pages
121
+ export const pages = sqliteTable(
122
+ "pages",
123
+ {
124
+ id: text("id")
125
+ .$defaultFn(() => crypto.randomUUID())
126
+ .notNull()
127
+ .primaryKey(),
128
+ slug: text("slug").notNull(),
129
+ type: text("type").$type<PageType>().notNull(),
130
+ templateId: text("template_id").references(() => pageTemplates.id, { onDelete: "set null" }),
131
+ templateVersion: integer("template_version").default(1).notNull(),
132
+ title: text("title", { mode: "json" }).$type<Localized>().notNull(),
133
+ description: text("description", { mode: "json" }).$type<Localized>(),
134
+ tags: text("tags", { mode: "json" })
135
+ .$type<Localized[]>()
136
+ .default(sql`'[]'`)
137
+ .notNull(),
138
+ authorIds: text("author_ids", { mode: "json" })
139
+ .$type<string[]>()
140
+ .default(sql`'[]'`),
141
+ bylines: text("bylines", { mode: "json" })
142
+ .$type<BylineCredit[]>()
143
+ .default(sql`'[]'`),
144
+ content: text("content", { mode: "json" })
145
+ .$type<{
146
+ blocks: BaseBlock[]
147
+ properties: Record<string, any>
148
+ }>()
149
+ .notNull(),
150
+ publishedVersionId: text("published_version_id").references(() => pageVersions.id, {
151
+ onDelete: "set null"
152
+ }),
153
+ postedAt: integer("posted_at", { mode: "timestamp" }),
154
+ createdAt: integer("created_at", { mode: "timestamp" })
155
+ .$defaultFn(() => new Date())
156
+ .notNull(),
157
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => new Date()),
158
+ deletedAt: integer("deleted_at", { mode: "timestamp" })
159
+ },
160
+ (table) => [
161
+ uniqueIndex("pages_slug_active_unique_idx")
162
+ .on(table.slug)
163
+ .where(sql`deleted_at IS NULL`)
164
+ ]
165
+ )
166
+
167
+ // 4. Page Drafts
168
+ export const pageDrafts = sqliteTable("page_drafts", {
169
+ pageId: text("page_id")
170
+ .notNull()
171
+ .primaryKey()
172
+ .references(() => pages.id, { onDelete: "cascade" }),
173
+ content: text("content", { mode: "json" })
174
+ .$type<{
175
+ blocks: BaseBlock[]
176
+ properties: Record<string, any>
177
+ }>()
178
+ .notNull(),
179
+ updatedBy: text("updated_by").notNull(),
180
+ updatedAt: integer("updated_at", { mode: "timestamp" })
181
+ .$defaultFn(() => new Date())
182
+ .notNull()
183
+ })
184
+
185
+ // 5. Page Draft Locks
186
+ export const pageDraftLocks = sqliteTable("page_draft_locks", {
187
+ pageId: text("page_id")
188
+ .notNull()
189
+ .primaryKey()
190
+ .references(() => pages.id, { onDelete: "cascade" }),
191
+ lockedByUserId: text("locked_by_user_id").notNull(),
192
+ lockedByUserName: text("locked_by_user_name").notNull(),
193
+ acquiredAt: integer("acquired_at", { mode: "timestamp" })
194
+ .$defaultFn(() => new Date())
195
+ .notNull(),
196
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull()
197
+ })
198
+
199
+ // 6. Page Version Approvals
200
+ export const pageVersionApprovals = sqliteTable("page_version_approvals", {
201
+ id: text("id")
202
+ .$defaultFn(() => crypto.randomUUID())
203
+ .notNull()
204
+ .primaryKey(),
205
+ versionId: text("version_id")
206
+ .notNull()
207
+ .references(() => pageVersions.id, { onDelete: "cascade" }),
208
+ userId: text("user_id").notNull(),
209
+ userRole: text("user_role").notNull(),
210
+ approvedAt: integer("approved_at", { mode: "timestamp" })
211
+ .$defaultFn(() => new Date())
212
+ .notNull()
213
+ })
214
+
215
+ // 7. Page Version Comments
216
+ export const pageVersionComments = sqliteTable("page_version_comments", {
217
+ id: text("id")
218
+ .$defaultFn(() => crypto.randomUUID())
219
+ .notNull()
220
+ .primaryKey(),
221
+ versionId: text("version_id")
222
+ .notNull()
223
+ .references(() => pageVersions.id, { onDelete: "cascade" }),
224
+ userId: text("user_id").notNull(),
225
+ userRole: text("user_role").notNull(),
226
+ content: text("content").notNull(),
227
+ blockId: text("block_id"),
228
+ createdAt: integer("created_at", { mode: "timestamp" })
229
+ .$defaultFn(() => new Date())
230
+ .notNull()
231
+ })
232
+
233
+ // 8. Search
234
+ export const contentSearchIndex = sqliteTable(
235
+ "content_search_index",
236
+ {
237
+ id: text("id")
238
+ .$defaultFn(() => crypto.randomUUID())
239
+ .notNull()
240
+ .primaryKey(),
241
+ pageId: text("page_id")
242
+ .notNull()
243
+ .references(() => pages.id, { onDelete: "cascade" }),
244
+ locale: text("locale").notNull(),
245
+ titleText: text("title_text").notNull(),
246
+ contentText: text("content_text").notNull()
247
+ },
248
+ (table) => [index("content_search_index_page_id_idx").on(table.pageId)]
249
+ )
250
+
251
+ // 9. Site Settings
252
+ export const siteSettings = sqliteTable("site_settings", {
253
+ id: text("id").primaryKey().default("default"),
254
+ name: text("name").notNull(),
255
+ description: text("description").notNull(),
256
+ url: text("url").notNull(),
257
+ ogImage: text("og_image").notNull(),
258
+ author: text("author").notNull(),
259
+ email: text("email").notNull().default(""),
260
+ branding: text("branding", { mode: "json" }).$type<SiteSettingsBranding>().notNull(),
261
+ seo: text("seo", { mode: "json" }).$type<SiteSettingsSeo>().notNull(),
262
+ createdAt: integer("created_at", { mode: "timestamp" })
263
+ .$defaultFn(() => new Date())
264
+ .notNull(),
265
+ updatedAt: integer("updated_at", { mode: "timestamp" })
266
+ .$defaultFn(() => new Date())
267
+ .$onUpdate(() => new Date())
268
+ .notNull()
269
+ })
270
+
271
+ // 10. Taxonomies
272
+ export const taxonomyTerms = sqliteTable(
273
+ "taxonomy_terms",
274
+ {
275
+ id: text("id")
276
+ .$defaultFn(() => crypto.randomUUID())
277
+ .notNull()
278
+ .primaryKey(),
279
+ taxonomy: text("taxonomy").notNull(),
280
+ slug: text("slug").notNull(),
281
+ label: text("label", { mode: "json" }).$type<Localized | string>().notNull(),
282
+ description: text("description", { mode: "json" }).$type<Localized | string>(),
283
+ parentId: text("parent_id").references((): AnySQLiteColumn => taxonomyTerms.id, {
284
+ onDelete: "cascade"
285
+ }),
286
+ displayOrder: integer("display_order").default(0).notNull(),
287
+ createdAt: integer("created_at", { mode: "timestamp" })
288
+ .$defaultFn(() => new Date())
289
+ .notNull(),
290
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => new Date())
291
+ },
292
+ (table) => [uniqueIndex("taxonomy_terms_tax_slug_idx").on(table.taxonomy, table.slug)]
293
+ )
294
+
295
+ export const pageTaxonomyTerms = sqliteTable(
296
+ "page_taxonomy_terms",
297
+ {
298
+ pageId: text("page_id")
299
+ .notNull()
300
+ .references(() => pages.id, { onDelete: "cascade" }),
301
+ termId: text("term_id")
302
+ .notNull()
303
+ .references(() => taxonomyTerms.id, { onDelete: "cascade" }),
304
+ assignedAt: integer("assigned_at", { mode: "timestamp" })
305
+ .$defaultFn(() => new Date())
306
+ .notNull()
307
+ },
308
+ (table) => [uniqueIndex("page_taxonomy_terms_page_term_idx").on(table.pageId, table.termId)]
309
+ )
310
+
311
+ // 11. Bylines
312
+ export const bylines = sqliteTable(
313
+ "bylines",
314
+ {
315
+ id: text("id")
316
+ .$defaultFn(() => crypto.randomUUID())
317
+ .primaryKey(),
318
+ name: text("name").notNull(),
319
+ slug: text("slug").notNull(),
320
+ websiteUrl: text("website_url"),
321
+ bio: text("bio"),
322
+ avatar: text("avatar"),
323
+ userId: text("user_id"),
324
+ socials: text("socials", { mode: "json" })
325
+ .$type<Record<string, string>>()
326
+ .default(sql`'{}'`),
327
+ metadata: text("metadata", { mode: "json" })
328
+ .$type<Record<string, any>>()
329
+ .default(sql`'{}'`),
330
+ createdAt: integer("created_at", { mode: "timestamp" })
331
+ .$defaultFn(() => new Date())
332
+ .notNull(),
333
+ updatedAt: integer("updated_at", { mode: "timestamp" })
334
+ .$defaultFn(() => new Date())
335
+ .notNull(),
336
+ deletedAt: integer("deleted_at", { mode: "timestamp" })
337
+ },
338
+ (table) => [uniqueIndex("bylines_slug_idx").on(table.slug)]
339
+ )
340
+
341
+ export type Byline = typeof bylines.$inferSelect
342
+ export type NewByline = typeof bylines.$inferInsert
343
+ export type SiteSettings = typeof siteSettings.$inferSelect
344
+ 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
+ )
@@ -0,0 +1,74 @@
1
+ import { eq, and, isNull, or } from "drizzle-orm"
2
+ import { bylines, type Byline, type NewByline } from "../schema/bylines.ts"
3
+
4
+ /**
5
+ * Returns all active (non-deleted) bylines.
6
+ */
7
+ export async function getBylines(db: any): Promise<Byline[]> {
8
+ if (!db) return []
9
+ return db.select().from(bylines).where(isNull(bylines.deletedAt)).orderBy(bylines.name)
10
+ }
11
+
12
+ /**
13
+ * Retrieves a single byline by ID or slug.
14
+ */
15
+ export async function getByline(db: any, idOrSlug: string): Promise<Byline | null> {
16
+ if (!db || !idOrSlug) return null
17
+ const rows = await db
18
+ .select()
19
+ .from(bylines)
20
+ .where(
21
+ and(
22
+ isNull(bylines.deletedAt),
23
+ or(eq(bylines.id, idOrSlug as any), eq(bylines.slug, idOrSlug))
24
+ )
25
+ )
26
+ .limit(1)
27
+
28
+ return rows[0] || null
29
+ }
30
+
31
+ /**
32
+ * Creates a new byline.
33
+ */
34
+ export async function createByline(db: any, data: NewByline): Promise<Byline> {
35
+ const [created] = await db
36
+ .insert(bylines)
37
+ .values({
38
+ ...data,
39
+ updatedAt: new Date()
40
+ })
41
+ .returning()
42
+ return created
43
+ }
44
+
45
+ /**
46
+ * Updates an existing byline.
47
+ */
48
+ export async function updateByline(
49
+ db: any,
50
+ id: string,
51
+ data: Partial<NewByline>
52
+ ): Promise<Byline | null> {
53
+ const [updated] = await db
54
+ .update(bylines)
55
+ .set({
56
+ ...data,
57
+ updatedAt: new Date()
58
+ })
59
+ .where(and(eq(bylines.id, id as any), isNull(bylines.deletedAt)))
60
+ .returning()
61
+ return updated || null
62
+ }
63
+
64
+ /**
65
+ * Soft deletes a byline.
66
+ */
67
+ export async function deleteByline(db: any, id: string): Promise<boolean> {
68
+ const [deleted] = await db
69
+ .update(bylines)
70
+ .set({ deletedAt: new Date(), updatedAt: new Date() })
71
+ .where(and(eq(bylines.id, id as any), isNull(bylines.deletedAt)))
72
+ .returning()
73
+ return !!deleted
74
+ }
@@ -0,0 +1,61 @@
1
+ import { extractTextFromBlocks } from "../core/excerpt"
2
+ import { contentSearchIndex } from "../schema/search"
3
+ import { eq, and, sql } from "drizzle-orm"
4
+
5
+ export async function indexPageForSearch(
6
+ db: any,
7
+ page: {
8
+ id: string
9
+ title: any
10
+ content: any
11
+ },
12
+ locales: string[] = ["en", "pt"]
13
+ ) {
14
+ if (!db) return
15
+
16
+ const parseJson = (val: any) => (typeof val === "string" ? JSON.parse(val) : val)
17
+ const titleObj = parseJson(page.title) || {}
18
+ const contentObj = parseJson(page.content) || {}
19
+ const blocks = contentObj.blocks || []
20
+
21
+ for (const locale of locales) {
22
+ const titleText =
23
+ typeof titleObj === "object" && titleObj !== null
24
+ ? titleObj[locale] || titleObj.en || Object.values(titleObj)[0] || ""
25
+ : String(titleObj || "")
26
+
27
+ const contentText = extractTextFromBlocks(blocks, locale)
28
+
29
+ if (!titleText && !contentText) continue
30
+
31
+ // Upsert search index record
32
+ try {
33
+ const existing = await db
34
+ .select()
35
+ .from(contentSearchIndex)
36
+ .where(and(eq(contentSearchIndex.pageId, page.id), eq(contentSearchIndex.locale, locale)))
37
+ .limit(1)
38
+
39
+ if (existing.length > 0) {
40
+ await db
41
+ .update(contentSearchIndex)
42
+ .set({
43
+ titleText,
44
+ contentText,
45
+ searchVector: sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`
46
+ })
47
+ .where(eq(contentSearchIndex.id, existing[0].id))
48
+ } else {
49
+ await db.insert(contentSearchIndex).values({
50
+ pageId: page.id,
51
+ locale,
52
+ titleText,
53
+ contentText,
54
+ searchVector: sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`
55
+ })
56
+ }
57
+ } catch (err) {
58
+ console.error(`[Search Indexer] Failed to index page ${page.id} for locale ${locale}:`, err)
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,108 @@
1
+ import { eq } from "drizzle-orm"
2
+ import {
3
+ siteSettings,
4
+ type InsertSiteSettings,
5
+ type SiteSettings
6
+ } from "../schema/site_settings.ts"
7
+
8
+ export const DEFAULT_SITE_SETTINGS: InsertSiteSettings = {
9
+ id: "default",
10
+ name: "Rimelight",
11
+ description: "Built with Rimelight CMS",
12
+ url: "https://rimelight.com",
13
+ ogImage: "https://cdn.rimelight.com/Images/og-placeholder.webp",
14
+ author: "Rimelight",
15
+ email: "",
16
+ branding: {
17
+ logo: {
18
+ alt: "Rimelight"
19
+ },
20
+ favicon: {
21
+ svg: "/favicon.svg"
22
+ },
23
+ colors: {
24
+ themeColor: "#ffffff",
25
+ backgroundColor: "#ffffff"
26
+ }
27
+ },
28
+ seo: {
29
+ titleTemplate: "%s | Rimelight",
30
+ ogImageFallback: "https://cdn.rimelight.com/Images/og-placeholder.webp",
31
+ maxDescriptionLength: 160
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Retrieves the dynamic site settings from the database. If no settings are found, seeds the
37
+ * default record and returns it.
38
+ */
39
+ export async function getSiteSettings(
40
+ db: any,
41
+ fallbackDefaults?: Partial<InsertSiteSettings>
42
+ ): Promise<SiteSettings> {
43
+ if (!db) {
44
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
45
+ return {
46
+ ...DEFAULT_SITE_SETTINGS,
47
+ ...fallbackDefaults,
48
+ createdAt: new Date(),
49
+ updatedAt: new Date()
50
+ } as SiteSettings
51
+ }
52
+
53
+ try {
54
+ const records = await db
55
+ .select()
56
+ .from(siteSettings)
57
+ .where(eq(siteSettings.id, "default"))
58
+ .limit(1)
59
+
60
+ if (records.length > 0) {
61
+ return records[0]
62
+ }
63
+
64
+ const initial: InsertSiteSettings = {
65
+ ...DEFAULT_SITE_SETTINGS,
66
+ ...fallbackDefaults,
67
+ id: "default"
68
+ }
69
+
70
+ const inserted = await db.insert(siteSettings).values(initial).returning()
71
+ return inserted[0]
72
+ } catch (error) {
73
+ console.warn("Failed to fetch site settings from DB, using fallback:", error)
74
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
75
+ return {
76
+ ...DEFAULT_SITE_SETTINGS,
77
+ ...fallbackDefaults,
78
+ createdAt: new Date(),
79
+ updatedAt: new Date()
80
+ } as SiteSettings
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Updates dynamic site settings in the database.
86
+ */
87
+ export async function updateSiteSettings(
88
+ db: any,
89
+ updates: Partial<InsertSiteSettings>,
90
+ id: string = "default"
91
+ ): Promise<SiteSettings> {
92
+ const current = await getSiteSettings(db)
93
+
94
+ const updatedValues = {
95
+ ...updates,
96
+ branding: updates.branding ? { ...current.branding, ...updates.branding } : current.branding,
97
+ seo: updates.seo ? { ...current.seo, ...updates.seo } : current.seo,
98
+ updatedAt: new Date()
99
+ }
100
+
101
+ const result = await db
102
+ .update(siteSettings)
103
+ .set(updatedValues)
104
+ .where(eq(siteSettings.id, id))
105
+ .returning()
106
+
107
+ return result[0]
108
+ }