@rigel-cms/rigel 1.0.0-rc.1 → 1.0.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.
- package/README.md +15 -7
- package/package.json +2 -7
- package/src/config.ts +6 -1
- package/src/editor/App.svelte +3 -3
- package/src/editor/lib/icons.ts +4 -0
- package/src/preview-urls.ts +6 -0
- package/src/preview.ts +16 -5
- package/src/slugs.ts +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
# Rigel CMS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@rigel-cms/rigel)
|
|
4
|
+
[](https://astro.build/)
|
|
5
|
+
[](package.json)
|
|
6
|
+
[](LICENSE)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
A CMS for Astro sites. You edit the text, Git keeps the history.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
Edit page copy, structured content and articles at `/rigel/`. Your content stays in your repository, and your Astro site controls how it looks.
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
- Visual editing on prepared page templates, with unsaved changes shown in the preview.
|
|
13
|
+
- A block editor for articles and a media library with local uploads.
|
|
14
|
+
- Explicit Save, with warnings before leaving unsaved changes.
|
|
15
|
+
- Local file editing or GitHub drafts with pull-request publishing.
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
Rigel edits content, not `.astro` components. Developers define the editable fields and add preview bindings to their templates. Saving content and publishing the website are separate steps.
|
|
12
18
|
|
|
13
|
-
##
|
|
19
|
+
## Get started
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
Use the [Astro starter](https://github.com/rigel-cms/rigel-starter), or follow the [installation guide](https://rigelcms.dev/docs/getting-started/) to add Rigel to an existing site. Requires Node.js 24 and Astro 7.3 or later within Astro 7.
|
|
22
|
+
|
|
23
|
+
[Documentation](https://rigelcms.dev/docs/) · [Content model](https://rigelcms.dev/docs/content-model/) · [Previews](https://rigelcms.dev/docs/preview/) · [GitHub setup](https://rigelcms.dev/docs/github-publishing/)
|
|
16
24
|
|
|
17
25
|
## License
|
|
18
26
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigel-cms/rigel",
|
|
3
|
-
"version": "1.0.0
|
|
4
|
-
"
|
|
5
|
-
"access": "public",
|
|
6
|
-
"tag": "rc"
|
|
7
|
-
},
|
|
8
|
-
"description": "Rigel CMS — the copy editor for Astro sites. Clients edit text, git keeps the history.",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CMS for Astro sites. You edit the text, Git keeps the history.",
|
|
9
5
|
"author": {
|
|
10
6
|
"name": "FreshJuice",
|
|
11
7
|
"email": "contact@freshjuice.dev",
|
|
@@ -53,7 +49,6 @@
|
|
|
53
49
|
"vite": "^8.0.0"
|
|
54
50
|
},
|
|
55
51
|
"devDependencies": {
|
|
56
|
-
|
|
57
52
|
"@types/markdown-it": "^14.2.0",
|
|
58
53
|
"@types/node": "^24.0.0",
|
|
59
54
|
"astro": "^7.3.2",
|
package/src/config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { validateSchema } from "./schema/validate";
|
|
|
3
3
|
import type { Schema } from "./schema/types";
|
|
4
4
|
import { validateSidebar, type SidebarEntry } from "./manifest/navigation";
|
|
5
5
|
import { withSeoFields } from "./manifest/seo";
|
|
6
|
-
import { previewBaseUrl } from "./preview-urls";
|
|
6
|
+
import { previewBaseUrl, previewRoute } from "./preview-urls";
|
|
7
7
|
import type { JsonOutput } from "./json-output";
|
|
8
8
|
import { DEFAULT_MAX_FILE_SIZE } from "./media-types";
|
|
9
9
|
|
|
@@ -53,6 +53,11 @@ export function parseConfig(options: Partial<RigelConfig> = {}): RigelConfig {
|
|
|
53
53
|
if (cfg.preview) {
|
|
54
54
|
cfg.preview = { ...cfg.preview };
|
|
55
55
|
for (const key of ["liveUrl", "draftUrl"] as const) if (cfg.preview[key] !== undefined) cfg.preview[key] = previewBaseUrl(cfg.preview[key]);
|
|
56
|
+
for (const mappings of [cfg.preview.pages, cfg.preview.documents, cfg.preview.collections]) {
|
|
57
|
+
for (const mapping of Object.values(mappings ?? {})) {
|
|
58
|
+
if (mapping && typeof mapping === "object" && mapping.template !== undefined && !previewRoute(mapping.template, true)) throw new Error("rigel: preview template must be a static root-relative path; only /rigel/preview/ is allowed inside the CMS");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
56
61
|
}
|
|
57
62
|
if (cfg.mediaDir !== undefined && (typeof cfg.mediaDir !== "string" || !cfg.mediaDir.split("/").every(part => /^[a-zA-Z0-9_-][a-zA-Z0-9_. -]*$/.test(part)))) throw new Error("rigel: mediaDir must be a relative folder inside publicDir");
|
|
58
63
|
if (cfg.backend === "github") {
|
package/src/editor/App.svelte
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import PublishDialog from "./lib/PublishDialog.svelte";
|
|
34
34
|
import VisualEditor from "./lib/VisualEditor.svelte";
|
|
35
35
|
import { newDocumentFromTemplate } from "../schema/paths";
|
|
36
|
-
import { previewUrl } from "../preview";
|
|
36
|
+
import { previewUrl, previewPath } from "../preview";
|
|
37
37
|
import type { ContentDocument, Schema } from "../schema/types";
|
|
38
38
|
import { buildSidebar, navigationKey, type Selection } from "../manifest/navigation";
|
|
39
39
|
import { startGitHubLogin, checkGitHubAccess, type GitHubAccess } from "./lib/github-auth";
|
|
@@ -851,7 +851,7 @@
|
|
|
851
851
|
|
|
852
852
|
{#if githubMode && manifest.github?.publishBranch}
|
|
853
853
|
<button id="publish-button" class="sidebar-publish" aria-label="Publish" data-tooltip={dirty ? "Save your edits before publishing" : "Publish saved branch changes"} disabled={publishBusy || publishLoading || saveState === "saving" || connectionBusy || tokenExpired || githubAccess?.status !== "ready"} onclick={reviewPublish}>
|
|
854
|
-
<Icon name="
|
|
854
|
+
<Icon name="rocketLaunch" size={18} /><span class="sidebar-label">Publish</span>
|
|
855
855
|
</button>
|
|
856
856
|
{/if}
|
|
857
857
|
<button class="sidebar-toggle" aria-expanded={!settings.userPref.sidebarCollapsed} aria-controls="rigel-sidebar-navigation" aria-label={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} data-tooltip={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} onclick={() => { settings.userPref.sidebarCollapsed = !settings.userPref.sidebarCollapsed; }}>
|
|
@@ -1035,7 +1035,7 @@
|
|
|
1035
1035
|
|
|
1036
1036
|
{:else if sel?.kind === "page" && page}
|
|
1037
1037
|
{#if !page.shared}
|
|
1038
|
-
<section><label><span>Website route</span><input type="text" value={manifest.preview
|
|
1038
|
+
<section><label><span>Website route</span><input type="text" value={previewPath(manifest.preview ?? {}, { kind: "page", slug: page.slug }) ?? "Not configured"} readonly /></label><p class="hint">This copy page uses a fixed Astro route. Its URL can only be changed in the website code; slug metadata cannot move it.</p></section>
|
|
1039
1039
|
{/if}
|
|
1040
1040
|
{#each page.groups as group (group.id)}
|
|
1041
1041
|
<section>
|
package/src/editor/lib/icons.ts
CHANGED
|
@@ -66,6 +66,10 @@ export const ICONS = {
|
|
|
66
66
|
viewBox: "0 0 256 256",
|
|
67
67
|
d: "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z",
|
|
68
68
|
},
|
|
69
|
+
rocketLaunch: {
|
|
70
|
+
viewBox: "0 0 256 256",
|
|
71
|
+
d: "M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z",
|
|
72
|
+
},
|
|
69
73
|
external: {
|
|
70
74
|
viewBox: "0 0 256 256",
|
|
71
75
|
d: "M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z",
|
package/src/preview-urls.ts
CHANGED
|
@@ -2,6 +2,12 @@ export function safePreviewUrl(value: string, image = false): boolean {
|
|
|
2
2
|
return !!value && !/[ \t\r\n\f\v\u0000-\u001f\u007f\\]/.test(value) && !value.startsWith("//") && (!/^[a-z][a-z\d+.-]*:/i.test(value) || (image ? /^https?:/i : /^(https?:|mailto:)/i).test(value));
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
export function previewRoute(value: unknown, template = false): string | null {
|
|
6
|
+
if (typeof value !== "string" || !/^\/(?!\/)/.test(value) || /[\\\s?#\u0000-\u001f\u007f]/.test(value) || /%(?:2e|2f|5c)/i.test(value) || value.split("/").some(part => part === "." || part === "..")) return null;
|
|
7
|
+
if (template && /[{}]/.test(value)) return null;
|
|
8
|
+
return !/^\/rigel(?:\/|$)/.test(value) || (template && /^\/rigel\/preview(?:\/|$)/.test(value)) ? value : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
export function previewBaseUrl(value: unknown): string {
|
|
6
12
|
try {
|
|
7
13
|
if (typeof value !== "string" || !/^https?:\/\/[^/@]+(?:\/|$)/.test(value) || /[\s\u0000-\u001f\u007f\\%?#]/.test(value) || value.split("/").some(part => part === "." || part === "..")) throw new Error();
|
package/src/preview.ts
CHANGED
|
@@ -4,19 +4,20 @@ import { pageRoute } from "./schema/paths";
|
|
|
4
4
|
import { slugify } from "./slugs";
|
|
5
5
|
import { renderArticle } from "./article-render";
|
|
6
6
|
import { createEmbedPreview } from "./editor/lib/embed-preview";
|
|
7
|
-
import { safePreviewUrl, previewBaseUrl } from "./preview-urls";
|
|
7
|
+
import { safePreviewUrl, previewBaseUrl, previewRoute } from "./preview-urls";
|
|
8
8
|
export { safePreviewUrl } from "./preview-urls";
|
|
9
9
|
|
|
10
10
|
export interface PreviewDocumentMapping {
|
|
11
11
|
url: string;
|
|
12
12
|
index?: string;
|
|
13
|
+
template?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface PreviewConfig {
|
|
16
17
|
liveUrl?: string;
|
|
17
18
|
draftUrl?: string;
|
|
18
|
-
pages?: Record<string, string>;
|
|
19
|
-
collections?: Record<string, { url: string; new
|
|
19
|
+
pages?: Record<string, string | PreviewDocumentMapping>;
|
|
20
|
+
collections?: Record<string, { url: string; new?: string; template?: string }>;
|
|
20
21
|
documents?: Record<string, PreviewDocumentMapping>;
|
|
21
22
|
}
|
|
22
23
|
export interface PreviewTarget {
|
|
@@ -27,13 +28,23 @@ export interface PreviewTarget {
|
|
|
27
28
|
type?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function templatePath(config: PreviewConfig, target: PreviewTarget): string | null | undefined {
|
|
32
|
+
const mapping = target.kind === "post" ? config.collections?.[target.collection ?? ""] : target.kind === "document" ? config.documents?.[target.type ?? ""] : target.kind === "page" ? config.pages?.[target.slug] : undefined;
|
|
33
|
+
return mapping && typeof mapping === "object" && mapping.template !== undefined ? previewRoute(mapping.template, true) : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
export function previewPath(config: PreviewConfig, target: PreviewTarget, isNew = false): string | null {
|
|
37
|
+
const template = isNew ? templatePath(config, target) : undefined;
|
|
38
|
+
if (template !== undefined) return template;
|
|
31
39
|
const mapping = target.kind === "post" ? config.collections?.[target.collection ?? ""] : undefined;
|
|
32
|
-
const
|
|
33
|
-
|
|
40
|
+
const page = config.pages?.[target.slug];
|
|
41
|
+
const path = target.kind === "document" ? pageRoute(target.slug, config.documents?.[target.type ?? ""]) : target.kind === "page" ? (typeof page === "string" ? page : page?.url) : mapping ? (isNew ? mapping.new : mapping.url.replaceAll("{slug}", encodeURIComponent(target.slug))) : undefined;
|
|
42
|
+
return previewRoute(path);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export function previewUrl(config: PreviewConfig, target: PreviewTarget, mode: "live" | "draft" | "website" = "draft", isNew = false): string | null {
|
|
46
|
+
const template = mode === "draft" ? templatePath(config, target) : undefined;
|
|
47
|
+
if (template !== undefined) return template;
|
|
37
48
|
if (mode !== "draft" && target.kind !== "page" && target.urlSlug !== undefined) {
|
|
38
49
|
const slug = slugify(target.urlSlug);
|
|
39
50
|
if (!slug) return null;
|
package/src/slugs.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function assertUniqueUrlSlugs(entries: { slug: string; values: Record<str
|
|
|
33
33
|
export function validateDocumentSlugs(schema: Schema, documents: ContentDocument[], preview?: PreviewConfig): void {
|
|
34
34
|
const seen = new Set<string>();
|
|
35
35
|
const pages = documents.filter(doc => schema.documents.some(type => type.name === doc.type && !type.shared));
|
|
36
|
-
const fixedRoutes = Object.entries(preview?.pages ?? {}).filter(([key]) => !pages.some(doc => doc.slug === key)).map(([, route]) => route);
|
|
36
|
+
const fixedRoutes = Object.entries(preview?.pages ?? {}).filter(([key]) => !pages.some(doc => doc.slug === key)).map(([, route]) => typeof route === "string" ? route : route.url);
|
|
37
37
|
for (const doc of documents) {
|
|
38
38
|
const type = schema.documents.find(type => type.name === doc.type);
|
|
39
39
|
if (!type || type.shared) continue;
|