@rigel-cms/rigel 1.4.0-rc.3 → 1.5.0-rc.1

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.
@@ -75,7 +75,7 @@ A schema reference declares `to: ["callout"]` and stores `{ "_ref": "callout:not
75
75
 
76
76
  Schema `markdown` fields store Markdown strings. Article collection bodies store Lexical state in `{ frontmatter, body }` JSON and have a read-only Markdown view. Keep those formats separate. Custom article blocks and widgets are covered in [Extensions](extensions.md).
77
77
 
78
- Markdown fields open in the article block editor when the source survives a conversion round trip. Set `sidebarOnly: true` on a `markdown` field to keep it in the properties panel even when its preview binding is available, for example a FAQ answer edited beside the question that owns it:
78
+ In Inline preview mode, clicking an eligible bound Markdown field on a non-shared schema page opens the article block editor when the source survives a conversion round trip. Unbound fields and shared-document forms keep their properties-panel editor. Set `sidebarOnly: true` on a `markdown` field to keep it in the properties panel even when its preview binding is available, for example a FAQ answer edited beside the question that owns it:
79
79
 
80
80
  ```ts
81
81
  const faqAnswer = { key: "answer", label: "Answer", type: "markdown", sidebarOnly: true };
@@ -44,7 +44,7 @@ export default defineExtension({
44
44
  if (typeof data.label !== 'string' || !data.label.trim()) {
45
45
  throw new Error('Enter a button label.');
46
46
  }
47
- if (typeof data.href !== 'string' || !/^\/[a-z0-9/-]*$/.test(data.href)) {
47
+ if (typeof data.href !== 'string' || !/^\/(?!\/)[a-z0-9/-]*$/.test(data.href)) {
48
48
  throw new Error('Choose a local path such as /contact/.');
49
49
  }
50
50
  },
package/docs/media.md CHANGED
@@ -48,17 +48,17 @@ Metadata lives in `.rigel-media.json` inside the configured media folder, keyed
48
48
 
49
49
  New uploads use random UUID filenames.
50
50
 
51
- Open an asset in Media, edit its display name, title, description or image alt text, then press Save. Metadata writes validate the listed asset URL, reject unknown fields and use an atomic sidecar replacement. Display names allow safe filename-like characters up to 120 characters without requiring an extension. Titles are capped at 200 characters; descriptions and alt text at 1,000. Nonimages cannot receive alt metadata.
51
+ On the local backend, open an asset in Media, edit its display name, title, description or image alt text, then press Save. Metadata writes validate the listed asset URL, reject unknown fields and use an atomic sidecar replacement. Display names allow safe filename-like characters up to 120 characters without requiring an extension. Titles are capped at 200 characters; descriptions and alt text at 1,000. Nonimages cannot receive alt metadata.
52
52
 
53
53
  Local file uploads send a browser-supplied author label through `X-Rigel-Author`. Rigel uses the browser identity's name or nickname, falling back to `Local user`. This is a descriptive label, not verified ownership or an audit identity. The metadata editor cannot change it. Existing files and URL or stock imports may have no captured author; the interface displays `Local user` as a fallback.
54
54
 
55
- Saved repository-image alt text supplies an insertion default. You can override it for each use, or leave it empty for a decorative image. Rigel does not rewrite alt text already stored in content. Rich-text insertions remain unsaved until the document's Save action.
55
+ For rich-text image insertion, saved repository-image alt text supplies a default that you can override per use, or leave empty for a decorative image. Image fields store the URL; declare and render a separate alt field where your site needs one. Store the original URL rather than a path your build generates. [Preview](preview.md) covers the `data-rigel-src` binding for optimized Astro images. Rigel does not rewrite alt text already stored in content. Rich-text insertions remain unsaved until the document's Save action.
56
56
 
57
57
  ## Image URLs and stock providers
58
58
 
59
59
  Image pickers offer Library, Upload, URL and Lorem Picsum. Pexels, Pixabay and Unsplash appear when their keys are configured in Settings under Media. Repository Upload excludes Unsplash.
60
60
 
61
- URL, Picsum, Pexels and Pixabay image imports download bytes through the browser and upload them locally. Remote image downloads require HTTPS and browser CORS permission. The import path accepts JPEG, PNG and WebP responses within the configured size limit. Download failures stay visible. The server accepts validated bytes rather than fetching arbitrary remote URLs.
61
+ Remote URL, Picsum, Pexels and Pixabay image imports download bytes through the browser and upload them locally. In a content picker, a root-relative URL can be used directly without making a copy. Remote image downloads require HTTPS and browser CORS permission. The import path accepts JPEG, PNG and WebP responses within the configured size limit. Download failures stay visible. The server accepts validated bytes rather than fetching arbitrary remote URLs.
62
62
 
63
63
  Unsplash selections keep the provider's hotlink and record the download through its API. They do not become repository files. Rich-text stock insertions retain credit links; check the provider's current licensing and [Unsplash hotlinking requirements](https://unsplash.com/documentation#hotlinking) for your use.
64
64
 
package/docs/menus.md CHANGED
@@ -90,6 +90,6 @@ The local menu endpoint supports reading and updating existing regular files. It
90
90
 
91
91
  GitHub-backed menus use the configured repository, content root and draft branch. The remote store validates the same menu shape and maximum depth, disallows menu creation, and checks the expected file state before writing. See [GitHub](github.md) for authentication, conflicts and publishing.
92
92
 
93
- URL validation accepts HTTP, HTTPS, mailto and tel protocols, including relative paths and fragments resolved against the site. It rejects protocol-relative URLs, backslashes, whitespace, control characters and unsafe protocols such as `javascript:`. It does not check whether the destination exists or is trustworthy. Keep the `noopener noreferrer` attributes when rendering new-tab links.
93
+ URL validation accepts HTTP, HTTPS, mailto and tel protocols, along with relative paths and fragments whose destinations your website renderer and browser determine. It rejects protocol-relative URLs, backslashes, leading/trailing whitespace, ASCII spaces and ASCII control characters, and unsafe protocols such as `javascript:`. It does not check whether the destination exists or is trustworthy. Keep the `noopener noreferrer` attributes when rendering new-tab links.
94
94
 
95
95
  For navigation inside the CMS rather than website links, use [sidebar configuration](sidebar.md). See the [documentation index](README.md) for the other guides.
@@ -33,9 +33,9 @@ Bases accept HTTPS with an optional path prefix. For example, `https://www.examp
33
33
 
34
34
  ## Separate templates from public routes
35
35
 
36
- The canvas resolves a configured `template` first. It is a static root-relative path on the editor's own origin, unaffected by either host base. Both new and saved content use it. A page mapping can use `{ url: "/about/", template: "/rigel/preview/about/" }`; document and collection mappings accept `template` too. The host must emit the route. Inside the CMS, only `/rigel/preview/` and its descendants are allowed as templates.
36
+ The canvas resolves a configured `template` first. It is a static root-relative path on the editor's own origin, unaffected by either host base. Saved schema pages and new or saved articles use it; new schema pages have no canvas until their first Save, even with a template configured. A page mapping can use `{ url: "/about/", template: "/rigel/preview/about/" }`; document and collection mappings accept `template` too. The host must emit the route. Inside the CMS, only `/rigel/preview/` and its descendants are allowed as templates.
37
37
 
38
- Without `template`, article canvases use the collection's `new` route even after Save or when reopening an existing article. This lets a prepared empty layout display an in-memory draft without waiting for deployment. Schema pages and legacy copy pages use their mapped routes; a new schema page needs an existing route or a prepared template to load successfully.
38
+ Without `template`, article canvases use the collection's `new` route even after Save or when reopening an existing article. This lets a prepared empty layout display an in-memory draft without waiting for deployment. With neither `template` nor `new`, an article has no canvas, even if its `url` is mapped. Saved schema pages and legacy copy pages use their mapped routes when no template is configured.
39
39
 
40
40
  Live links always use the real content route, including a new article's eventual URL. Draft website links also use the real route, rather than the canvas template. These links can lead to a page that has not been deployed yet. Edited URL slugs apply to website/live destinations; the prepared canvas can keep using its stable template.
41
41
 
package/docs/preview.md CHANGED
@@ -24,9 +24,9 @@ rigel({
24
24
 
25
25
  `pages` maps legacy copy-page slugs. `documents` maps schema type names; `index` is the route for the effective slug `home`. `collections` maps article collection names. Every configured route must actually exist in your host output.
26
26
 
27
- `template` is a static editor-local route used by the canvas, for both new and saved content. It stays on the editor's origin even when `draftUrl` is set. `/rigel/preview/` is the permitted template namespace inside the CMS, not a route generator. The host must supply the prepared page.
27
+ `template` is a static editor-local route used by the canvas for saved schema pages and new or saved articles. It stays on the editor's origin even when `draftUrl` is set. `/rigel/preview/` is the permitted template namespace inside the CMS, not a route generator. The host must supply the prepared page.
28
28
 
29
- A collection can instead use `new: "/preview/blog/"`. Without `template`, the article canvas uses `new` for saved articles too, while the public link uses `url`. Schema pages without `template` use their mapped document route. Set a template when a new page must preview before its public route exists. See [Preview hosts](preview-hosts.md) for the full destination rules.
29
+ A collection can instead use `new: "/preview/blog/"`. Without `template`, the article canvas uses `new` for saved articles too, while the public link uses `url`. Article canvases require `template` or `new`; a `url` mapping alone provides website links but no canvas. Saved schema pages without `template` use their mapped document route. New schema pages have no canvas until their first Save, even with a configured template. See [Preview hosts](preview-hosts.md) for the full destination rules.
30
30
 
31
31
  ## Bind rendered fields
32
32
 
@@ -44,6 +44,21 @@ These HTML excerpts illustrate the scalar anchors. Render the saved values throu
44
44
 
45
45
  Paths use object keys and stable array `_key` values. Array positions are not preview bindings. For schema content, put `data-rigel-doc="page:about"` on each bound element and on section/list containers. It identifies the source document for that element; a parent attribute does not substitute for attributes on descendant anchors. Referenced records use their own IDs and local field paths.
46
46
 
47
+ ### Keep optimized images editable
48
+
49
+ Astro's `Image` and `Picture` components replace `src` with hashed, responsive candidates, so changing `src` alone changes nothing while `srcset` still lists the old files. Mark an editable image with `data-rigel-src` to show the draft instead:
50
+
51
+ ```astro
52
+ <Image src={cover} alt={hero.alt} widths={[480, 960]} sizes="100vw" data-rigel-image="hero.image" data-rigel-alt="hero.alt" data-rigel-src={hero.image} />
53
+ <Picture src={cover} formats={["avif", "webp"]} alt={hero.alt} data-rigel-image="hero.image" data-rigel-src={hero.image} />
54
+ ```
55
+
56
+ `data-rigel-image` names the field, as before. `data-rigel-src` holds the original browser-addressable URL that the field stores, and it marks the element as optimized. In preview, a marked image loses its `srcset` and `sizes`, and the surrounding `picture` sources lose theirs, so the field value is the file the browser loads. The attribute is not a second source of truth: preview renders the field value, and a value that fails the image URL check is dropped rather than loaded.
57
+
58
+ Public pages are untouched. Rigel patches only the editor iframe, so built markup keeps its hashed URLs, `srcset` and AVIF or WebP sources. Images without `data-rigel-src` keep their previous behavior: the `src` changes and any `srcset` stays.
59
+
60
+ Store the original reference in content, never a generated path. `/images/cover.jpg`, a media URL or an HTTPS URL work; `/_astro/cover.BnV1MXKY.webp` does not, because Astro derives it at build time. An imported file is not addressable either: `src/assets/cover.jpg` has no URL a browser can request. Serve the file from a public path or the media folder and bind that URL, even when the template imports the same file for optimization. Preview shows the original at its own size, not a resized or converted variant, and the built `width` and `height` stay as they were, so check a replacement with different proportions against the template's layout. An upload becomes reachable at its media URL once the host serves it or rebuilds.
61
+
47
62
  ## Prepare changing sections and lists
48
63
 
49
64
  Scalar anchors update existing elements. Adding sections needs inert templates for each allowed section type. This HTML excerpt uses the `intro` section from [Content model](content-model.md):
@@ -81,11 +96,13 @@ These bindings patch declared regions, not arbitrary component logic. Conditiona
81
96
 
82
97
  Browse mode is read-only. Edit mode lets editors select bound regions, work with section structure and open properties. The page remains visible with a notice if the required bindings are missing. A double-click on a supported single-line text binding opens inline editing; Enter accepts the draft value and Escape reverts that inline edit. Neither action writes content.
83
98
 
84
- Clicking a bound schema `markdown` region opens the same block editor the article body uses, mounted on that rendered element. Root-level fields, section fields and fields nested in objects or array items all take this path. Editors get block handles with duplicate, delete and move actions, drag reordering, insertion between blocks, table and media insertion, inline formatting, and undo and redo. The toolbar and the block handle float beside the prose, which keeps the website's typography and flow. Edits write back to the same in-memory Markdown string, and the active rich-text region belongs to its editor until editing ends rather than being overwritten by the DOM patcher.
99
+ In Inline preview mode, clicking an eligible bound schema `markdown` region on a non-shared page opens the same block editor the article body uses, mounted on that rendered element. Root-level fields, section fields, nested objects and single-shape object arrays can use this path. Nested-array lookup uses the first declared object shape; fields exclusive to other item types in a heterogeneous array cannot be edited inline. Editors get block handles with duplicate, delete and move actions, drag reordering, insertion between blocks, table and media insertion, inline formatting, and undo and redo. The toolbar and the block handle float beside the prose, which keeps the website's typography and flow. Edits write back to the same in-memory Markdown string, and the active rich-text region belongs to its editor until editing ends rather than being overwritten by the DOM patcher.
100
+
101
+ A field uses the fallback Markdown editor when its source cannot round-trip through the block format without loss, such as raw HTML, a Markdown table, a fenced block with a language, or a hard-wrapped paragraph. The fallback has its own round-trip check: a language-tagged fence can remain visual there, while unsupported syntax opens in source view. Nothing is converted or written until an editor actually changes the field, and the stored value stays a Markdown string either way. The shared toolbar does not make every article block or format losslessly representable in Markdown; edited content may reopen in the fallback editor. Set `sidebarOnly: true` on a field to keep it in the properties panel, as described in [content model](content-model.md).
85
102
 
86
- A field keeps the inline Markdown editor when its source cannot round-trip through the block format without loss, such as raw HTML, a Markdown table, a fenced block with a language, or a hard-wrapped paragraph. That editor falls back to its source view for the same reasons. Nothing is converted or written until an editor actually changes the field, and the stored value stays a Markdown string either way. Set `sidebarOnly: true` on a field to keep it in the properties panel, as described in [content model](content-model.md).
103
+ Settings > Content > Article body editing selects Inline preview (the default) or Sidebar. Eligible schema Markdown regions use Inline preview in place; the properties panel offers Edit in preview instead of a second editor. Unbound fields, shared-document forms and fields declared `sidebarOnly` keep their properties-panel editor. Sidebar moves eligible rich-text editing back into the panel.
87
104
 
88
- Articles use the same canvas. Settings > Content > Article body editing selects Inline preview (the default) or Sidebar. Inline preview mounts the body editor on the prepared `$body` element and opens bound rich-text Markdown regions in place; the properties panel then offers Edit in preview for those regions instead of a second editor. Fields without a preview binding, and fields declared `sidebarOnly`, keep their sidebar editor, and Sidebar moves rich-text editing back into the panel. If the `$body` region is unavailable, the body editor stays in the sidebar. Only one editor owns the draft at a time, and the preview patcher leaves its active region alone. Article Markdown view is read-only.
105
+ Articles use the same canvas, but their body and frontmatter follow separate paths. Inline preview mounts the article body editor on the prepared `$body` element; if that region is unavailable, the body stays in the sidebar. Article frontmatter Markdown fields remain sidebar-edited even when bound in the preview. Only one editor owns an active region, and the preview patcher leaves it alone. Article Markdown view is read-only.
89
106
 
90
107
  Explicit Save writes content through the configured backend. Navigation guards and failed-save handling apply to canvas edits too. Browser preferences save independently. See [GitHub](github.md) for saved drafts and publishing, and [Extensions](extensions.md) for custom article content.
91
108
 
@@ -95,6 +112,6 @@ The iframe uses `sandbox="allow-same-origin"` without `allow-scripts`. The trust
95
112
 
96
113
  Same-origin pages load directly in the iframe. Cross-origin pages are fetched with CORS, sanitized and displayed in a same-origin `srcdoc` snapshot. There is no draft postMessage bridge. See [Preview hosts](preview-hosts.md) for credentials, headers, CSP and subresource limits before using a remote host.
97
114
 
98
- Markdown preview disables raw HTML and passes its rendered output through a DOMPurify allowlist before inserting a DOM fragment. Article preview uses the shared article renderer and sanitizes its output. URL bindings reject script/data schemes, protocol-relative URLs, backslashes, whitespace and control characters; image bindings accept relative or HTTP(S) URLs, and links also accept `mailto:`. Host-specific Markdown plugins and JavaScript syntax highlighting are outside this renderer's scope.
115
+ Markdown preview disables raw HTML and passes its rendered output through a DOMPurify allowlist before inserting a DOM fragment. Article preview uses the shared article renderer and sanitizes its output. URL bindings reject script/data schemes, protocol-relative URLs, backslashes, ASCII whitespace and ASCII control characters; image bindings accept relative or HTTP(S) URLs, and links also accept `mailto:`. Host-specific Markdown plugins and JavaScript syntax highlighting are outside this renderer's scope.
99
116
 
100
117
  The host remains responsible for safe public rendering. Preview sanitization does not sanitize your Astro templates, create public routes or implement draft filtering for visitors. Verify both the editor canvas and the public output when adding a field or section.
package/docs/seo.md CHANGED
@@ -68,7 +68,7 @@ const socialImage = imageUrl && ["https:", "http:"].includes(imageUrl.protocol)
68
68
  </html>
69
69
  ```
70
70
 
71
- Set Astro's `site` to your real public base URL so relative image paths produce production URLs; `Astro.url` is the development fallback in this example. Choose your own description fallback, title suffix and document language. The starter uses its shared Site settings for the default description and site-name suffix, rather than a fixed product name.
71
+ Set Astro's `site` to your real public base URL so relative image paths produce production URLs; this example falls back to `Astro.url` whenever `site` is unset, including production builds. Choose your own description fallback, title suffix and document language. The starter uses its shared Site settings for the default description and site-name suffix, rather than a fixed product name.
72
72
 
73
73
  Canonical links, social title/description tags, structured data, sitemaps and language alternates remain host-template work. The example emits only the tags shown. Check the built HTML, including absolute image URLs and empty-image behavior, rather than treating the editor canvas as proof that public metadata is correct.
74
74
 
package/docs/sidebar.md CHANGED
@@ -34,7 +34,7 @@ sidebar: [
34
34
 
35
35
  Entries appear in configured order. Use headings for labels and separators for plain dividers; these groups are flat, not nested or collapsible. A link's optional `label` overrides its content-derived label.
36
36
 
37
- Targets use stable identifiers rather than display labels:
37
+ Targets use content IDs or configured names. Category targets use the exact declared category string:
38
38
 
39
39
  | Target | Opens |
40
40
  | --- | --- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigel-cms/rigel",
3
- "version": "1.4.0-rc.3",
3
+ "version": "1.5.0-rc.1",
4
4
  "description": "A CMS for Astro sites. You edit the text, Git keeps the history.",
5
5
  "keywords": [
6
6
  "astro",
package/src/preview.ts CHANGED
@@ -217,7 +217,22 @@ export function patchPreview(doc: Document, values: Record<string, unknown>, bod
217
217
  if (blocks) previews.set(el, mountExtensionPreviews(el));
218
218
  rendered.set(el, source);
219
219
  }
220
- for (const [anchor, attr, tag] of [["image", "src", "IMG"], ["alt", "alt", "IMG"], ["href", "href", "A"]]) {
220
+ for (const el of doc.querySelectorAll<HTMLImageElement>("[data-rigel-image]")) {
221
+ if (el.tagName !== "IMG") continue;
222
+ const result = get(el, el.getAttribute("data-rigel-image")!);
223
+ if (!result.found) continue;
224
+ const value = result.value === null || result.value === undefined ? "" : String(result.value);
225
+ const safe = safePreviewUrl(value, true);
226
+ if (el.hasAttribute("data-rigel-src")) {
227
+ el.removeAttribute("srcset");
228
+ el.removeAttribute("sizes");
229
+ for (const source of el.closest("picture")?.querySelectorAll("source") ?? []) { source.removeAttribute("srcset"); source.removeAttribute("sizes"); }
230
+ if (safe) el.setAttribute("data-rigel-src", value);
231
+ }
232
+ if (safe) { if (el.getAttribute("src") !== value) el.setAttribute("src", value); }
233
+ else el.removeAttribute("src");
234
+ }
235
+ for (const [anchor, attr, tag] of [["alt", "alt", "IMG"], ["href", "href", "A"]]) {
221
236
  for (const el of doc.querySelectorAll(`[data-rigel-${anchor}]`)) {
222
237
  if (el.tagName !== tag) continue;
223
238
  const result = get(el, el.getAttribute(`data-rigel-${anchor}`)!);