@rigel-cms/rigel 1.4.0-rc.2 → 1.4.0-rc.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.
- package/README.md +4 -2
- package/docs/README.md +28 -0
- package/docs/authors.md +133 -0
- package/docs/content-locales.md +1 -1
- package/docs/content-model.md +131 -0
- package/docs/getting-started.md +139 -0
- package/docs/github.md +112 -0
- package/docs/media.md +75 -0
- package/docs/menus.md +95 -0
- package/docs/preview-hosts.md +78 -0
- package/docs/preview.md +100 -0
- package/docs/seo.md +87 -0
- package/docs/sidebar.md +97 -0
- package/package.json +2 -3
- package/src/article-render.ts +12 -5
- package/src/editor/App.svelte +36 -15
- package/src/editor/i18n/de/fields.json +2 -0
- package/src/editor/i18n/de/shell.json +2 -1
- package/src/editor/i18n/en/fields.json +2 -0
- package/src/editor/i18n/en/shell.json +2 -1
- package/src/editor/i18n/es/fields.json +2 -0
- package/src/editor/i18n/es/shell.json +2 -1
- package/src/editor/i18n/fr/fields.json +2 -0
- package/src/editor/i18n/fr/shell.json +2 -1
- package/src/editor/i18n/it/fields.json +2 -0
- package/src/editor/i18n/it/shell.json +2 -1
- package/src/editor/i18n/ja/fields.json +2 -0
- package/src/editor/i18n/ja/shell.json +2 -1
- package/src/editor/i18n/nl/fields.json +2 -0
- package/src/editor/i18n/nl/shell.json +2 -1
- package/src/editor/i18n/ru/fields.json +2 -0
- package/src/editor/i18n/ru/shell.json +2 -1
- package/src/editor/i18n/zh/fields.json +2 -0
- package/src/editor/i18n/zh/shell.json +2 -1
- package/src/editor/lib/BlockEditor.svelte +4 -3
- package/src/editor/lib/CollectionPanel.svelte +10 -2
- package/src/editor/lib/MarkdownEditor.svelte +38 -5
- package/src/editor/lib/PropertiesTabs.svelte +8 -1
- package/src/editor/lib/SchemaField.svelte +16 -2
- package/src/editor/lib/SchemaPanel.svelte +9 -3
- package/src/editor/lib/VisualEditor.svelte +64 -13
- package/src/editor/lib/markdown-blocks.ts +69 -0
- package/src/editor/lib/media-library.ts +2 -1
- package/src/editor/styles.css +7 -1
- package/src/schema/types.ts +1 -0
- package/src/schema/validate.ts +1 -0
package/docs/media.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Media
|
|
2
|
+
|
|
3
|
+
Media browses supported files in the site's public media folder. Files already in that folder appear alongside uploads made through Rigel. The library is shared content, not a private file store.
|
|
4
|
+
|
|
5
|
+
## Configure storage
|
|
6
|
+
|
|
7
|
+
Set these options in the [Astro integration](getting-started.md):
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
rigel({
|
|
11
|
+
mediaDir: "images",
|
|
12
|
+
maxFileSize: 2 * 1024 * 1024,
|
|
13
|
+
})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`mediaDir` is relative to Astro's `publicDir` and defaults to `rigel-media`. Traversal paths are rejected. `maxFileSize` is a positive integer in bytes; the default is 2 MiB, displayed as 2 MB in the interface. Both the browser and local upload endpoint enforce the limit, including streamed requests without a reliable Content-Length.
|
|
17
|
+
|
|
18
|
+
The local development server supports uploading, metadata updates and file deletion. A static build emits `/rigel/media.json` as a read-only snapshot. Its `assets` array contains all supported files; `images` contains images only. Rebuild to refresh the snapshot. GitHub-backed content editing does not provide GitHub media writes; repository imports need the local server. See [GitHub](github.md) for content Save and Publish behavior.
|
|
19
|
+
|
|
20
|
+
## Supported files
|
|
21
|
+
|
|
22
|
+
The library reads subfolders recursively, skips hidden files and symlinks, and accepts these six categories:
|
|
23
|
+
|
|
24
|
+
| Category | Extensions |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| Images | jpg, jpeg, png, webp, gif, avif |
|
|
27
|
+
| Audio | mp3, wav, ogg, m4a |
|
|
28
|
+
| Video | mp4, webm, mov |
|
|
29
|
+
| Documents | txt, pdf, doc, docx, rtf, odt, ppt, pptx |
|
|
30
|
+
| Spreadsheets | xls, xlsx, csv, ods |
|
|
31
|
+
| Archives | zip, gz, tar, 7z, rar |
|
|
32
|
+
|
|
33
|
+
SVG, HTML, scripts, executables and unknown extensions are outside the upload allowlist. An Other filter is present for unclassified listing entries; it does not enable additional upload formats. Image fields and rich-text image pickers accept images only. File fields can narrow their choices with `accept`; video pickers use the supported video formats. See the [content model](content-model.md).
|
|
34
|
+
|
|
35
|
+
## Browse and upload
|
|
36
|
+
|
|
37
|
+
Use Grid or List view, search, category and modified-month filters. Filters combine; Reset filters clears them. Refresh rereads the listing. Dates come from filesystem modification time in UTC, not an upload history. A checkout can change them. Missing date metadata does not match a specific month.
|
|
38
|
+
|
|
39
|
+
Images have thumbnails, videos have playback previews, and PDF and text files have document previews. Other files show a type icon and metadata with a download link. Previewing a file is not a safety check.
|
|
40
|
+
|
|
41
|
+
Open Upload to choose a local file, an image URL, Lorem Picsum or an enabled stock provider. Choose a result, review its preview and confirm. Back changes the selection; cancelling before confirmation leaves content unchanged. Confirmation writes imported bytes immediately, separately from saving the document. Cancelling a later document edit does not remove an uploaded file.
|
|
42
|
+
|
|
43
|
+
The library's Delete file action removes the file and its metadata after confirmation. Check its uses first: deleting an asset can break existing content links.
|
|
44
|
+
|
|
45
|
+
## Labels and metadata
|
|
46
|
+
|
|
47
|
+
Metadata lives in `.rigel-media.json` inside the configured media folder, keyed by public asset URL. Each entry can contain `name`, `title`, `description`, image `alt` and `author`. The listing merges these values into its asset records. Treat the sidecar and listing as public data.
|
|
48
|
+
|
|
49
|
+
New uploads use random UUID filenames.
|
|
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.
|
|
52
|
+
|
|
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
|
+
|
|
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.
|
|
56
|
+
|
|
57
|
+
## Image URLs and stock providers
|
|
58
|
+
|
|
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
|
+
|
|
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.
|
|
62
|
+
|
|
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
|
+
|
|
65
|
+
Provider keys live in browser storage and requests go directly to the provider. They are visible to scripts on the same origin and in browser network tools. Use credentials intended for browser clients, restrict them where supported, and keep the CMS origin trusted. For Unsplash, use an Access Key, never a Secret Key. Browser storage is not a place for server credentials.
|
|
66
|
+
|
|
67
|
+
## Upload and hosting safety
|
|
68
|
+
|
|
69
|
+
The local upload endpoint requires a matching request origin and is disabled in production. It validates safe filenames, canonical MIME types, size and file signatures, then writes a new file without overwriting an existing asset. Metadata updates and deletion also require same-origin requests and listed asset URLs.
|
|
70
|
+
|
|
71
|
+
Signature and container checks are not full decoding or malware scanning. Rigel does not extract archives. Office documents and PDFs can contain active content, so upload and download only files you trust.
|
|
72
|
+
|
|
73
|
+
The local media handler sets a canonical Content-Type and `X-Content-Type-Options: nosniff`. Images and videos can be inline; PDF and text previews can also be inline, while other downloads use attachment disposition. Configure equivalent headers on the production host: a static build cannot enforce response headers. Exclusion from the CMS listing does not make a file under `publicDir` private. Keep secrets and untrusted files outside it.
|
|
74
|
+
|
|
75
|
+
See [preview hosts](preview-hosts.md) for host setup, [SEO](seo.md) for image metadata used by the site, and the [documentation index](README.md) for the other guides.
|
package/docs/menus.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Menus
|
|
2
|
+
|
|
3
|
+
Menus are developer-declared JSON documents. Editors can change their titles, links and ordering, but menu creation and deletion belong in the site's source files.
|
|
4
|
+
|
|
5
|
+
## Configure menus
|
|
6
|
+
|
|
7
|
+
Declare a folder in the [Astro integration](getting-started.md):
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
rigel({
|
|
11
|
+
menus: {
|
|
12
|
+
folder: "src/content/menus",
|
|
13
|
+
maxDepth: 3,
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The folder is relative to the site root. `maxDepth` defaults to 3 and accepts integers from 1 to 10. Top-level items count as depth 1. The limit applies to each whole branch, including branches moved with Indent or Outdent.
|
|
19
|
+
|
|
20
|
+
Create `src/content/menus/main.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"slug": "main",
|
|
25
|
+
"title": "Main navigation",
|
|
26
|
+
"items": [
|
|
27
|
+
{
|
|
28
|
+
"id": "company",
|
|
29
|
+
"label": "Company",
|
|
30
|
+
"url": "/about/",
|
|
31
|
+
"children": [
|
|
32
|
+
{
|
|
33
|
+
"id": "contact",
|
|
34
|
+
"label": "Contact",
|
|
35
|
+
"url": "/contact/",
|
|
36
|
+
"newTab": false,
|
|
37
|
+
"children": []
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The slug must be lowercase kebab-case and match the filename. Each item needs a nonempty stable `id`, `label`, `url` and a `children` array, even when empty. IDs must be unique across the entire menu. `newTab` is an optional boolean; omission means ordinary same-tab navigation.
|
|
46
|
+
|
|
47
|
+
Group items use this same shape and require a URL. A group is not a separate content type. Menus are stored link trees: adding a page or changing its route does not automatically update menu links. Review links when changing [content routes](preview.md).
|
|
48
|
+
|
|
49
|
+
## Edit links
|
|
50
|
+
|
|
51
|
+
The editor uses adjacent panels. The first lists root items; selecting one opens its label, URL and children in the next panel. Selecting a sibling replaces the open branch. The path above the panels returns to parents without discarding edits. Narrow screens show the current panel.
|
|
52
|
+
|
|
53
|
+
Add item creates a root item; Add child works inside the selected item. New items open for editing immediately. Drag handles reorder within the current list without nesting items or moving them between panels. Up, Down, Indent and Outdent provide explicit movement actions. Remove asks for confirmation and removes the selected item's children too.
|
|
54
|
+
|
|
55
|
+
Open in new tab defaults off. This stores `newTab`; the website renderer must apply the matching link attributes.
|
|
56
|
+
|
|
57
|
+
Selection and panel navigation leave the content unchanged. Edits remain in memory until Save. Leaving with unsaved edits offers Save and leave, Discard changes or Keep editing.
|
|
58
|
+
|
|
59
|
+
## Render on the website
|
|
60
|
+
|
|
61
|
+
Read the menu through the build's virtual manifest. This Astro example renders its root links:
|
|
62
|
+
|
|
63
|
+
```astro
|
|
64
|
+
---
|
|
65
|
+
import { menus } from "virtual:rigel-manifest";
|
|
66
|
+
const main = menus?.entries.find(menu => menu.slug === "main");
|
|
67
|
+
---
|
|
68
|
+
<nav aria-label={main?.title ?? "Main navigation"}>
|
|
69
|
+
<ul>
|
|
70
|
+
{main?.items.map(item => (
|
|
71
|
+
<li>
|
|
72
|
+
<a
|
|
73
|
+
href={item.url}
|
|
74
|
+
target={item.newTab ? "_blank" : undefined}
|
|
75
|
+
rel={item.newTab ? "noopener noreferrer" : undefined}
|
|
76
|
+
>{item.label}</a>
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
</nav>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For nested menus, render each item's `children` recursively in your own component. Use real links and keyboard-accessible controls for submenus. The starter's `src/components/MenuLinks.astro` demonstrates recursive `Astro.self` rendering with disclosure controls. Rigel stores the links; your templates own their layout and interaction.
|
|
84
|
+
|
|
85
|
+
A static website consumes menu files from its build checkout, not the editor's unsaved state. With the GitHub backend, Save writes to the configured draft branch; publishing and rebuilding the live branch are separate steps. See [GitHub](github.md) and [preview hosts](preview-hosts.md).
|
|
86
|
+
|
|
87
|
+
## Storage and validation
|
|
88
|
+
|
|
89
|
+
The local menu endpoint supports reading and updating existing regular files. It is available only in development with `backend: "local"` and a configured menu folder. Writes require a matching request origin. It rejects creation, deletion, mismatched slugs and symlinked folders or files, and replaces validated JSON atomically.
|
|
90
|
+
|
|
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
|
+
|
|
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.
|
|
94
|
+
|
|
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.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Preview hosts
|
|
2
|
+
|
|
3
|
+
Live links, deployed draft pages and unsaved canvas edits are separate. A successful Save does not mean a site build has finished, and a preview URL is not a save backend. See [GitHub](github.md) for saving and publishing, and [Preview](preview.md) for DOM bindings.
|
|
4
|
+
|
|
5
|
+
## Configure destinations
|
|
6
|
+
|
|
7
|
+
This complete `astro.config.mjs` example sets URL mappings only. Add your site's schema and collections separately:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { defineConfig } from "astro/config";
|
|
11
|
+
import { rigel } from "@rigel-cms/rigel";
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
integrations: [
|
|
15
|
+
rigel({
|
|
16
|
+
preview: {
|
|
17
|
+
liveUrl: "https://www.example.com",
|
|
18
|
+
draftUrl: "https://draft.example.com",
|
|
19
|
+
pages: { about: "/about/" },
|
|
20
|
+
documents: { page: { url: "/{slug}/", index: "/" } },
|
|
21
|
+
collections: { blog: { url: "/blog/{slug}/", new: "/preview/blog/" } },
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Mappings must match real routes; Rigel does not infer or generate them. `index` maps an effective document slug of `home`. Route mappings are root-relative paths, not external URLs. They reject traversal, backslashes, whitespace, query strings, fragments and encoded dot/slash escapes. Ordinary routes cannot point into `/rigel/`.
|
|
29
|
+
|
|
30
|
+
`liveUrl` is the base for View live page. `draftUrl` is the base for mapped canvas routes and the draft website destination. Omit either base to use the editor's own origin for that destination; they default independently. Setting `liveUrl` alone does not move the canvas. A content field such as `draft` does not choose the host.
|
|
31
|
+
|
|
32
|
+
Bases accept HTTPS with an optional path prefix. For example, `https://www.example.com/site` plus `/about/` resolves to `https://www.example.com/site/about/`. HTTP is allowed only on loopback for development. Credentials, queries, fragments, percent-encoded base paths and dot segments are rejected during configuration parsing.
|
|
33
|
+
|
|
34
|
+
## Separate templates from public routes
|
|
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.
|
|
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.
|
|
39
|
+
|
|
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
|
+
|
|
42
|
+
The `previewPath` and `previewUrl` helpers are exported from `@rigel-cms/rigel/preview`. `previewPath(config, target, isNew)` returns a relative path. `previewUrl(config, target, mode, isNew)` resolves `"draft"` (canvas), `"live"` or `"website"` (draft website) destinations. A missing or rejected mapping returns `null`. Host templates and builds remain developer-owned.
|
|
43
|
+
|
|
44
|
+
## Prepare a remote draft host
|
|
45
|
+
|
|
46
|
+
Deploy prepared HTML for every mapped canvas route, including article `new` routes. Keep the [anchors and templates](preview.md) used by your public components. The editor fetches a deployed HTML shell and patches bound fields locally; it does not send unsaved content to the host to render a page. Unanchored regions retain deployed content.
|
|
47
|
+
|
|
48
|
+
Cross-origin HTML must be readable without authentication and return an HTML Content-Type and a CORS response header. For an editor opened at `https://cms.example.com/rigel/`, the response can contain:
|
|
49
|
+
|
|
50
|
+
```http
|
|
51
|
+
Content-Type: text/html; charset=utf-8
|
|
52
|
+
Access-Control-Allow-Origin: https://cms.example.com
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The allowed origin has no path. On a host that supports `public/_headers`, an equivalent file is:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
/*
|
|
59
|
+
Access-Control-Allow-Origin: https://cms.example.com
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use your serving platform's response-header configuration if it does not apply `_headers`. A public host may allow `*`. For a dynamic origin allowlist, return the matching origin and `Vary: Origin`. Credentialed CORS and tokens in preview URLs are inappropriate for this fetch: it omits credentials and has no authentication handshake.
|
|
63
|
+
|
|
64
|
+
Use final URLs, including the correct trailing slash. Cross-origin HTML redirects are rejected. The HTML fetch has a 15-second timeout; the frame also has a 20-second loading timeout. Missing routes, non-HTML responses and fetch failures produce a notice. The browser may group CORS, CSP and network failures under the same fetch error, so check its console and network panel.
|
|
65
|
+
|
|
66
|
+
If the editor has a CSP, allow the remote draft origin in `connect-src`, plus the needed style, image and font sources in their directives. A `srcdoc` document inherits the editor's CSP. Fonts may require separate CORS headers. Assets must be reachable without a login or anti-hotlink restriction; use HTTPS assets when the editor uses HTTPS.
|
|
67
|
+
|
|
68
|
+
## Understand the snapshot boundary
|
|
69
|
+
|
|
70
|
+
Same-origin destinations load directly in a sandboxed iframe. For a cross-origin destination, the browser cannot expose the iframe DOM to the editor. Rigel instead fetches HTML with CORS, `credentials: "omit"`, `referrerPolicy: "no-referrer"` and `redirect: "error"`. It sanitizes the result with DOMPurify and places it in same-origin `srcdoc`, with a base URL pointing at the remote page so relative assets resolve there.
|
|
71
|
+
|
|
72
|
+
The snapshot removes scripts, inline event handlers, authored base/meta elements, nested frames, objects, embeds and forms. It keeps stylesheet links, adds a restrictive script-free CSP and uses `sandbox="allow-same-origin"` without script permission. The trusted parent can patch bound DOM and mount inline editing. Text edits keep the loaded frame; they do not refetch HTML. Save and unsaved-navigation guards work independently of the host.
|
|
73
|
+
|
|
74
|
+
This is an inert HTML preview, not the site's interactive application. Hydration and script-driven widgets do not run. Remote response CSP and framing headers are not copied onto the sanitized snapshot; the editor's inherited policy and the snapshot policy apply. Same-origin direct frames still need host headers that permit framing.
|
|
75
|
+
|
|
76
|
+
Configure only hosts and assets you trust. Styles and images still load through the browser, and subresource requests follow browser cookie rules even though the HTML fetch omits credentials. The remote HTML fetch does not receive Rigel's GitHub token or browser storage. This is not an authenticated preview service, and a CORS allowlist does not make public draft HTML private. Keep confidential content off publicly readable preview hosts.
|
|
77
|
+
|
|
78
|
+
For schema setup, see [Content model](content-model.md); for routing in more than one content language, see [Content locales](content-locales.md). Return to the [documentation index](README.md).
|
package/docs/preview.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Preview
|
|
2
|
+
|
|
3
|
+
Rigel previews prepared HTML from your site. After loading the page, the editor patches bound elements with the in-memory draft. Typing does not require saving, rebuilding or requesting a rendered page. Image changes can load a different asset.
|
|
4
|
+
|
|
5
|
+
See [Content model](content-model.md) for schemas and [Preview hosts](preview-hosts.md) for live and draft destinations. The [documentation index](README.md) lists the other guides.
|
|
6
|
+
|
|
7
|
+
## Map prepared routes
|
|
8
|
+
|
|
9
|
+
This is an integration configuration excerpt:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
rigel({
|
|
13
|
+
preview: {
|
|
14
|
+
pages: { about: "/about/" },
|
|
15
|
+
documents: {
|
|
16
|
+
page: { url: "/{slug}/", index: "/", template: "/rigel/preview/page/" },
|
|
17
|
+
},
|
|
18
|
+
collections: {
|
|
19
|
+
blog: { url: "/blog/{slug}/", template: "/rigel/preview/blog/" },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
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
|
+
|
|
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.
|
|
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.
|
|
30
|
+
|
|
31
|
+
## Bind rendered fields
|
|
32
|
+
|
|
33
|
+
These HTML excerpts illustrate the scalar anchors. Render the saved values through your host template:
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<h1 data-rigel-text="title">Saved title</h1>
|
|
37
|
+
<div data-rigel-richtext="intro.body"><p>Saved rendered Markdown</p></div>
|
|
38
|
+
<img data-rigel-image="hero.image" data-rigel-alt="hero.alt" src="/images/cover.jpg" alt="Saved description">
|
|
39
|
+
<a data-rigel-href="intro.href" href="/about/">Read more</a>
|
|
40
|
+
<div data-rigel-richtext="$body"><p>Saved rendered article body</p></div>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`data-rigel-text` updates text content. `data-rigel-richtext` renders a Markdown string, or the article body for `$body`. Image and alt bindings apply to `<img>`; href bindings apply to `<a>`. Collection tags can use `data-rigel-tags="tags"` on a `<ul>` or `<ol>` to render comma-separated tags as list items.
|
|
44
|
+
|
|
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
|
+
|
|
47
|
+
## Prepare changing sections and lists
|
|
48
|
+
|
|
49
|
+
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):
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<div data-rigel-each="sections" data-rigel-doc="page:about">
|
|
53
|
+
<section data-rigel-section="intro" data-rigel-key="intro1" data-rigel-doc="page:about">
|
|
54
|
+
<h1 data-rigel-text="sections.intro1.heading" data-rigel-doc="page:about">About</h1>
|
|
55
|
+
<div data-rigel-richtext="sections.intro1.body" data-rigel-doc="page:about"></div>
|
|
56
|
+
</section>
|
|
57
|
+
<template data-rigel-item-template="intro">
|
|
58
|
+
<section data-rigel-section="intro" data-rigel-key="__item__" data-rigel-doc="page:about">
|
|
59
|
+
<h1 data-rigel-text="sections.__item__.heading" data-rigel-doc="page:about"></h1>
|
|
60
|
+
<div data-rigel-richtext="sections.__item__.body" data-rigel-doc="page:about"></div>
|
|
61
|
+
</section>
|
|
62
|
+
</template>
|
|
63
|
+
</div>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Keep rendered items and their templates as direct children of the `data-rigel-each` container. The patcher clones the template's first element, replaces the `sections.__item__` path prefix with the new stable key, then adds, removes or reorders keyed children. Templates remain available when the list becomes empty. Use the same Astro component for public and template markup to keep them consistent.
|
|
67
|
+
|
|
68
|
+
For an ordinary object array, use `data-rigel-item="row1"` on rendered children and an empty `data-rigel-item-template` value. Its template has one root item keyed `__item__`; descendant paths use the list path followed by `.__item__`. Named items match templates by `_type`. Prepared markup is required for each type you want to insert visually.
|
|
69
|
+
|
|
70
|
+
Keep optional markup mounted with `data-rigel-if="path"`; render it initially `hidden` when the saved value is empty. The patcher toggles `hidden` using the field's truthiness. Include this CSS so authored display rules do not override it:
|
|
71
|
+
|
|
72
|
+
```css
|
|
73
|
+
[data-rigel-if][hidden] { display: none !important; }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A shared region can declare `data-rigel-reference="sections.intro1.callout"` with the page's `data-rigel-doc`. Its descendants declare the referenced document's ID and local paths. Changing the selector rebinds those IDs, including inside array templates. If only part of the region belongs to the reference, wrap that part in an element with `data-rigel-reference-target` and keep page-local anchors outside it.
|
|
77
|
+
|
|
78
|
+
These bindings patch declared regions, not arbitrary component logic. Conditional Astro output that was omitted from the HTML, unprepared article feeds and script-driven layouts cannot be regenerated from a field change. Shared records themselves open standalone forms rather than a website canvas; their values reach consuming pages after Save and the host's refresh/build.
|
|
79
|
+
|
|
80
|
+
## Edit without saving
|
|
81
|
+
|
|
82
|
+
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
|
+
|
|
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.
|
|
85
|
+
|
|
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).
|
|
87
|
+
|
|
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.
|
|
89
|
+
|
|
90
|
+
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
|
+
|
|
92
|
+
## Keep the preview inert
|
|
93
|
+
|
|
94
|
+
The iframe uses `sandbox="allow-same-origin"` without `allow-scripts`. The trusted parent editor reads and patches its DOM. Site scripts, forms and popups stay disabled, and editor handlers suppress link navigation and submissions. Browse mode does not turn the page into an interactive site. Do not add `allow-scripts`: together with same-origin access it would expose editor storage to scripts in the preview.
|
|
95
|
+
|
|
96
|
+
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
|
+
|
|
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.
|
|
99
|
+
|
|
100
|
+
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
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# SEO
|
|
2
|
+
|
|
3
|
+
Rigel provides metadata fields for website pages and article collections. Your Astro templates must put those values into the public page's `<head>`; adding a CMS field alone does not change the rendered site.
|
|
4
|
+
|
|
5
|
+
See [Content model](content-model.md) for native documents and [Preview](preview.md) for canvas bindings. The [documentation index](README.md) lists the other guides.
|
|
6
|
+
|
|
7
|
+
## Use the native fields
|
|
8
|
+
|
|
9
|
+
The integration adds missing declarations for these top-level keys to non-shared schema document types and article collections:
|
|
10
|
+
|
|
11
|
+
| Key | Type | Editor label |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `metaTitle` | `string` | SEO title |
|
|
14
|
+
| `description` | `text` | SEO description |
|
|
15
|
+
| `ogImage` | `image` | Social image |
|
|
16
|
+
|
|
17
|
+
Existing declarations with the same key are retained rather than duplicated, and configuration parsing does not rewrite saved content. Keep the native key/type shape if you declare these fields yourself. Shared records, such as authors or site settings, do not receive the automatic SEO fields. Legacy copy fields need their own declarations and host rendering.
|
|
18
|
+
|
|
19
|
+
For schema pages, values live directly in the document JSON. For article collections, they live in the file's `frontmatter` object alongside the article's other fields; the separate `body` stores Lexical state. `ogImage` is a URL string, not an image object. A page's relevant values might look like this JSON excerpt:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"title": "About",
|
|
24
|
+
"metaTitle": "About the observatory",
|
|
25
|
+
"description": "Learn about the observatory and its public programme.",
|
|
26
|
+
"ogImage": "/images/observatory-social.jpg",
|
|
27
|
+
"draft": false
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The website page and article properties panels separate Content and SEO. Page name (`title`) identifies the CMS entry; it is independent of headings inside sections and of `metaTitle`. Editing the name does not change the URL slug. Keep article title, social image and body/section images separate too. SEO fields are excluded from default table columns unless explicitly requested.
|
|
32
|
+
|
|
33
|
+
## Render the head
|
|
34
|
+
|
|
35
|
+
This complete minimal Astro layout accepts page or article metadata. Pass the appropriate saved values from your route; it does not load content itself:
|
|
36
|
+
|
|
37
|
+
```astro
|
|
38
|
+
---
|
|
39
|
+
interface Props {
|
|
40
|
+
title: string;
|
|
41
|
+
metaTitle?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
ogImage?: string;
|
|
44
|
+
}
|
|
45
|
+
const { title, metaTitle, description, ogImage } = Astro.props;
|
|
46
|
+
const headTitle = metaTitle?.trim() || title;
|
|
47
|
+
const headDescription = description?.trim() || "Notes from the observatory.";
|
|
48
|
+
const imageUrl = ogImage?.trim()
|
|
49
|
+
? new URL(ogImage.trim(), Astro.site ?? Astro.url)
|
|
50
|
+
: undefined;
|
|
51
|
+
const socialImage = imageUrl && ["https:", "http:"].includes(imageUrl.protocol)
|
|
52
|
+
? imageUrl.href
|
|
53
|
+
: undefined;
|
|
54
|
+
---
|
|
55
|
+
<html lang="en">
|
|
56
|
+
<head>
|
|
57
|
+
<meta charset="utf-8" />
|
|
58
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
59
|
+
<title>{headTitle}</title>
|
|
60
|
+
<meta name="description" content={headDescription} />
|
|
61
|
+
{socialImage && <>
|
|
62
|
+
<meta property="og:image" content={socialImage} />
|
|
63
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
64
|
+
<meta name="twitter:image" content={socialImage} />
|
|
65
|
+
</>}
|
|
66
|
+
</head>
|
|
67
|
+
<body><slot /></body>
|
|
68
|
+
</html>
|
|
69
|
+
```
|
|
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.
|
|
72
|
+
|
|
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
|
+
|
|
75
|
+
## Read the guidance
|
|
76
|
+
|
|
77
|
+
SEO title and description show live character counts and length meters. Titles target 50 to 60 characters; descriptions target 120 to 160. Empty values are neutral, short values approach the target in yellow, the recommended range is green, and very short or over-target values are red. These are editing hints, not ranking scores or hard limits. Values remain editable and saveable outside the suggested range. Host-added title suffixes are not counted, and actual snippets vary by query, device and available width.
|
|
78
|
+
|
|
79
|
+
Social image uses the native [media picker](media.md) and supports removal. Its preview uses a centered 1200 × 628 crop within the sidebar. That display does not resize or crop the stored image bytes. Supply an image suitable for social sharing rather than assuming a page-section or article-cover image will be used automatically.
|
|
80
|
+
|
|
81
|
+
## Keep drafts out of public builds
|
|
82
|
+
|
|
83
|
+
Non-shared schema document types receive a `draft` boolean declaration if they lack one. Article collections declare their own Draft field. A saved Draft badge is content state, not an access-control rule or deployment result.
|
|
84
|
+
|
|
85
|
+
The starter's page and article routes filter drafts from production output while retaining routes in development. Your host must implement that filtering in its own routes and feeds, and decide which routes a deployed draft host emits. Public static files are readable by anyone who can reach their URLs, even if omitted from navigation.
|
|
86
|
+
|
|
87
|
+
SEO edits and Draft changes stay in memory until explicit Save. Saving and publishing depend on the configured backend; see [GitHub](github.md). Preview host selection is separate from a record's Draft value; see [Preview hosts](preview-hosts.md).
|
package/docs/sidebar.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Content navigation
|
|
2
|
+
|
|
3
|
+
The CMS sidebar opens Pages, configured article collections, shared documents, Menus and Media. Opening an entry uses its editor and explicit Save workflow. The sidebar controls editor navigation; [menus](menus.md) control links on the public website.
|
|
4
|
+
|
|
5
|
+
## Default navigation
|
|
6
|
+
|
|
7
|
+
Without a `sidebar` option, Rigel builds these groups from the content manifest:
|
|
8
|
+
|
|
9
|
+
- Website contains Pages, configured collections and named page categories.
|
|
10
|
+
- Shared content contains shared document types, shared copy entries and Menus when configured.
|
|
11
|
+
- Assets contains Media, followed by a separator and shared singleton documents such as site settings.
|
|
12
|
+
|
|
13
|
+
Shared document types use `shared: true`; copy entries can also declare `shared: true`. A category name does not make content shared. Shared nonsingletons open a table, while an existing shared singleton opens its record directly. Website settings are content; browser preferences remain in the account menu.
|
|
14
|
+
|
|
15
|
+
## Configure the sidebar
|
|
16
|
+
|
|
17
|
+
Set `sidebar` in the [Astro integration](getting-started.md). This example assumes the named content types and singleton already exist:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
sidebar: [
|
|
21
|
+
{ kind: "heading", label: "Website" },
|
|
22
|
+
{ kind: "link", target: { kind: "pages" } },
|
|
23
|
+
{ kind: "link", target: { kind: "collection", name: "blog" }, icon: "newspaper" },
|
|
24
|
+
{ kind: "heading", label: "Shared content" },
|
|
25
|
+
{ kind: "link", target: { kind: "documents", type: "author" }, icon: "users" },
|
|
26
|
+
{ kind: "link", target: { kind: "documents", type: "logoSet" } },
|
|
27
|
+
{ kind: "link", target: { kind: "menus" } },
|
|
28
|
+
{ kind: "heading", label: "Assets" },
|
|
29
|
+
{ kind: "link", target: { kind: "media" } },
|
|
30
|
+
{ kind: "separator" },
|
|
31
|
+
{ kind: "link", target: { kind: "document", type: "settings", id: "settings:site" }, label: "Site settings", icon: "gear" },
|
|
32
|
+
]
|
|
33
|
+
```
|
|
34
|
+
|
|
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
|
+
|
|
37
|
+
Targets use stable identifiers rather than display labels:
|
|
38
|
+
|
|
39
|
+
| Target | Opens |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `{ kind: "pages" }` | The combined Pages table |
|
|
42
|
+
| `{ kind: "pages", category: "Legal" }` | Pages filtered to a named category |
|
|
43
|
+
| `{ kind: "page", slug: "home" }` | A specific copy entry |
|
|
44
|
+
| `{ kind: "collection", name: "blog" }` | A configured article collection |
|
|
45
|
+
| `{ kind: "documents", type: "author" }` | A native document type |
|
|
46
|
+
| `{ kind: "document", type: "settings", id: "settings:site" }` | A specific native document |
|
|
47
|
+
| `{ kind: "menus" }` | The menu list |
|
|
48
|
+
| `{ kind: "media" }` | The media library |
|
|
49
|
+
|
|
50
|
+
Duplicate targets are suppressed, and targets absent from the visible manifest are skipped. Omitted default links appear under More content, so new declarations remain reachable. An explicit Pages link already covers the category views, so Rigel does not append those category links again. Headings remain visible even if their links are unavailable.
|
|
51
|
+
|
|
52
|
+
This navigation is a view over content declarations, not a permission boundary. Hiding a link does not remove its data from the manifest or change backend write permissions. See [content model](content-model.md) and [GitHub](github.md).
|
|
53
|
+
|
|
54
|
+
## Pages and categories
|
|
55
|
+
|
|
56
|
+
Pages combines nonshared copy entries and native documents. It starts at All pages, not Core. When named categories exist, a dropdown offers All pages, Core and each named category. Core means content without a category; it is a virtual group, not a directory or schema type.
|
|
57
|
+
|
|
58
|
+
Set `category` on a native document definition or copy declaration to group its pages. Categories are separate from source folders and URL routes. They do not move files. Search within Pages matches titles, slugs and categories.
|
|
59
|
+
|
|
60
|
+
The Pages table includes a Path column derived from preview route configuration. It shows the website path rather than the content file's filesystem location. Configure routes in [preview](preview.md); a sidebar label or category does not create a public URL. Add new uses the available native document templates, with choices based on the declared schema.
|
|
61
|
+
|
|
62
|
+
## Table search and navigation
|
|
63
|
+
|
|
64
|
+
Pages and article collections have live search with a clear button. Collection search matches titles, effective slugs and declared description or tags fields. Each collection keeps its own query; it does not filter another collection or Pages.
|
|
65
|
+
|
|
66
|
+
Back returns to the original table with its search and category intact. Clicking Pages in the sidebar starts at All pages with an empty query. Clicking a collection in the sidebar clears that collection's query. These resets happen after any unsaved-change prompt is accepted; Keep editing preserves the current view and filters.
|
|
67
|
+
|
|
68
|
+
## Content languages and search
|
|
69
|
+
|
|
70
|
+
When [content locales](content-locales.md) are configured, the content-language switcher filters the sidebar, Pages and collections to that locale. Declarations without a locale remain visible across languages, which suits shared authors, menus and media. The editor's interface language is a separate preference.
|
|
71
|
+
|
|
72
|
+
Global search defaults to all content languages, even while the sidebar shows one. Configure the site default with `search: { scope: "all" }` or `search: { scope: "currentLocale" }`. Editors can override it in Settings under Content with the Search content languages preference, or choose Website default to follow configuration. Search covers content records, configured collections and menus rather than serving as a media filename search.
|
|
73
|
+
|
|
74
|
+
Changing a view does not save content or bypass unsaved-change protection. See [content locales](content-locales.md) for locale-specific declarations and selection behavior.
|
|
75
|
+
|
|
76
|
+
## Collapsed rail and icons
|
|
77
|
+
|
|
78
|
+
Collapse sidebar switches the desktop rail to icons and saves that browser preference. Mobile retains text labels. Collapsed links keep accessible names and hover titles, while group headings become dividers. Collapsing the rail leaves the preview and current draft in place.
|
|
79
|
+
|
|
80
|
+
Links have default icons based on their targets. Override `icon` with a bundled Phosphor name or a same-site root-relative SVG path:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
{ kind: "link", target: { kind: "collection", name: "blog" }, icon: "book-open" },
|
|
84
|
+
{ kind: "link", target: { kind: "documents", type: "product" }, icon: "/icons/products.svg" },
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Put the custom file at `public/icons/products.svg` when using Astro's default public directory. Paths must begin with a single `/`, end in `.svg`, and contain only letters, numbers, underscores, hyphens, dots and slashes. Rigel rejects remote and data URLs, traversal segments, encoded characters, queries and fragments. The SVG must exist at the configured public URL.
|
|
88
|
+
|
|
89
|
+
Icons use monochrome masks that inherit the interface text color rather than preserving multicolor artwork. Bundled icons are separate build assets, so the browser downloads those used by the navigation without an icon CDN.
|
|
90
|
+
|
|
91
|
+
The bundled names are `files`, `file-text`, `newspaper`, `megaphone`, `images`, `squares-four`, `list`, `layout`, `sidebar-simple`, `magnifying-glass`, `house`, `users`, `user`, `gear`, `globe`, `folder`, `folders`, `book-open`, `calendar`, `tag`, `heart`, `shopping-bag`, `chart-bar`, `pencil-simple` and `square-half`.
|
|
92
|
+
|
|
93
|
+
## Demo content
|
|
94
|
+
|
|
95
|
+
`demo` defaults to `true`. Set `rigel({ demo: false })` to exclude document types marked `demo: true` from discovery and the writable schema. Their sidebar and search entries disappear, but their files stay on disk. This option does not generate demo files or routes; those belong to the site.
|
|
96
|
+
|
|
97
|
+
See [authors](authors.md) for shared document setup, [media](media.md) for asset browsing, [extensions](extensions.md) for custom fields and blocks, and the [documentation index](README.md) for the other guides.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigel-cms/rigel",
|
|
3
|
-
"version": "1.4.0-rc.
|
|
3
|
+
"version": "1.4.0-rc.3",
|
|
4
4
|
"description": "A CMS for Astro sites. You edit the text, Git keeps the history.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -53,8 +53,7 @@
|
|
|
53
53
|
"src",
|
|
54
54
|
"!src/**/*.test.ts",
|
|
55
55
|
"README.md",
|
|
56
|
-
"docs
|
|
57
|
-
"docs/content-locales.md"
|
|
56
|
+
"docs"
|
|
58
57
|
],
|
|
59
58
|
"peerDependencies": {
|
|
60
59
|
"astro": "^7.3.0",
|
package/src/article-render.ts
CHANGED
|
@@ -80,9 +80,11 @@ function plain(node: ArticleNode): string {
|
|
|
80
80
|
|
|
81
81
|
export function articleMarkdown(body: ArticleBody, options: ArticleMarkdownOptions = {}): string {
|
|
82
82
|
validateBody(body);
|
|
83
|
-
const escaped = (text: string) => text.replace(/[\\`*_[\]<>#~]/g, "\\$&")
|
|
83
|
+
const escaped = (text: string) => text.replace(/[\\`*_[\]<>#~]/g, "\\$&")
|
|
84
|
+
.replace(/^([ \t]*)([-+=])(?=[ \t+=-]|$)/gm, "$1\\$2")
|
|
85
|
+
.replace(/^([ \t]*\d*)([.)])(?=[ \t]|$)/gm, "$1\\$2");
|
|
84
86
|
function walk(node: ArticleNode, depth = 0): string {
|
|
85
|
-
const content = () => (node.children ?? []).map(child => walk(child, depth)).join("");
|
|
87
|
+
const content = () => (node.children ?? []).map((child, index) => child.type === "linebreak" && index === node.children!.length - 1 ? "<br>" : walk(child, depth)).join("");
|
|
86
88
|
if (node.type === "extension") {
|
|
87
89
|
const markdown = options.extensionMarkdown?.(node.name as string, node.data as import("./extensions").BlockData);
|
|
88
90
|
if (markdown !== undefined) return markdown + "\n\n";
|
|
@@ -94,13 +96,18 @@ export function articleMarkdown(body: ArticleBody, options: ArticleMarkdownOptio
|
|
|
94
96
|
let text = escaped(node.text ?? "");
|
|
95
97
|
const format = Number(node.format);
|
|
96
98
|
if (format & (8 | 32 | 64 | 128)) return renderText(node);
|
|
99
|
+
const whitespace = !(format & 16) && (format & 7) ? /^(\s*)([\s\S]*?)(\s*)$/.exec(node.text ?? "") : null;
|
|
100
|
+
if (whitespace) {
|
|
101
|
+
if (!whitespace[2]) return text;
|
|
102
|
+
text = escaped(whitespace[2]);
|
|
103
|
+
}
|
|
97
104
|
if (format & 16) { const ticks = "`".repeat(Math.max(1, ...(node.text?.match(/`+/g) ?? []).map(run => run.length + 1))); text = ticks + " " + (node.text ?? "") + " " + ticks; }
|
|
98
105
|
if (format & 1) text = `**${text}**`;
|
|
99
106
|
if (format & 2) text = `*${text}*`;
|
|
100
107
|
if (format & 4) text = `~~${text}~~`;
|
|
101
|
-
return text;
|
|
108
|
+
return whitespace ? whitespace[1] + text + whitespace[3] : text;
|
|
102
109
|
}
|
|
103
|
-
if (node.type === "linebreak") return "
|
|
110
|
+
if (node.type === "linebreak") return "\\\n";
|
|
104
111
|
if (node.type === "paragraph") return content() + "\n\n";
|
|
105
112
|
if (node.type === "heading") return "#".repeat(Number(node.tag?.slice(1))) + " " + content() + "\n\n";
|
|
106
113
|
if (node.type === "quote") return content().trimEnd().split("\n").map(line => "> " + line).join("\n") + "\n\n";
|
|
@@ -108,7 +115,7 @@ export function articleMarkdown(body: ArticleBody, options: ArticleMarkdownOptio
|
|
|
108
115
|
if (node.type === "list") return (node.children ?? []).map((child, index) => " ".repeat(depth) + (node.listType === "number" ? `${(node.start ?? 1) + index}. ` : "- ") + walk(child, depth + 1).trimEnd() + "\n").join("") + "\n";
|
|
109
116
|
if (node.type === "table") {
|
|
110
117
|
const rows = node.children!;
|
|
111
|
-
const row = (cells: ArticleNode[]) => "| " + cells.map(cell => walk(cell).trim().replace(/\|/g, "\\|").replace(
|
|
118
|
+
const row = (cells: ArticleNode[]) => "| " + cells.map(cell => walk(cell).trim().replace(/\|/g, "\\|").replace(/\\\n|\n+/g, "<br>")).join(" | ") + " |\n";
|
|
112
119
|
const header = rows[0].children!.every(cell => Number(cell.headerState) & 1);
|
|
113
120
|
return (header ? row(rows[0].children!) : "| " + rows[0].children!.map(() => " ").join(" | ") + " |\n") + "| " + rows[0].children!.map(() => "---").join(" | ") + " |\n" + rows.slice(header ? 1 : 0).map(item => row(item.children!)).join("") + "\n";
|
|
114
121
|
}
|