@sandboxsonny/ace-kit 0.7.0 → 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 +12 -0
- package/README.md +1 -1
- package/astro/AceSeo.astro +12 -0
- package/dist/astro-integration.js +1 -1
- package/dist/{chunk-T7DKI2U3.js → chunk-2W2NWDKV.js} +8 -6
- package/dist/cli.js +2 -2
- package/dist/{environment-J4VYOHDK.js → environment-NGXBLWXM.js} +1 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +32 -1
- package/dist/{main-AGLPUGNN.js → main-JOA27GX6.js} +37 -44
- package/dist/vite.js +1 -1
- package/docs/astro.md +7 -8
- package/docs/command-line.md +2 -5
- package/docs/dev-sites.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/react-router.md +4 -6
- package/docs/typed-content.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,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.7.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 98f4d58: The command line no longer shows where CMS Ace lives. `--help`, the messages and a new site's `.env.example` talk about CMS Ace, not an address, and connect to it without being told. `--api-url` and `ACE_API_URL` still work for a CMS on your own machine; they are just not advertised (see CONTRIBUTING.md).
|
|
14
|
+
|
|
3
15
|
## 0.7.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ const about = await getEntry("pages", "about");
|
|
|
58
58
|
| [API reference](docs/api-reference.md) | Every export |
|
|
59
59
|
| [Releasing](docs/releasing.md) | Versions, the changelog and publishing |
|
|
60
60
|
|
|
61
|
-
The same pages, with live examples of the React components, are in Storybook: `npm run storybook
|
|
61
|
+
The same pages, with live examples of the React components, are in Storybook: **https://sandboxsonny.github.io/ace-kit/** (or `npm run storybook` to run it yourself).
|
|
62
62
|
|
|
63
63
|
## Status
|
|
64
64
|
|
|
@@ -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} />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { startDevSite, sectionSync } from './chunk-
|
|
1
|
+
import { startDevSite, sectionSync } from './chunk-2W2NWDKV.js';
|
|
2
2
|
import { domainFromSite, hostedAdminOrigin, hostedApiUrl } from './chunk-N5DIMG3X.js';
|
|
3
3
|
import { envField } from 'astro/config';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
@@ -140,6 +140,13 @@ async function chooseLoginStore(options) {
|
|
|
140
140
|
return file;
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
// src/cli/api-url.ts
|
|
145
|
+
var cmsLabel = (apiUrl) => apiUrl === hostedApiUrl ? "CMS Ace" : apiUrl;
|
|
146
|
+
var loginCommandFor = (apiUrl) => apiUrl === hostedApiUrl ? "npx ace-kit login" : `npx ace-kit login --api-url ${apiUrl}`;
|
|
147
|
+
function isLocalApiUrl(apiUrl) {
|
|
148
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(apiUrl).hostname);
|
|
149
|
+
}
|
|
143
150
|
var runCommand = (command, commandArguments, input) => new Promise((resolve, reject) => {
|
|
144
151
|
const child = spawn(command, commandArguments, { stdio: ["pipe", "pipe", "ignore"] });
|
|
145
152
|
let stdout = "";
|
|
@@ -236,11 +243,6 @@ async function resolveLogin(options) {
|
|
|
236
243
|
return { token: stored.token, email: stored.email, source: options.store.description };
|
|
237
244
|
}
|
|
238
245
|
|
|
239
|
-
// src/cli/api-url.ts
|
|
240
|
-
function isLocalApiUrl(apiUrl) {
|
|
241
|
-
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(apiUrl).hostname);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
246
|
// src/dev-site/prepare-dev-site.ts
|
|
245
247
|
var off = ["false", "off", "no", "0"];
|
|
246
248
|
var on = ["true", "on", "yes", "1"];
|
|
@@ -305,7 +307,7 @@ async function startDevSite(options) {
|
|
|
305
307
|
return null;
|
|
306
308
|
case "waiting-for-login":
|
|
307
309
|
log.warn(
|
|
308
|
-
`You are not signed in to ${outcome.apiUrl}, so this is showing the live site and section sync is off. Run:
|
|
310
|
+
`You are not signed in to ${cmsLabel(outcome.apiUrl)}, so this is showing the live site and section sync is off. Run: ${loginCommandFor(outcome.apiUrl)}`
|
|
309
311
|
);
|
|
310
312
|
return null;
|
|
311
313
|
case "problem":
|
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 };
|
|
@@ -126,12 +126,6 @@ async function createSite(options) {
|
|
|
126
126
|
text = replaceOnce(file, text, /site: "https?:\/\/[^"]+"/, `site: "https://${domain}"`);
|
|
127
127
|
if (text !== original) await writeFile(path, text);
|
|
128
128
|
}
|
|
129
|
-
const settings = [
|
|
130
|
-
"# Where CMS Ace runs. Leave these out for the hosted CMS Ace; they are for a CMS on your own machine.",
|
|
131
|
-
"# ACE_API_URL=http://localhost:8080",
|
|
132
|
-
"# ACE_PREVIEW_ADMIN_ORIGIN=http://localhost:5173",
|
|
133
|
-
""
|
|
134
|
-
];
|
|
135
129
|
const extraSettings = Object.entries(options.environmentSettings ?? {}).map(([name2, value]) => `${name2}=${value}`);
|
|
136
130
|
if (options.framework === "astro") {
|
|
137
131
|
await writeNew(
|
|
@@ -140,8 +134,7 @@ async function createSite(options) {
|
|
|
140
134
|
[
|
|
141
135
|
"# The site's domain in CMS Ace is the `site` setting in astro.config.mjs. Set this only to override it.",
|
|
142
136
|
"# ACE_SITE_DOMAIN=" + domain,
|
|
143
|
-
""
|
|
144
|
-
...settings
|
|
137
|
+
""
|
|
145
138
|
].join("\n")
|
|
146
139
|
);
|
|
147
140
|
if (extraSettings.length > 0) await writeNew(folder, ".env", `${extraSettings.join("\n")}
|
|
@@ -149,7 +142,7 @@ async function createSite(options) {
|
|
|
149
142
|
} else {
|
|
150
143
|
const domainLine = `ACE_SITE_DOMAIN=${domain}
|
|
151
144
|
`;
|
|
152
|
-
await writeNew(folder, ".env.example", [`# The site's domain in CMS Ace.`, domainLine
|
|
145
|
+
await writeNew(folder, ".env.example", [`# The site's domain in CMS Ace.`, domainLine].join("\n"));
|
|
153
146
|
await writeNew(folder, ".env", [domainLine.trim(), ...extraSettings].join("\n") + "\n");
|
|
154
147
|
}
|
|
155
148
|
await writeNew(folder, "README.md", readme({ name, domain, framework: options.framework }));
|
|
@@ -204,6 +197,8 @@ function starterEntries(framework, siteName) {
|
|
|
204
197
|
title: "Home",
|
|
205
198
|
description: `Welcome to ${siteName}.`,
|
|
206
199
|
values: {
|
|
200
|
+
"seo-title": `${siteName} | Welcome`,
|
|
201
|
+
"seo-description": `Welcome to ${siteName}.`,
|
|
207
202
|
headline: `Welcome to ${siteName}`,
|
|
208
203
|
body: richText(
|
|
209
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."
|
|
@@ -261,6 +256,8 @@ function starterEntries(framework, siteName) {
|
|
|
261
256
|
title: "About",
|
|
262
257
|
description: `About ${siteName}.`,
|
|
263
258
|
values: {
|
|
259
|
+
"seo-title": `About | ${siteName}`,
|
|
260
|
+
"seo-description": `About ${siteName}.`,
|
|
264
261
|
headline: "About",
|
|
265
262
|
body: richText(
|
|
266
263
|
"Say who you are and what you do.\n\nThis text is an entry in the CMS: editing it there changes this page."
|
|
@@ -278,6 +275,8 @@ function starterEntries(framework, siteName) {
|
|
|
278
275
|
title: "Contact",
|
|
279
276
|
description: `How to reach ${siteName}.`,
|
|
280
277
|
values: {
|
|
278
|
+
"seo-title": `Contact | ${siteName}`,
|
|
279
|
+
"seo-description": `How to reach ${siteName}.`,
|
|
281
280
|
headline: "Contact",
|
|
282
281
|
body: richText("Tell people how to get in touch."),
|
|
283
282
|
// A link to another entry of the site: it needs About's ID, which exists by now.
|
|
@@ -396,6 +395,9 @@ async function resolveApiUrl(fromOption, environment) {
|
|
|
396
395
|
return null;
|
|
397
396
|
}
|
|
398
397
|
}
|
|
398
|
+
var cmsName = (apiUrl) => apiUrl === hostedApiUrl ? "CMS Ace" : `the CMS at ${apiUrl}`;
|
|
399
|
+
var cmsLabel = (apiUrl) => apiUrl === hostedApiUrl ? "CMS Ace" : apiUrl;
|
|
400
|
+
var loginCommandFor = (apiUrl) => apiUrl === hostedApiUrl ? "npx ace-kit login" : `npx ace-kit login --api-url ${apiUrl}`;
|
|
399
401
|
function isLocalApiUrl(apiUrl) {
|
|
400
402
|
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(apiUrl).hostname);
|
|
401
403
|
}
|
|
@@ -525,13 +527,12 @@ async function waitForApproval(dependencies) {
|
|
|
525
527
|
var loginUsage = `ace-kit login: sign in to CMS Ace from the command line.
|
|
526
528
|
|
|
527
529
|
Usage:
|
|
528
|
-
ace-kit login
|
|
529
|
-
ace-kit logout
|
|
530
|
-
ace-kit whoami
|
|
530
|
+
ace-kit login Sign in. Opens the admin app, where you type the code shown here.
|
|
531
|
+
ace-kit logout Sign out, and forget the login kept on this machine.
|
|
532
|
+
ace-kit whoami Say who you are signed in as.
|
|
531
533
|
|
|
532
534
|
Options:
|
|
533
|
-
--
|
|
534
|
-
--help Show this message.
|
|
535
|
+
--help Show this message.
|
|
535
536
|
|
|
536
537
|
The login is kept in the macOS Keychain, or in a file only you can read on other systems. On a build server,
|
|
537
538
|
where nobody can approve a login in a browser, set ACE_TOKEN instead.
|
|
@@ -565,7 +566,7 @@ async function loginCommand(commandArguments, environment) {
|
|
|
565
566
|
const { apiUrl } = read;
|
|
566
567
|
if (apiUrl.startsWith("http://") && !isLocalApiUrl(apiUrl)) {
|
|
567
568
|
environment.error(
|
|
568
|
-
`Warning: ${apiUrl} is not encrypted (http://), so the login could be read on the way. Use https:// if you can.`
|
|
569
|
+
`Warning: ${cmsName(apiUrl)} is not encrypted (http://), so the login could be read on the way. Use https:// if you can.`
|
|
569
570
|
);
|
|
570
571
|
}
|
|
571
572
|
try {
|
|
@@ -589,7 +590,7 @@ async function signIn(apiUrl, environment) {
|
|
|
589
590
|
"unsupported",
|
|
590
591
|
"The CMS sent an approval address that isn't a web address, so it was not opened."
|
|
591
592
|
);
|
|
592
|
-
environment.out(`Signing in to ${apiUrl}
|
|
593
|
+
environment.out(`Signing in to ${cmsLabel(apiUrl)}
|
|
593
594
|
`);
|
|
594
595
|
environment.out(` 1. Go to ${page}`);
|
|
595
596
|
environment.out(` 2. Type ${request.userCode}
|
|
@@ -619,7 +620,7 @@ async function logoutCommand(commandArguments, environment) {
|
|
|
619
620
|
const { apiUrl } = read;
|
|
620
621
|
const stored = await environment.store.read(apiUrl);
|
|
621
622
|
if (!stored) {
|
|
622
|
-
environment.out(`Not signed in to ${apiUrl}.`);
|
|
623
|
+
environment.out(`Not signed in to ${cmsLabel(apiUrl)}.`);
|
|
623
624
|
return 0;
|
|
624
625
|
}
|
|
625
626
|
const ended = await environment.fetchImplementation(`${apiUrl}/auth/logout`, {
|
|
@@ -632,7 +633,7 @@ async function logoutCommand(commandArguments, environment) {
|
|
|
632
633
|
);
|
|
633
634
|
await environment.store.remove(apiUrl);
|
|
634
635
|
environment.out(
|
|
635
|
-
ended ? `Signed out of ${apiUrl}.` : `Forgot the login for ${apiUrl} on this machine, but could not reach the CMS to end it there. It ends by itself after 30 days unused; a super admin can also end it from the Sessions screen.`
|
|
636
|
+
ended ? `Signed out of ${cmsLabel(apiUrl)}.` : `Forgot the login for ${cmsLabel(apiUrl)} on this machine, but could not reach the CMS to end it there. It ends by itself after 30 days unused; a super admin can also end it from the Sessions screen.`
|
|
636
637
|
);
|
|
637
638
|
return 0;
|
|
638
639
|
}
|
|
@@ -651,7 +652,7 @@ async function whoamiCommand(commandArguments, environment) {
|
|
|
651
652
|
now: environment.now
|
|
652
653
|
});
|
|
653
654
|
if (!login) {
|
|
654
|
-
environment.out(`Not signed in to ${apiUrl}. Run:
|
|
655
|
+
environment.out(`Not signed in to ${cmsLabel(apiUrl)}. Run: ${loginCommandFor(apiUrl).replace("npx ", "")}`);
|
|
655
656
|
return 1;
|
|
656
657
|
}
|
|
657
658
|
try {
|
|
@@ -668,10 +669,10 @@ async function whoamiCommand(commandArguments, environment) {
|
|
|
668
669
|
return 1;
|
|
669
670
|
}
|
|
670
671
|
const me = await response.json();
|
|
671
|
-
environment.out(`Signed in to ${apiUrl} as ${String(me.email)}, using the login in ${login.source}.`);
|
|
672
|
+
environment.out(`Signed in to ${cmsLabel(apiUrl)} as ${String(me.email)}, using the login in ${login.source}.`);
|
|
672
673
|
return 0;
|
|
673
674
|
} catch {
|
|
674
|
-
environment.error(`Could not reach
|
|
675
|
+
environment.error(`Could not reach ${cmsName(apiUrl)}.`);
|
|
675
676
|
return 1;
|
|
676
677
|
}
|
|
677
678
|
}
|
|
@@ -680,7 +681,7 @@ async function whoamiCommand(commandArguments, environment) {
|
|
|
680
681
|
var createUsage = `ace-kit create: make a new site that uses CMS Ace.
|
|
681
682
|
|
|
682
683
|
Usage:
|
|
683
|
-
ace-kit create <folder> <domain> [--framework astro|react-router] [--name <name>]
|
|
684
|
+
ace-kit create <folder> <domain> [--framework astro|react-router] [--name <name>]
|
|
684
685
|
|
|
685
686
|
It signs you in if you aren't already (your browser opens to approve it), makes the site in CMS Ace with starter
|
|
686
687
|
content (a Pages type, Home, About and Contact, a main menu), and copies a starter site into the folder with your
|
|
@@ -691,7 +692,6 @@ Options:
|
|
|
691
692
|
<domain> The domain the site has in CMS Ace, e.g. example.com. (--site <domain> means the same.)
|
|
692
693
|
--framework astro (the default) or react-router.
|
|
693
694
|
--name The name shown to visitors (default: made from the folder's name, so my-site is "My Site").
|
|
694
|
-
--api-url Where the CMS Ace API lives (or set ACE_API_URL). Only for a CMS on your own machine.
|
|
695
695
|
--help Show this message.
|
|
696
696
|
`;
|
|
697
697
|
async function createCommand(commandArguments, environment) {
|
|
@@ -742,7 +742,7 @@ ${createUsage}`);
|
|
|
742
742
|
}
|
|
743
743
|
if (apiUrl.startsWith("http://") && !isLocalApiUrl(apiUrl)) {
|
|
744
744
|
environment.error(
|
|
745
|
-
`Warning: ${apiUrl} is not encrypted (http://), so your login could be read on the way. Use https:// if you can.`
|
|
745
|
+
`Warning: ${cmsName(apiUrl)} is not encrypted (http://), so your login could be read on the way. Use https:// if you can.`
|
|
746
746
|
);
|
|
747
747
|
}
|
|
748
748
|
let login = await resolveLogin({
|
|
@@ -797,7 +797,7 @@ Nothing was made.`);
|
|
|
797
797
|
switch (seeded.kind) {
|
|
798
798
|
case "seeded":
|
|
799
799
|
environment.out(
|
|
800
|
-
`Made ${created.domain} in CMS Ace (${apiUrl}) with a Pages type, ${seeded.entries} entries and a main menu.`
|
|
800
|
+
`Made ${created.domain} in ${apiUrl === hostedApiUrl ? "CMS Ace" : `CMS Ace (${apiUrl})`} with a Pages type, ${seeded.entries} entries and a main menu.`
|
|
801
801
|
);
|
|
802
802
|
break;
|
|
803
803
|
case "exists":
|
|
@@ -1014,7 +1014,7 @@ async function loadSchemas(files, root, importModule) {
|
|
|
1014
1014
|
var doctorUsage = `ace-kit doctor: check that this site is set up to work with CMS Ace.
|
|
1015
1015
|
|
|
1016
1016
|
Usage:
|
|
1017
|
-
ace-kit doctor [--site <domain>] [--
|
|
1017
|
+
ace-kit doctor [--site <domain>] [--dir <folder>]
|
|
1018
1018
|
|
|
1019
1019
|
Run it in your site's folder. It changes nothing and never prints your login. It checks your Node version, that Ace Kit
|
|
1020
1020
|
is installed, which CMS and site the settings point at, that the CMS can be reached and knows the site, that you are
|
|
@@ -1023,7 +1023,6 @@ says what to do. It exits with 1 if anything is wrong.
|
|
|
1023
1023
|
|
|
1024
1024
|
Options:
|
|
1025
1025
|
--site The domain your site has in the CMS (default: ACE_SITE_DOMAIN, else Astro's \`site\` setting).
|
|
1026
|
-
--api-url Where the CMS Ace API lives (default: ACE_API_URL, else the hosted CMS Ace).
|
|
1027
1026
|
--dir The folder holding your *.schema.ts files (default: src and app).
|
|
1028
1027
|
--help Show this message.
|
|
1029
1028
|
`;
|
|
@@ -1111,7 +1110,7 @@ async function doctorCommand(commandArguments, environment) {
|
|
|
1111
1110
|
let apiUrl = null;
|
|
1112
1111
|
try {
|
|
1113
1112
|
apiUrl = normaliseApiUrl(values["api-url"] ?? environment.variables.ACE_API_URL ?? hostedApiUrl);
|
|
1114
|
-
add("ok",
|
|
1113
|
+
add("ok", apiUrl === hostedApiUrl ? "Connected to CMS Ace" : `CMS: ${apiUrl}`);
|
|
1115
1114
|
} catch (error) {
|
|
1116
1115
|
add("fail", `The CMS address is wrong: ${error instanceof Error ? error.message : String(error)}`);
|
|
1117
1116
|
}
|
|
@@ -1147,7 +1146,7 @@ async function doctorCommand(commandArguments, environment) {
|
|
|
1147
1146
|
);
|
|
1148
1147
|
return;
|
|
1149
1148
|
}
|
|
1150
|
-
add("ok", "
|
|
1149
|
+
add("ok", "CMS Ace answers");
|
|
1151
1150
|
if (!siteDomain) return;
|
|
1152
1151
|
const asSite = { "X-Ace-Site-Domain": siteDomain };
|
|
1153
1152
|
const site = await request("/public/site", asSite);
|
|
@@ -1195,11 +1194,7 @@ async function doctorCommand(commandArguments, environment) {
|
|
|
1195
1194
|
now: environment.now
|
|
1196
1195
|
});
|
|
1197
1196
|
if (!login) {
|
|
1198
|
-
add(
|
|
1199
|
-
"warn",
|
|
1200
|
-
"Not signed in",
|
|
1201
|
-
`Run: npx ace-kit login${isLocalApiUrl(address) ? ` --api-url ${address}` : ""}. Section sync and dev sites need it.`
|
|
1202
|
-
);
|
|
1197
|
+
add("warn", "Not signed in", `Run: ${loginCommandFor(address)}. Section sync and dev sites need it.`);
|
|
1203
1198
|
return;
|
|
1204
1199
|
}
|
|
1205
1200
|
const authorised = { Authorization: `Bearer ${login.token}` };
|
|
@@ -1442,11 +1437,10 @@ export {};
|
|
|
1442
1437
|
var generateUsage = `ace-kit generate: write TypeScript types for your CMS Ace content types.
|
|
1443
1438
|
|
|
1444
1439
|
Usage:
|
|
1445
|
-
ace-kit generate [--site <domain>] [--
|
|
1440
|
+
ace-kit generate [--site <domain>] [--out <file>]
|
|
1446
1441
|
|
|
1447
1442
|
Options:
|
|
1448
1443
|
--site The domain your site has in the CMS, e.g. example.com (or set ACE_SITE_DOMAIN). --domain means the same.
|
|
1449
|
-
--api-url Where the CMS Ace API lives (or set ACE_API_URL). Only for a CMS on your own machine.
|
|
1450
1444
|
--out Where to write the types (default: src/ace-kit.generated.d.ts).
|
|
1451
1445
|
--help Show this message.
|
|
1452
1446
|
`;
|
|
@@ -1565,7 +1559,7 @@ async function syncSectionTypes(options) {
|
|
|
1565
1559
|
var syncUsage = `ace-kit sync: send the section types your code defines to CMS Ace.
|
|
1566
1560
|
|
|
1567
1561
|
Usage:
|
|
1568
|
-
ace-kit sync [--site <domain>] [--
|
|
1562
|
+
ace-kit sync [--site <domain>] [--dir <folder>]
|
|
1569
1563
|
|
|
1570
1564
|
It finds every *.schema.ts file in the folder (src and app by default), checks them, and makes the CMS's section types for your
|
|
1571
1565
|
site match. The dev server does this for you whenever a schema file is saved; run it yourself for a one-off sync, or on a
|
|
@@ -1575,9 +1569,8 @@ Nothing is deleted: a section type, block or field you take out of your code is
|
|
|
1575
1569
|
written in it survives. If any schema file can't be loaded, nothing is synced.
|
|
1576
1570
|
|
|
1577
1571
|
Options:
|
|
1578
|
-
--site The domain your site has in the CMS, e.g. example.com (
|
|
1572
|
+
--site The domain your site has in the CMS, e.g. example.com (or set ACE_SITE_DOMAIN).
|
|
1579
1573
|
(--domain means the same.)
|
|
1580
|
-
--api-url Where the CMS Ace API lives (default: ACE_API_URL, else the hosted CMS Ace). Only for a CMS on your own machine.
|
|
1581
1574
|
--dir The folder to look in (default: src and app).
|
|
1582
1575
|
--help Show this message.
|
|
1583
1576
|
`;
|
|
@@ -1635,7 +1628,7 @@ async function syncCommand(commandArguments, environment) {
|
|
|
1635
1628
|
now: environment.now
|
|
1636
1629
|
});
|
|
1637
1630
|
if (!login) {
|
|
1638
|
-
environment.out(`Section sync is waiting for you to sign in. Run:
|
|
1631
|
+
environment.out(`Section sync is waiting for you to sign in. Run: ${loginCommandFor(apiUrl)}`);
|
|
1639
1632
|
return automatic ? 0 : 1;
|
|
1640
1633
|
}
|
|
1641
1634
|
const loaded = await loadSchemas(files, environment.workingDirectory, environment.importModule);
|
|
@@ -1656,17 +1649,17 @@ async function syncCommand(commandArguments, environment) {
|
|
|
1656
1649
|
const defined = outcome.sections.filter((section) => !section.retired).map((section) => section.handle);
|
|
1657
1650
|
const retired = outcome.sections.filter((section) => section.retired).map((section) => section.handle);
|
|
1658
1651
|
environment.out(
|
|
1659
|
-
`Synced ${defined.length} section type${defined.length === 1 ? "" : "s"} (${defined.join(", ")}) to ${outcome.site.domain} (${apiUrl}).` + (retired.length > 0 ? ` No longer in your code, kept but hidden: ${retired.join(", ")}.` : "")
|
|
1652
|
+
`Synced ${defined.length} section type${defined.length === 1 ? "" : "s"} (${defined.join(", ")}) to ${outcome.site.domain}${apiUrl === hostedApiUrl ? "" : ` (${apiUrl})`}.` + (retired.length > 0 ? ` No longer in your code, kept but hidden: ${retired.join(", ")}.` : "")
|
|
1660
1653
|
);
|
|
1661
1654
|
for (const warning of outcome.warnings) environment.error(`Warning: ${warning}`);
|
|
1662
1655
|
return 0;
|
|
1663
1656
|
}
|
|
1664
1657
|
case "login-ended":
|
|
1665
|
-
environment.out(`Your login has ended. Run:
|
|
1658
|
+
environment.out(`Your login has ended. Run: ${loginCommandFor(apiUrl)}`);
|
|
1666
1659
|
return automatic ? 0 : 1;
|
|
1667
1660
|
case "site-not-found":
|
|
1668
1661
|
environment.error(
|
|
1669
|
-
|
|
1662
|
+
`${cmsName(apiUrl)} has no site with the domain "${outcome.domain}" that you can see` + (outcome.available.length > 0 ? ` (your sites: ${outcome.available.join(", ")})` : "") + ". Set ACE_SITE_DOMAIN or pass --site."
|
|
1670
1663
|
);
|
|
1671
1664
|
return 1;
|
|
1672
1665
|
case "forbidden":
|
|
@@ -1674,7 +1667,7 @@ async function syncCommand(commandArguments, environment) {
|
|
|
1674
1667
|
environment.error(outcome.message);
|
|
1675
1668
|
return 1;
|
|
1676
1669
|
case "unreachable":
|
|
1677
|
-
environment.error(`Could not reach
|
|
1670
|
+
environment.error(`Could not reach ${cmsName(apiUrl)}.`);
|
|
1678
1671
|
return 1;
|
|
1679
1672
|
}
|
|
1680
1673
|
}
|
package/dist/vite.js
CHANGED
package/docs/astro.md
CHANGED
|
@@ -15,21 +15,19 @@ export default defineConfig({
|
|
|
15
15
|
});
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The integration declares
|
|
18
|
+
The integration declares the site's settings, adds the middleware that lets the CMS's admin app (and only it) frame the site for [live preview](./live-preview.md), and makes Vite process the kit. The settings are read when the server starts, not baked into the build, so one build runs anywhere:
|
|
19
19
|
|
|
20
|
-
| Setting
|
|
21
|
-
|
|
|
22
|
-
| `
|
|
23
|
-
| `ACE_SITE_DOMAIN` | the host of Astro's `site` setting, else `localhost` | the domain this site has in the CMS (the port is ignored) |
|
|
24
|
-
| `ACE_PREVIEW_ADMIN_ORIGIN` | `https://app.ace-cms.co.uk` | where the CMS admin app runs |
|
|
20
|
+
| Setting | Default | Meaning |
|
|
21
|
+
| ----------------- | ---------------------------------------------------- | --------------------------------------------------------- |
|
|
22
|
+
| `ACE_SITE_DOMAIN` | the host of Astro's `site` setting, else `localhost` | the domain this site has in the CMS (the port is ignored) |
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
A site connects to CMS Ace by itself and only needs to say which site it is, which it does with Astro's own `site` setting, which Astro needs for canonical addresses anyway:
|
|
27
25
|
|
|
28
26
|
```js
|
|
29
27
|
export default defineConfig({ site: "https://www.example.com", integrations: [ace()] });
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
To try a site without editing the config, `npm run dev -- --site https://www.example.com` does the same. `ACE_SITE_DOMAIN`, when set, wins.
|
|
30
|
+
To try a site without editing the config, `npm run dev -- --site https://www.example.com` does the same. `ACE_SITE_DOMAIN`, when set, wins.
|
|
33
31
|
|
|
34
32
|
## Pages
|
|
35
33
|
|
|
@@ -94,6 +92,7 @@ import AcePreviewRefresh from "@sandboxsonny/ace-kit/astro/AcePreviewRefresh.ast
|
|
|
94
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.
|
|
95
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) { ... }`.
|
|
96
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.
|
|
97
96
|
- **AcePreviewRefresh** goes once in your layout. It does nothing unless the page was opened by the CMS's editor.
|
|
98
97
|
- **AceNotice** is the message for a page that isn't an entry.
|
|
99
98
|
|
package/docs/command-line.md
CHANGED
|
@@ -12,7 +12,7 @@ npx ace-kit generate # write TypeScript types (see Typed content)
|
|
|
12
12
|
npx ace-kit sync # send your section types to the CMS (see Sections)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Commands
|
|
15
|
+
Commands connect to CMS Ace for you, so `ace-kit login` and `ace-kit whoami` need no flags at all. Commands about a site (`sync`, `generate`, `doctor`) take `--site example.com`, or read `ACE_SITE_DOMAIN`.
|
|
16
16
|
|
|
17
17
|
## Making a site
|
|
18
18
|
|
|
@@ -36,9 +36,6 @@ Your account must be a **partner or super admin** (only they can make sites). Th
|
|
|
36
36
|
| `<domain>` (or `--site <domain>`) | The domain the site has in CMS Ace. Required. |
|
|
37
37
|
| `--framework astro\|react-router` | `astro` (the default) or `react-router`. Both have the same three example sections. |
|
|
38
38
|
| `--name <name>` | The name shown to visitors. Default: made from the folder, so `my-site` is "My Site". |
|
|
39
|
-
| `--api-url <address>` | Only for a CMS on your own machine (or set `ACE_API_URL`). |
|
|
40
|
-
|
|
41
|
-
Against a CMS on your own machine (`--api-url` or `ACE_API_URL` set to `http://localhost:8080`), it also writes a `.env` in the new folder with `ACE_API_URL` and `ACE_PREVIEW_ADMIN_ORIGIN=http://localhost:5173`, and sets the site's Preview address to the local dev server (`http://localhost:4321`, or 4322 for React Router), so the site and live preview work with no more setup. Against the hosted CMS Ace none of that is needed.
|
|
42
39
|
|
|
43
40
|
The folder must not exist yet (or must be empty). The starter is a small server-rendered site with a header, footer, rich text, live preview and Prettier, ESLint, tests and a CI workflow. When it finishes: `cd my-site`, `npm install`, `npm run dev`.
|
|
44
41
|
|
|
@@ -64,7 +61,7 @@ Run it in your site's folder when something isn't working. It changes nothing, n
|
|
|
64
61
|
✓ The CMS has all of them
|
|
65
62
|
```
|
|
66
63
|
|
|
67
|
-
It checks your Node version, that Ace Kit is installed, which CMS and site your settings point at, that the CMS can be reached and knows the site, that the site has its Pages content, that you are signed in, the site's preview address, and that the section types in your code match the CMS. `✗` is a problem (the command exits with 1), `!` is a warning, `-` is a note. It takes `--site
|
|
64
|
+
It checks your Node version, that Ace Kit is installed, which CMS and site your settings point at, that the CMS can be reached and knows the site, that the site has its Pages content, that you are signed in, the site's preview address, and that the section types in your code match the CMS. `✗` is a problem (the command exits with 1), `!` is a warning, `-` is a note. It takes `--site` and `--dir` like `sync`.
|
|
68
65
|
|
|
69
66
|
## Signing in
|
|
70
67
|
|
package/docs/dev-sites.md
CHANGED
|
@@ -34,7 +34,7 @@ A dev site is never merged into the live site. When your change is ready, deploy
|
|
|
34
34
|
|
|
35
35
|
## If something stops it
|
|
36
36
|
|
|
37
|
-
The dev server says so once, and carries on showing the **live site**, read-only, with section sync off: you aren't signed in (run `npx ace-kit login
|
|
37
|
+
The dev server says so once, and carries on showing the **live site**, read-only, with section sync off: you aren't signed in (run `npx ace-kit login`), your account can't have a dev site, the site isn't one you can see, or the CMS can't be reached.
|
|
38
38
|
|
|
39
39
|
## Housekeeping
|
|
40
40
|
|
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/docs/react-router.md
CHANGED
|
@@ -26,13 +26,11 @@ export default defineConfig({ plugins: [ace(), reactRouter()] });
|
|
|
26
26
|
|
|
27
27
|
The settings are environment variables, read when the server starts, so one build runs anywhere. Put them in `.env` for development and in the host's environment for production:
|
|
28
28
|
|
|
29
|
-
| Variable
|
|
30
|
-
|
|
|
31
|
-
| `
|
|
32
|
-
| `ACE_SITE_DOMAIN` | `localhost` | the domain this site has in the CMS (the port is ignored) |
|
|
33
|
-
| `ACE_PREVIEW_ADMIN_ORIGIN` | `https://app.ace-cms.co.uk` | where the CMS admin app runs |
|
|
29
|
+
| Variable | Default | Meaning |
|
|
30
|
+
| ----------------- | ----------- | --------------------------------------------------------- |
|
|
31
|
+
| `ACE_SITE_DOMAIN` | `localhost` | the domain this site has in the CMS (the port is ignored) |
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
A site connects to CMS Ace by itself and only needs to say which site it is, with `ACE_SITE_DOMAIN`.
|
|
36
34
|
|
|
37
35
|
**2. The server entry**, which renders pages and lets the CMS's admin app (and only it) frame the site for [live preview](./live-preview.md):
|
|
38
36
|
|
package/docs/typed-content.md
CHANGED
|
@@ -12,7 +12,7 @@ or with the settings in the environment:
|
|
|
12
12
|
ACE_SITE_DOMAIN=www.example.com npx ace-kit generate
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
It reads `GET /public/content-types` (no login: it describes field names and kinds, never entries) and writes `src/ace-kit.generated.d.ts` (change it with `--out`). Make sure the file is inside your `tsconfig` `include`. Commit it, and run the command again whenever content types change (a good moment is a CI step, or a `generate` script next to `dev`).
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
// ace-kit.generated.d.ts (generated)
|
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
|
) : (
|