@sandboxsonny/ace-kit 0.7.1 → 0.8.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/CHANGELOG.md +6 -0
- package/astro/AceSeo.astro +12 -0
- package/dist/cli.js +2 -2
- package/dist/{environment-TGMGSRJ3.js → environment-NGXBLWXM.js} +1 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +32 -1
- package/dist/{main-IAQ2IL3M.js → main-JOA27GX6.js} +6 -0
- package/docs/astro.md +1 -0
- package/docs/getting-started.md +1 -1
- package/package.json +7 -2
- package/templates/astro/package.json +3 -2
- package/templates/astro/src/ace-kit.generated.d.ts +4 -0
- package/templates/astro/src/layouts/BaseLayout.astro +15 -4
- package/templates/astro/src/pages/[...slug].astro +1 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @sandboxsonny/ace-kit
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7b85a62: Add `AceSeo` for Astro: one component for a page's title, description, canonical address, robots and sharing tags, built on `astro-seo` (with `siteName` ending the title) and filled from the entry's SEO title and description. Every content type in the CMS now has those two required fields, so the Astro starter depends on `astro-seo`, uses `AceSeo` in its layout, and fills them in on its starter pages. `seoProperties` is exported for any other framework.
|
|
8
|
+
|
|
3
9
|
## 0.7.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEO } from "astro-seo";
|
|
3
|
+
|
|
4
|
+
import { seoProperties } from "../dist/index.js";
|
|
5
|
+
import type { AceSeoOptions } from "../dist/index.js";
|
|
6
|
+
|
|
7
|
+
type Props = AceSeoOptions;
|
|
8
|
+
|
|
9
|
+
const properties = seoProperties(Astro.props);
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<SEO {...properties} />
|
package/dist/cli.js
CHANGED
|
@@ -9,8 +9,8 @@ if (major < 22 || major === 22 && minor < 12) {
|
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
11
|
try {
|
|
12
|
-
const { createRealEnvironment } = await import("./environment-
|
|
13
|
-
const { runCli } = await import("./main-
|
|
12
|
+
const { createRealEnvironment } = await import("./environment-NGXBLWXM.js");
|
|
13
|
+
const { runCli } = await import("./main-JOA27GX6.js");
|
|
14
14
|
const environment = await createRealEnvironment();
|
|
15
15
|
process.exit(await runCli(process.argv.slice(2), environment));
|
|
16
16
|
} catch (error) {
|
|
@@ -145,7 +145,7 @@ function createLastApiUrl(path) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// src/cli/version.ts
|
|
148
|
-
var aceKitVersion = true ? "0.
|
|
148
|
+
var aceKitVersion = true ? "0.8.0" : "development";
|
|
149
149
|
|
|
150
150
|
// src/cli/environment.ts
|
|
151
151
|
var runCommand = (command, commandArguments, input) => new Promise((resolve, reject) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -447,6 +447,54 @@ declare function refreshPageContent(): Promise<void>;
|
|
|
447
447
|
*/
|
|
448
448
|
declare function listenForPreviewRefresh(adminOrigin: string, refresh?: () => Promise<void>): () => void;
|
|
449
449
|
|
|
450
|
+
/** The two values every entry has, whatever its content type: what a search engine shows for it. */
|
|
451
|
+
declare const seoTitleHandle = "seo-title";
|
|
452
|
+
declare const seoDescriptionHandle = "seo-description";
|
|
453
|
+
type AceSeoOptions = {
|
|
454
|
+
/** The entry the page shows. Its SEO title and description are used, falling back to its title and description. */
|
|
455
|
+
entry?: Pick<AceEntry, "title" | "description" | "values">;
|
|
456
|
+
/** Used when there is no entry, or the entry has no title of its own. */
|
|
457
|
+
title?: string;
|
|
458
|
+
/** Used when there is no entry, or the entry has no description of its own. */
|
|
459
|
+
description?: string;
|
|
460
|
+
/** The site's name: it ends the page's `<title>` ("About us | Acme") and is the shared link's `og:site_name`. */
|
|
461
|
+
siteName?: string;
|
|
462
|
+
/** The page's address, if it isn't the one requested. Left out, the requested address is used. */
|
|
463
|
+
canonical?: string;
|
|
464
|
+
/** A picture to show when the page is shared: a full web address. */
|
|
465
|
+
image?: string;
|
|
466
|
+
/** Tells search engines to leave the page out, as for a draft shown in preview. */
|
|
467
|
+
noindex?: boolean;
|
|
468
|
+
};
|
|
469
|
+
type Meta = {
|
|
470
|
+
property?: string;
|
|
471
|
+
name?: string;
|
|
472
|
+
content: string;
|
|
473
|
+
};
|
|
474
|
+
/** What `astro-seo`'s `<SEO>` takes (only the parts used here). */
|
|
475
|
+
type AceSeoProperties = {
|
|
476
|
+
title?: string;
|
|
477
|
+
description?: string;
|
|
478
|
+
canonical?: string;
|
|
479
|
+
noindex: boolean;
|
|
480
|
+
twitter: {
|
|
481
|
+
card: "summary" | "summary_large_image";
|
|
482
|
+
title?: string;
|
|
483
|
+
description?: string;
|
|
484
|
+
image?: string;
|
|
485
|
+
};
|
|
486
|
+
extend: {
|
|
487
|
+
meta: Meta[];
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
/**
|
|
491
|
+
* Works out what a page's search and sharing tags say. An entry's SEO title and description win; if the editor left
|
|
492
|
+
* them out (an entry saved before they were required, say) its own title and description are used, then the
|
|
493
|
+
* options'. The site's name ends the `<title>` (not the sharing titles), unless the title already is the name.
|
|
494
|
+
* Open Graph is written as plain meta tags, so a page with no picture needs no placeholder.
|
|
495
|
+
*/
|
|
496
|
+
declare function seoProperties(options: AceSeoOptions): AceSeoProperties;
|
|
497
|
+
|
|
450
498
|
/**
|
|
451
499
|
* A text area field's text as HTML. With `multiline` (the default) blank lines start new paragraphs (`<p>`) and a
|
|
452
500
|
* single new line is a line break; without it the text is one line, with no element around it. Every piece of text
|
|
@@ -488,4 +536,4 @@ type AceSection = {
|
|
|
488
536
|
blocks: AceSectionBlock[];
|
|
489
537
|
};
|
|
490
538
|
|
|
491
|
-
export { type AceAddress, type AceClient, type AceClientOptions, type AceContentTypeHandle, type AceContentTypeSchema, type AceContentTypes, type AceDateFormat, type AceEntry, type AceFieldSchema, type AceImageAttributes, type AceImageData, type AceImageOptions, type AceLinkAttributes, type AceLinkData, type AceMenu, type AceMenuItem, type AceNavigationLink, type AcePage, AcePreviewExpiredError, type AceRichTextDocument, type AceRichTextMark, type AceRichTextNode, type AceSection, type AceSectionBlock, AceUnavailableError, type AceValuesOf, type BlockDefinition, type SectionDefinition, type SectionFieldDefinition, type SectionFieldType, type SectionOf, type SectionTypesPayload, type ValuesOfFields, addFramingPolicy, addressFromPath, checkSectionDefinition, createAceClient, defineSection, formatAceDate, framingPolicy, headingLevel, homeAddress, imageAttributes, isCurrentPage, isHomeAddress, isInternalPath, isRefreshRequest, linkAddress, linkAttributes, linksFromMenu, listStart, listenForPreviewRefresh, loadMenuLinks, loadPage, originOf, pageNotices, pathOfAddress, previewHeaders, previewParameter, previewTokenFrom, refreshMessageType, refreshPageContent, renderRichText, renderTextArea, richTextImageSizes, sectionTypesPayload, swapPageContent, visitablePath };
|
|
539
|
+
export { type AceAddress, type AceClient, type AceClientOptions, type AceContentTypeHandle, type AceContentTypeSchema, type AceContentTypes, type AceDateFormat, type AceEntry, type AceFieldSchema, type AceImageAttributes, type AceImageData, type AceImageOptions, type AceLinkAttributes, type AceLinkData, type AceMenu, type AceMenuItem, type AceNavigationLink, type AcePage, AcePreviewExpiredError, type AceRichTextDocument, type AceRichTextMark, type AceRichTextNode, type AceSection, type AceSectionBlock, type AceSeoOptions, type AceSeoProperties, AceUnavailableError, type AceValuesOf, type BlockDefinition, type SectionDefinition, type SectionFieldDefinition, type SectionFieldType, type SectionOf, type SectionTypesPayload, type ValuesOfFields, addFramingPolicy, addressFromPath, checkSectionDefinition, createAceClient, defineSection, formatAceDate, framingPolicy, headingLevel, homeAddress, imageAttributes, isCurrentPage, isHomeAddress, isInternalPath, isRefreshRequest, linkAddress, linkAttributes, linksFromMenu, listStart, listenForPreviewRefresh, loadMenuLinks, loadPage, originOf, pageNotices, pathOfAddress, previewHeaders, previewParameter, previewTokenFrom, refreshMessageType, refreshPageContent, renderRichText, renderTextArea, richTextImageSizes, sectionTypesPayload, seoDescriptionHandle, seoProperties, seoTitleHandle, swapPageContent, visitablePath };
|
package/dist/index.js
CHANGED
|
@@ -133,4 +133,35 @@ function formatAceDate(date, format = {}) {
|
|
|
133
133
|
}).format(moment);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
// src/helpers/seo.ts
|
|
137
|
+
var seoTitleHandle = "seo-title";
|
|
138
|
+
var seoDescriptionHandle = "seo-description";
|
|
139
|
+
var textValue = (value) => typeof value === "string" ? value.trim() : "";
|
|
140
|
+
function seoProperties(options) {
|
|
141
|
+
const { entry } = options;
|
|
142
|
+
const title = textValue(entry?.values?.[seoTitleHandle]) || textValue(entry?.title) || textValue(options.title);
|
|
143
|
+
const description = textValue(entry?.values?.[seoDescriptionHandle]) || textValue(entry?.description) || textValue(options.description);
|
|
144
|
+
const meta = [{ property: "og:type", content: "website" }];
|
|
145
|
+
if (title) meta.push({ property: "og:title", content: title });
|
|
146
|
+
if (description) meta.push({ property: "og:description", content: description });
|
|
147
|
+
if (options.siteName) meta.push({ property: "og:site_name", content: options.siteName });
|
|
148
|
+
if (options.canonical) meta.push({ property: "og:url", content: options.canonical });
|
|
149
|
+
if (options.image) meta.push({ property: "og:image", content: options.image });
|
|
150
|
+
const siteName = options.siteName?.trim();
|
|
151
|
+
const pageTitle = title && siteName && title !== siteName ? `${title} | ${siteName}` : title;
|
|
152
|
+
return {
|
|
153
|
+
...pageTitle && { title: pageTitle },
|
|
154
|
+
...description && { description },
|
|
155
|
+
...options.canonical && { canonical: options.canonical },
|
|
156
|
+
noindex: options.noindex === true,
|
|
157
|
+
twitter: {
|
|
158
|
+
card: options.image ? "summary_large_image" : "summary",
|
|
159
|
+
...title && { title },
|
|
160
|
+
...description && { description },
|
|
161
|
+
...options.image && { image: options.image }
|
|
162
|
+
},
|
|
163
|
+
extend: { meta }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { checkSectionDefinition, defineSection, formatAceDate, sectionTypesPayload, seoDescriptionHandle, seoProperties, seoTitleHandle };
|
|
@@ -197,6 +197,8 @@ function starterEntries(framework, siteName) {
|
|
|
197
197
|
title: "Home",
|
|
198
198
|
description: `Welcome to ${siteName}.`,
|
|
199
199
|
values: {
|
|
200
|
+
"seo-title": `${siteName} | Welcome`,
|
|
201
|
+
"seo-description": `Welcome to ${siteName}.`,
|
|
200
202
|
headline: `Welcome to ${siteName}`,
|
|
201
203
|
body: richText(
|
|
202
204
|
"Edit this text in the CMS and reload the page to see it change.\n\nPages are rendered on each request, so there is nothing to rebuild."
|
|
@@ -254,6 +256,8 @@ function starterEntries(framework, siteName) {
|
|
|
254
256
|
title: "About",
|
|
255
257
|
description: `About ${siteName}.`,
|
|
256
258
|
values: {
|
|
259
|
+
"seo-title": `About | ${siteName}`,
|
|
260
|
+
"seo-description": `About ${siteName}.`,
|
|
257
261
|
headline: "About",
|
|
258
262
|
body: richText(
|
|
259
263
|
"Say who you are and what you do.\n\nThis text is an entry in the CMS: editing it there changes this page."
|
|
@@ -271,6 +275,8 @@ function starterEntries(framework, siteName) {
|
|
|
271
275
|
title: "Contact",
|
|
272
276
|
description: `How to reach ${siteName}.`,
|
|
273
277
|
values: {
|
|
278
|
+
"seo-title": `Contact | ${siteName}`,
|
|
279
|
+
"seo-description": `How to reach ${siteName}.`,
|
|
274
280
|
headline: "Contact",
|
|
275
281
|
body: richText("Tell people how to get in touch."),
|
|
276
282
|
// A link to another entry of the site: it needs About's ID, which exists by now.
|
package/docs/astro.md
CHANGED
|
@@ -92,6 +92,7 @@ import AcePreviewRefresh from "@sandboxsonny/ace-kit/astro/AcePreviewRefresh.ast
|
|
|
92
92
|
- **AceLink** draws a link field as an `<a>` with the editor's words (or the address when there are none; put your own words in the slot to replace them). Nothing is drawn if the address can't be trusted, so check for a missing link yourself if you need a fallback. A date field is plain text: `formatAceDate(entry.values.published, { locale: "en-GB" })` gives "13 October 2026", and `<time datetime={entry.values.published}>` takes the raw value.
|
|
93
93
|
- **AceRichText** draws a rich text field as HTML. It outputs bare elements (`h2`, `p`, `ul`, `a`...); style them from a wrapping element. Astro scopes styles, so reach them with `:global`, e.g. `.rich-text :global(h2) { ... }`.
|
|
94
94
|
- **AceTextArea** draws a Textarea field (plain text): `multiline` (the default) makes paragraphs of blocks and a `<br>` of a new line; `multiline={false}` is one line of text with no element around it.
|
|
95
|
+
- **AceSeo** goes in your layout's `<head>`: the page's `<title>`, description, canonical address, robots and Open Graph / Twitter tags, built on [astro-seo](https://github.com/jonasmerlin/astro-seo) (a site made by `ace-kit create` already depends on it; add `astro-seo` to a site you made by hand). Give it the entry and it uses the entry's SEO title and description (every content type has those two required fields), falling back to the entry's title and description, then to its own `title` and `description` props. Other props: `siteName` (ends the `<title>`, as "About us | Acme", and is the shared link's site name; leave it out if editors write the whole title), `canonical`, `image` (a full web address, for sharing) and `noindex` (set it for a draft shown in preview). On another framework, `seoProperties(options)` gives the same answer as plain data.
|
|
95
96
|
- **AcePreviewRefresh** goes once in your layout. It does nothing unless the page was opened by the CMS's editor.
|
|
96
97
|
- **AceNotice** is the message for a page that isn't an entry.
|
|
97
98
|
|
package/docs/getting-started.md
CHANGED
|
@@ -25,7 +25,7 @@ npm install @sandboxsonny/ace-kit
|
|
|
25
25
|
| `@sandboxsonny/ace-kit` | The framework-free core: the client, errors, `loadPage`, addressing, image attributes, menu helpers, rich text to HTML, preview helpers, all the types |
|
|
26
26
|
| `@sandboxsonny/ace-kit/astro/integration` | The Astro integration: add it to `astro.config.mjs` and an Astro site is wired up |
|
|
27
27
|
| `@sandboxsonny/ace-kit/astro` | What an Astro page calls: `getPage`, `getEntry`, `getEntries`, `getMenuLinks`, `getMenu`, `ace` (the client) |
|
|
28
|
-
| `@sandboxsonny/ace-kit/astro/AceImage.astro` and friends | Astro components: `AceImage`, `AceRichText`, `AceMenu`, `AceNotice`, `AcePreviewRefresh`
|
|
28
|
+
| `@sandboxsonny/ace-kit/astro/AceImage.astro` and friends | Astro components: `AceImage`, `AceRichText`, `AceSeo`, `AceMenu`, `AceNotice`, `AcePreviewRefresh` |
|
|
29
29
|
| `@sandboxsonny/ace-kit/react` | `AceImage`, `AceRichText`, `usePreviewRefresh` |
|
|
30
30
|
| `@sandboxsonny/ace-kit/react-router` | React Router 7, browser-safe: `AceRichText` (router links), `AceImage`, `AceNotice`, `createMeta`, `useAceMenu`, `useAcePreview` |
|
|
31
31
|
| `@sandboxsonny/ace-kit/react-router/server` | React Router 7, server only: `createPageLoader`, `createRootLoader`, `handleRequest` (the server entry), `getEntry`, `getEntries`, `getMenuLinks`, `getAceClient` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandboxsonny/ace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Everything a website needs to use CMS Ace: a typed client, addressing, images, menus, rich text, live preview, and Astro and React adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
@@ -105,7 +105,8 @@
|
|
|
105
105
|
"react-router": ">=7",
|
|
106
106
|
"react-dom": ">=18",
|
|
107
107
|
"@react-router/node": ">=7",
|
|
108
|
-
"vite": ">=5"
|
|
108
|
+
"vite": ">=5",
|
|
109
|
+
"astro-seo": ">=1"
|
|
109
110
|
},
|
|
110
111
|
"peerDependenciesMeta": {
|
|
111
112
|
"astro": {
|
|
@@ -125,6 +126,9 @@
|
|
|
125
126
|
},
|
|
126
127
|
"vite": {
|
|
127
128
|
"optional": true
|
|
129
|
+
},
|
|
130
|
+
"astro-seo": {
|
|
131
|
+
"optional": true
|
|
128
132
|
}
|
|
129
133
|
},
|
|
130
134
|
"devDependencies": {
|
|
@@ -139,6 +143,7 @@
|
|
|
139
143
|
"@types/react": "^19.3.0",
|
|
140
144
|
"@types/react-dom": "^19.3.0",
|
|
141
145
|
"astro": "^7.3.8",
|
|
146
|
+
"astro-seo": "^1.2.0",
|
|
142
147
|
"eslint": "^10.12.0",
|
|
143
148
|
"eslint-config-prettier": "^10.1.8",
|
|
144
149
|
"eslint-plugin-astro": "^1.7.0",
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@astrojs/node": "^11.1.7",
|
|
24
|
-
"@sandboxsonny/ace-kit": "
|
|
25
|
-
"astro": "^7.3.8"
|
|
24
|
+
"@sandboxsonny/ace-kit": "file:../ace-kit/sandboxsonny-ace-kit-0.7.0.tgz",
|
|
25
|
+
"astro": "^7.3.8",
|
|
26
|
+
"astro-seo": "^1.2.0"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@astrojs/check": "^0.9.10",
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
import AcePreviewRefresh from "@sandboxsonny/ace-kit/astro/AcePreviewRefresh.astro";
|
|
3
|
+
import AceSeo from "@sandboxsonny/ace-kit/astro/AceSeo.astro";
|
|
4
|
+
import type { AceEntry } from "@sandboxsonny/ace-kit";
|
|
3
5
|
import SiteFooter from "~/components/SiteFooter.astro";
|
|
4
6
|
import SiteHeader from "~/components/SiteHeader.astro";
|
|
5
7
|
import "~/styles/global.css";
|
|
6
8
|
|
|
7
9
|
type Props = {
|
|
10
|
+
/** The entry the page shows: its SEO title and description fill the page's search and sharing tags. */
|
|
11
|
+
entry?: AceEntry;
|
|
12
|
+
/** Used when there is no entry (a notice page), and when an entry has no title of its own. */
|
|
8
13
|
title: string;
|
|
9
14
|
description?: string;
|
|
10
15
|
/** True when the page is showing a draft inside the CMS's preview frame. */
|
|
11
16
|
preview?: boolean;
|
|
12
17
|
};
|
|
13
18
|
|
|
14
|
-
const { title, description = "A simple example site for testing CMS Ace.", preview = false } = Astro.props;
|
|
19
|
+
const { entry, title, description = "A simple example site for testing CMS Ace.", preview = false } = Astro.props;
|
|
15
20
|
---
|
|
16
21
|
|
|
17
22
|
<!doctype html>
|
|
@@ -19,11 +24,17 @@ const { title, description = "A simple example site for testing CMS Ace.", previ
|
|
|
19
24
|
<head>
|
|
20
25
|
<meta charset="utf-8" />
|
|
21
26
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
-
<meta name="description" content={description} />
|
|
23
27
|
<meta name="generator" content={Astro.generator} />
|
|
24
|
-
{preview && <meta name="robots" content="noindex" />}
|
|
25
28
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
26
|
-
|
|
29
|
+
{/* Title, description, canonical address, robots and sharing tags. A draft shown in preview is kept out of search. */}
|
|
30
|
+
<AceSeo
|
|
31
|
+
entry={entry}
|
|
32
|
+
{title}
|
|
33
|
+
{description}
|
|
34
|
+
siteName="Example Site"
|
|
35
|
+
|
|
36
|
+
noindex={preview}
|
|
37
|
+
/>
|
|
27
38
|
</head>
|
|
28
39
|
<body>
|
|
29
40
|
<SiteHeader />
|
|
@@ -15,11 +15,7 @@ const isPreview = Boolean(page.headers["Cache-Control"]);
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
{page.kind === "entry" ? (
|
|
18
|
-
<BaseLayout
|
|
19
|
-
title={`${page.entry.title} | Example Site`}
|
|
20
|
-
description={page.entry.description || undefined}
|
|
21
|
-
preview={isPreview}
|
|
22
|
-
>
|
|
18
|
+
<BaseLayout entry={page.entry} title={page.entry.title} preview={isPreview}>
|
|
23
19
|
<PageContent entry={page.entry} />
|
|
24
20
|
</BaseLayout>
|
|
25
21
|
) : (
|