@rimelight/cms 0.0.9 → 0.0.10
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.
- package/dist/index.d.mts +1832 -16
- package/dist/index.mjs +1534 -218
- package/dist/schema/index.d.mts +257 -0
- package/dist/schema/index.mjs +18 -2
- package/dist/schema/sqlite.d.mts +232 -0
- package/dist/schema/sqlite.mjs +15 -1
- package/package.json +29 -16
- package/src/admin/api/media-file.ts +0 -1
- package/src/admin/api/media.ts +0 -2
- package/src/admin/layouts/CMSDashboardLayout.astro +7 -1
- package/src/admin/layouts/DefaultParentLayout.astro +0 -1
- package/src/admin/pages/pages-edit.astro +4 -1
- package/src/admin/pages/pages-index.astro +262 -101
- package/src/admin/pages/pages-preview.astro +5 -2
- package/src/admin/pages/templates-index.astro +224 -54
- package/src/admin/pages/users.astro +96 -50
- package/src/admin/pages/versions.astro +49 -37
- package/src/astro/blocks/DialogueBlock.astro +25 -0
- package/src/astro/blocks/LivePreviewBlock.astro +33 -0
- package/src/astro/blocks/SceneBlock.astro +39 -0
- package/src/astro/blocks/ScriptBlock.astro +77 -0
- package/src/astro/blocks/TableBlock.astro +30 -37
- package/src/astro/blocks/index.ts +9 -1
- package/src/env.d.ts +7 -1
- package/src/loader/live.ts +1 -2
- package/src/mcp/index.ts +1290 -32
- package/src/schema/content_types.ts +59 -0
- package/src/schema/index.ts +1 -0
- package/src/schema/sqlite.ts +35 -0
- package/src/services/search-indexer.ts +20 -9
- package/src/services/site-settings.ts +18 -14
- package/src/services/template-seeder.ts +249 -0
- package/src/storage/index.ts +0 -1
- package/LICENSE +0 -21
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
|
|
3
3
|
import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
|
|
4
|
-
import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
|
|
5
4
|
import RLATable from "@rimelight/ui/components/table/RLATable.astro";
|
|
6
|
-
import type { TableColumn } from "@rimelight/ui/components/table/table.ts";
|
|
5
|
+
import type { TableColumn, TableRow } from "@rimelight/ui/components/table/table.ts";
|
|
7
6
|
import { db } from "virtual:rimelight-cms/db";
|
|
8
7
|
import { pages } from "../../schema/pages.ts";
|
|
9
8
|
import { desc, isNull } from "drizzle-orm";
|
|
@@ -13,134 +12,296 @@ export const prerender = false;
|
|
|
13
12
|
|
|
14
13
|
const currentLocale = Astro.currentLocale ?? "en";
|
|
15
14
|
|
|
16
|
-
let
|
|
15
|
+
let rawPages: any[] = [];
|
|
16
|
+
|
|
17
17
|
if (db) {
|
|
18
18
|
try {
|
|
19
|
-
|
|
19
|
+
rawPages = await db
|
|
20
20
|
.select()
|
|
21
21
|
.from(pages)
|
|
22
22
|
.where(isNull(pages.deletedAt))
|
|
23
|
-
.orderBy(desc(pages.createdAt));
|
|
23
|
+
.orderBy(desc(pages.updatedAt), desc(pages.createdAt));
|
|
24
24
|
} catch (err) {
|
|
25
25
|
console.warn("Failed to fetch pages:", err);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// Format page data with resilient localized string parsing
|
|
30
|
+
const pagesList = rawPages.map((page) => {
|
|
31
|
+
let titleStr = "";
|
|
32
|
+
if (typeof page.title === "object" && page.title !== null) {
|
|
33
|
+
const titleObj = page.title as Record<string, string>;
|
|
34
|
+
titleStr = titleObj[currentLocale] || titleObj["en"] || (Object.values(titleObj)[0] as string | undefined) || "";
|
|
35
|
+
} else {
|
|
36
|
+
titleStr = String(page.title || "Untitled");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let descStr = "";
|
|
40
|
+
if (typeof page.description === "object" && page.description !== null) {
|
|
41
|
+
descStr = (page.description as Record<string, string>)[currentLocale] || (page.description as Record<string, string>)["en"] || "";
|
|
42
|
+
} else if (page.description) {
|
|
43
|
+
descStr = String(page.description);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isPublished = Boolean(page.publishedVersionId);
|
|
47
|
+
const type = String(page.type || "page").toLowerCase();
|
|
48
|
+
|
|
34
49
|
return {
|
|
35
50
|
id: page.id,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
slug: page.slug || "",
|
|
52
|
+
title: titleStr,
|
|
53
|
+
description: descStr,
|
|
54
|
+
type,
|
|
55
|
+
status: isPublished ? "published" : "draft",
|
|
56
|
+
isPublished,
|
|
40
57
|
publishedVersionId: page.publishedVersionId,
|
|
41
|
-
|
|
58
|
+
postedAt: page.postedAt ? new Date(page.postedAt) : null,
|
|
59
|
+
createdAt: page.createdAt ? new Date(page.createdAt) : new Date(),
|
|
60
|
+
updatedAt: page.updatedAt ? new Date(page.updatedAt) : (page.createdAt ? new Date(page.createdAt) : new Date())
|
|
42
61
|
};
|
|
43
62
|
});
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
function formatDate(date: Date): string {
|
|
65
|
+
try {
|
|
66
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
67
|
+
month: "short",
|
|
68
|
+
day: "numeric",
|
|
69
|
+
year: "numeric",
|
|
70
|
+
hour: "2-digit",
|
|
71
|
+
minute: "2-digit"
|
|
72
|
+
}).format(date);
|
|
73
|
+
} catch {
|
|
74
|
+
return date.toISOString().slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function timeAgo(date: Date): string {
|
|
79
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
80
|
+
if (seconds < 60) return "Just now";
|
|
81
|
+
const minutes = Math.floor(seconds / 60);
|
|
82
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
83
|
+
const hours = Math.floor(minutes / 60);
|
|
84
|
+
if (hours < 24) return `${hours}h ago`;
|
|
85
|
+
const days = Math.floor(hours / 24);
|
|
86
|
+
if (days < 30) return `${days}d ago`;
|
|
87
|
+
return formatDate(date);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getTypeBadgeColor(type: string): "primary" | "secondary" | "success" | "warning" | "error" | "neutral" {
|
|
91
|
+
switch (type) {
|
|
92
|
+
case "blog":
|
|
93
|
+
return "primary";
|
|
94
|
+
case "doc":
|
|
95
|
+
case "documentation":
|
|
96
|
+
return "secondary";
|
|
97
|
+
case "legal":
|
|
98
|
+
case "policy":
|
|
99
|
+
return "warning";
|
|
100
|
+
case "wiki":
|
|
101
|
+
return "success";
|
|
102
|
+
case "announcement":
|
|
103
|
+
return "error";
|
|
104
|
+
default:
|
|
105
|
+
return "neutral";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getTypeIcon(type: string): string {
|
|
110
|
+
switch (type) {
|
|
111
|
+
case "blog":
|
|
112
|
+
return "i-lucide-newspaper";
|
|
113
|
+
case "doc":
|
|
114
|
+
case "documentation":
|
|
115
|
+
return "i-lucide-book-open";
|
|
116
|
+
case "legal":
|
|
117
|
+
case "policy":
|
|
118
|
+
return "i-lucide-shield-alert";
|
|
119
|
+
case "wiki":
|
|
120
|
+
return "i-lucide-library";
|
|
121
|
+
case "announcement":
|
|
122
|
+
return "i-lucide-megaphone";
|
|
123
|
+
default:
|
|
124
|
+
return "i-lucide-file-text";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pageColumns: TableColumn<any>[] = [
|
|
129
|
+
{
|
|
130
|
+
accessorKey: "title",
|
|
131
|
+
header: "Page & Slug",
|
|
132
|
+
cell: ({ row }: { row: TableRow<any> }) => {
|
|
133
|
+
const page = row.original;
|
|
134
|
+
return `<div class="flex items-start gap-3 py-1">
|
|
135
|
+
<div class="mt-0.5 size-8 rounded-lg bg-elevated border border-default text-muted flex items-center justify-center shrink-0">
|
|
136
|
+
<span class="${getTypeIcon(page.type)} size-4"></span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="min-w-0 flex-1">
|
|
139
|
+
<a href="${getRelativeLocaleUrl(currentLocale, `/cms/pages/${page.id}/edit`)}" class="font-semibold text-highlighted hover:text-primary transition-colors text-sm truncate block" title="${page.title}">
|
|
140
|
+
${page.title}
|
|
141
|
+
</a>
|
|
142
|
+
<div class="flex items-center gap-2 mt-1">
|
|
143
|
+
<span class="font-mono text-xs text-muted bg-elevated px-1.5 py-0.5 rounded border border-default/60 truncate max-w-xs">
|
|
144
|
+
/${page.slug}
|
|
145
|
+
</span>
|
|
146
|
+
${page.isPublished ? `<a href="/${currentLocale}/${page.slug}" target="_blank" rel="noopener noreferrer" class="text-muted hover:text-primary p-0.5 rounded transition cursor-pointer" title="View on site"><span class="i-lucide-external-link size-3.5 inline-block"></span></a>` : ''}
|
|
147
|
+
</div>
|
|
148
|
+
${page.description ? `<p class="text-xs text-muted/80 mt-1 line-clamp-1 max-w-md">${page.description}</p>` : ''}
|
|
149
|
+
</div>
|
|
150
|
+
</div>`;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
accessorKey: "type",
|
|
155
|
+
header: "Type",
|
|
156
|
+
size: 130,
|
|
157
|
+
cell: ({ row }: { row: TableRow<any> }) => {
|
|
158
|
+
const page = row.original;
|
|
159
|
+
const color = getTypeBadgeColor(page.type);
|
|
160
|
+
return `<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wider bg-${color}-500/10 text-${color}-500 border border-${color}-500/20">${page.type}</span>`;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
accessorKey: "status",
|
|
165
|
+
header: "Status",
|
|
166
|
+
size: 130,
|
|
167
|
+
cell: ({ row }: { row: TableRow<any> }) => {
|
|
168
|
+
const page = row.original;
|
|
169
|
+
return page.isPublished
|
|
170
|
+
? `<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-500 border border-emerald-500/20"><span class="size-1.5 rounded-full bg-emerald-500 animate-pulse"></span><span>Published</span></div>`
|
|
171
|
+
: `<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-amber-500/10 text-amber-500 border border-amber-500/20"><span class="size-1.5 rounded-full bg-amber-500"></span><span>Draft</span></div>`;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
accessorKey: "updatedAt",
|
|
176
|
+
header: "Last Updated",
|
|
177
|
+
size: 160,
|
|
178
|
+
cell: ({ row }: { row: TableRow<any> }) => {
|
|
179
|
+
const page = row.original;
|
|
180
|
+
return `<div class="text-xs font-medium text-highlighted" title="${formatDate(page.updatedAt)}">${timeAgo(page.updatedAt)}</div><div class="text-[11px] text-muted font-mono mt-0.5">${page.updatedAt.toISOString().slice(0, 10)}</div>`;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "actions",
|
|
185
|
+
actions: (row: TableRow<any>) => {
|
|
186
|
+
const page = row.original;
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
label: "Edit Page",
|
|
190
|
+
icon: "i-lucide-pencil",
|
|
191
|
+
to: getRelativeLocaleUrl(currentLocale, `/cms/pages/${page.id}/edit`)
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
label: "Live Preview",
|
|
195
|
+
icon: "i-lucide-eye",
|
|
196
|
+
to: getRelativeLocaleUrl(currentLocale, `/cms/pages/${page.id}/preview`)
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
label: "Version History",
|
|
200
|
+
icon: "i-lucide-git-compare",
|
|
201
|
+
to: getRelativeLocaleUrl(currentLocale, `/cms/pages/${page.id}/review`)
|
|
202
|
+
},
|
|
203
|
+
...(page.isPublished ? [
|
|
204
|
+
{
|
|
205
|
+
label: "View on Live Site",
|
|
206
|
+
icon: "i-lucide-external-link",
|
|
207
|
+
to: `/${currentLocale}/${page.slug}`,
|
|
208
|
+
target: "_blank"
|
|
209
|
+
}
|
|
210
|
+
] : []),
|
|
211
|
+
{
|
|
212
|
+
type: "separator" as const
|
|
213
|
+
},
|
|
214
|
+
...(page.isPublished ? [
|
|
215
|
+
{
|
|
216
|
+
label: "Unpublish to Draft",
|
|
217
|
+
icon: "i-lucide-archive",
|
|
218
|
+
color: "warning" as const,
|
|
219
|
+
id: `unpublish-page-${page.id}`
|
|
220
|
+
}
|
|
221
|
+
] : []),
|
|
222
|
+
{
|
|
223
|
+
label: "Delete Page",
|
|
224
|
+
icon: "i-lucide-trash-2",
|
|
225
|
+
color: "error" as const,
|
|
226
|
+
id: `delete-page-${page.id}`
|
|
227
|
+
}
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
51
231
|
];
|
|
52
232
|
---
|
|
53
233
|
|
|
54
|
-
<CMSDashboardLayout title="Pages" description="Manage
|
|
234
|
+
<CMSDashboardLayout title="Pages" description="Manage, edit, and publish dynamic site content" activeSection="pages">
|
|
55
235
|
<Fragment slot="toolbar-right">
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
236
|
+
<div class="flex items-center gap-2">
|
|
237
|
+
<RLAButton
|
|
238
|
+
href={getRelativeLocaleUrl(currentLocale, "/cms/templates")}
|
|
239
|
+
variant="outline"
|
|
240
|
+
color="neutral"
|
|
241
|
+
size="sm"
|
|
242
|
+
leadingIcon="i-lucide-layout-template"
|
|
243
|
+
>
|
|
244
|
+
Templates
|
|
245
|
+
</RLAButton>
|
|
246
|
+
<RLAButton
|
|
247
|
+
href={getRelativeLocaleUrl(currentLocale, "/cms/pages/new")}
|
|
248
|
+
variant="solid"
|
|
249
|
+
color="primary"
|
|
250
|
+
size="sm"
|
|
251
|
+
leadingIcon="i-lucide-plus"
|
|
252
|
+
>
|
|
253
|
+
Create Page
|
|
254
|
+
</RLAButton>
|
|
255
|
+
</div>
|
|
59
256
|
</Fragment>
|
|
60
257
|
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<td class="p-3.5 text-sm font-medium text-highlighted" data-col-id="title" data-slot="td">
|
|
74
|
-
{page.title}
|
|
75
|
-
</td>
|
|
76
|
-
<td class="p-3.5 text-sm text-muted font-mono text-xs" data-col-id="slug" data-slot="td">
|
|
77
|
-
{page.slug}
|
|
78
|
-
</td>
|
|
79
|
-
<td class="p-3.5" data-col-id="type" data-slot="td">
|
|
80
|
-
<RLABadge variant="soft" color="blue" size="xs">
|
|
81
|
-
{page.type}
|
|
82
|
-
</RLABadge>
|
|
83
|
-
</td>
|
|
84
|
-
<td class="p-3.5" data-col-id="status" data-slot="td">
|
|
85
|
-
{page.status === "published" ? (
|
|
86
|
-
<RLABadge variant="soft" color="green" size="xs">Published</RLABadge>
|
|
87
|
-
) : (
|
|
88
|
-
<RLABadge variant="soft" color="yellow" size="xs">Draft</RLABadge>
|
|
89
|
-
)}
|
|
90
|
-
</td>
|
|
91
|
-
<td class="p-3.5 text-right whitespace-nowrap" data-col-id="actions" data-slot="td">
|
|
92
|
-
<div class="flex items-center justify-end gap-2">
|
|
93
|
-
<RLAButton
|
|
94
|
-
href={getRelativeLocaleUrl(currentLocale, `/cms/pages/${page.id}/edit`)}
|
|
95
|
-
variant="ghost"
|
|
96
|
-
color="primary"
|
|
97
|
-
size="xs"
|
|
98
|
-
>
|
|
99
|
-
Edit
|
|
100
|
-
</RLAButton>
|
|
101
|
-
<RLAButton
|
|
102
|
-
href={getRelativeLocaleUrl(currentLocale, `/cms/pages/${page.id}/preview`)}
|
|
103
|
-
variant="ghost"
|
|
104
|
-
color="neutral"
|
|
105
|
-
size="xs"
|
|
106
|
-
>
|
|
107
|
-
Preview
|
|
108
|
-
</RLAButton>
|
|
109
|
-
<RLAButton
|
|
110
|
-
variant="ghost"
|
|
111
|
-
color="error"
|
|
112
|
-
size="xs"
|
|
113
|
-
class="delete-page-btn"
|
|
114
|
-
data-page-id={page.id}
|
|
115
|
-
>
|
|
116
|
-
Delete
|
|
117
|
-
</RLAButton>
|
|
118
|
-
</div>
|
|
119
|
-
</td>
|
|
120
|
-
</tr>
|
|
121
|
-
))}
|
|
122
|
-
</RLATable>
|
|
258
|
+
<div class="flex flex-col gap-4">
|
|
259
|
+
<RLATable
|
|
260
|
+
data={pagesList}
|
|
261
|
+
columns={pageColumns}
|
|
262
|
+
striped
|
|
263
|
+
hoverable
|
|
264
|
+
searchable
|
|
265
|
+
showColumnsToggle
|
|
266
|
+
exportable
|
|
267
|
+
exportFilename="rimelight-pages-export"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
123
270
|
|
|
124
271
|
<script>
|
|
125
|
-
document.
|
|
126
|
-
|
|
127
|
-
|
|
272
|
+
document.addEventListener("click", async (e) => {
|
|
273
|
+
const target = e.target as HTMLElement;
|
|
274
|
+
|
|
275
|
+
// Handle unpublish
|
|
276
|
+
const unpublishItem = target.closest<HTMLElement>("[data-item-id^='unpublish-page-']");
|
|
277
|
+
if (unpublishItem) {
|
|
278
|
+
const pageId = unpublishItem.getAttribute("data-item-id")?.replace("unpublish-page-", "") || "";
|
|
128
279
|
if (!pageId) return;
|
|
129
|
-
if (!confirm("Are you sure you want to
|
|
280
|
+
if (!confirm("Are you sure you want to unpublish this page?")) return;
|
|
281
|
+
try {
|
|
282
|
+
const res = await fetch(`/api/cms/pages/${pageId}/unpublish`, { method: "POST" });
|
|
283
|
+
if (res.ok) window.location.reload();
|
|
284
|
+
else alert("Failed to unpublish page");
|
|
285
|
+
} catch {
|
|
286
|
+
alert("Network error unpublishing page");
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
130
290
|
|
|
291
|
+
// Handle delete
|
|
292
|
+
const deleteItem = target.closest<HTMLElement>("[data-item-id^='delete-page-']");
|
|
293
|
+
if (deleteItem) {
|
|
294
|
+
const pageId = deleteItem.getAttribute("data-item-id")?.replace("delete-page-", "") || "";
|
|
295
|
+
if (!pageId) return;
|
|
296
|
+
if (!confirm("Are you sure you want to delete this page? This action cannot be undone.")) return;
|
|
131
297
|
try {
|
|
132
|
-
const res = await fetch(`/api/cms/pages/${pageId}`, {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (res.ok) {
|
|
136
|
-
window.location.reload();
|
|
137
|
-
} else {
|
|
138
|
-
alert("Failed to delete page");
|
|
139
|
-
}
|
|
298
|
+
const res = await fetch(`/api/cms/pages/${pageId}`, { method: "DELETE" });
|
|
299
|
+
if (res.ok) window.location.reload();
|
|
300
|
+
else alert("Failed to delete page");
|
|
140
301
|
} catch {
|
|
141
|
-
alert("
|
|
302
|
+
alert("Network error deleting page");
|
|
142
303
|
}
|
|
143
|
-
}
|
|
304
|
+
}
|
|
144
305
|
});
|
|
145
306
|
</script>
|
|
146
307
|
</CMSDashboardLayout>
|
|
@@ -18,9 +18,12 @@ if (!id) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const token = Astro.url.searchParams.get("token");
|
|
21
|
-
const secret = process.env["CMS_PREVIEW_SECRET"] || "rimelight-preview-secret-key";
|
|
21
|
+
const secret = process.env["CMS_PREVIEW_SECRET"] || process.env["AUTH0_SECRET"] || "rimelight-preview-secret-key";
|
|
22
22
|
if (token) {
|
|
23
|
-
await verifyPreviewToken(id, token, secret);
|
|
23
|
+
const isTokenValid = await verifyPreviewToken(id, token, secret);
|
|
24
|
+
if (!isTokenValid) {
|
|
25
|
+
return new Response("Forbidden: Invalid or expired preview token", { status: 403 });
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
let page: any = null;
|