@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 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-T7DKI2U3.js';
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: npx ace-kit login --api-url ${outcome.apiUrl}`
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-J4VYOHDK.js");
13
- const { runCli } = await import("./main-AGLPUGNN.js");
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.7.0" : "development";
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
- export { checkSectionDefinition, defineSection, formatAceDate, sectionTypesPayload };
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, ...settings].join("\n"));
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 [--api-url <address>] Sign in. Opens the admin app, where you type the code shown here.
529
- ace-kit logout [--api-url <address>] Sign out, and forget the login kept on this machine.
530
- ace-kit whoami [--api-url <address>] Say who you are signed in as.
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
- --api-url Where the CMS Ace API lives (or set ACE_API_URL, in the environment or a .env file).
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: ace-kit login`);
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 the CMS at ${apiUrl}.`);
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>] [--api-url <address>]
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>] [--api-url <address>] [--dir <folder>]
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", `CMS: ${apiUrl}${apiUrl === hostedApiUrl ? " (the hosted CMS Ace)" : ""}`);
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", "The CMS answers");
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>] [--api-url <address>] [--out <file>]
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>] [--api-url <address>] [--dir <folder>]
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 (default: ACE_SITE_DOMAIN, else localhost).
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: npx ace-kit login --api-url ${apiUrl}`);
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: npx ace-kit login --api-url ${apiUrl}`);
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
- `The CMS at ${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."
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 the CMS at ${apiUrl}.`);
1670
+ environment.error(`Could not reach ${cmsName(apiUrl)}.`);
1678
1671
  return 1;
1679
1672
  }
1680
1673
  }
package/dist/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { startDevSite, startSectionSync } from './chunk-T7DKI2U3.js';
1
+ import { startDevSite, startSectionSync } from './chunk-2W2NWDKV.js';
2
2
  import './chunk-N5DIMG3X.js';
3
3
  import { resolve } from 'path';
4
4
  import { loadEnv } from 'vite';
package/docs/astro.md CHANGED
@@ -15,21 +15,19 @@ export default defineConfig({
15
15
  });
16
16
  ```
17
17
 
18
- The integration declares three 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:
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 | Default | Meaning |
21
- | -------------------------- | ---------------------------------------------------- | --------------------------------------------------------- |
22
- | `ACE_API_URL` | `https://api.ace-cms.co.uk` | where the CMS Ace API lives |
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
- CMS Ace is a hosted platform, so a site normally sets none of these except its domain, and it does that with Astro's own `site` setting, which Astro needs for canonical addresses anyway:
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. Set `ACE_API_URL` and `ACE_PREVIEW_ADMIN_ORIGIN` (in `.env`) only to use a CMS running on your own machine, for example `http://localhost:8080` and `http://localhost:5173`.
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
 
@@ -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 talk to the hosted CMS Ace unless told otherwise. Only for a CMS on your own machine, give `--api-url`, or set `ACE_API_URL` in the environment or a `.env` file in the folder you run it in. Failing both, a command uses the address you last signed in to (kept in a small file beside your logins), and failing that the hosted one, so `ace-kit login` and `ace-kit whoami` need no flags at all. Commands about a site (`sync`, `generate`) take `--site example.com`, or read `ACE_SITE_DOMAIN`.
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`, `--api-url` and `--dir` like `sync`.
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 --api-url <address>`), your account can't have a dev site, the site isn't one you can see, or the CMS can't be reached.
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
 
@@ -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` |
@@ -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 | Default | Meaning |
30
- | -------------------------- | --------------------------- | --------------------------------------------------------- |
31
- | `ACE_API_URL` | `https://api.ace-cms.co.uk` | where the CMS Ace API lives |
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
- CMS Ace is a hosted platform, so a site normally sets only `ACE_SITE_DOMAIN`. Set the other two only to use a CMS on your own machine (`http://localhost:8080` and `http://localhost:5173`).
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
 
@@ -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
- Add `--api-url http://localhost:8080` (or `ACE_API_URL`) only for a CMS on your own machine. 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`).
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.7.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": "^0.5.0",
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",
@@ -17,6 +17,10 @@ declare module "@sandboxsonny/ace-kit" {
17
17
  "button"?: AceLinkData;
18
18
  /** Content */
19
19
  "content"?: AceSection[];
20
+ /** SEO title */
21
+ "seo-title": string;
22
+ /** SEO description */
23
+ "seo-description": string;
20
24
  };
21
25
  }
22
26
  }
@@ -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
- <title>{title}</title>
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
  ) : (