@sandboxsonny/ace-kit 0.9.0 → 0.11.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.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f0a08c3: Follow the redirects set up in the CMS. When a request is for an address the site has no page for, `getPage` (and the React Router page loader) now asks the CMS whether the site's owners redirected it, and answers with that redirect (301 or 302, to a path on the site or another address) instead of "not found". Your page already handles redirects, so there is nothing to change. A page shown in the CMS's preview frame is never redirected. Also adds `client.getRedirect(address)`.
8
+
9
+ ## 0.10.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 4215f32: Show the site's favicon in `<AceSeo>`. The favicon is chosen in the site's settings in the CMS; `<AceSeo>` adds `<link rel="icon">` and `<link rel="apple-touch-icon">` for it when you don't pass a `favicon` yourself. If your layout already has its own icon link, remove it, or the browser may pick either. `getSite()` now also returns the favicon.
14
+
3
15
  ## 0.9.0
4
16
 
5
17
  ### Minor Changes
@@ -7,10 +7,15 @@ import { getSite } from "../dist/astro.js";
7
7
 
8
8
  type Props = AceSeoOptions;
9
9
 
10
- // With no picture of its own, a page shows the social image chosen in the site's settings. The tags are never worth
11
- // failing the page for, so a CMS that can't be reached just means no picture.
12
- const site = Astro.props.image === undefined ? await getSite().catch(() => null) : null;
13
- const properties = seoProperties({ ...Astro.props, image: Astro.props.image ?? site?.social_image?.src });
10
+ // With no picture or icon of its own, a page shows the ones chosen in the site's settings. The tags are never worth
11
+ // failing the page for, so a CMS that can't be reached just means none.
12
+ const needsSite = Astro.props.image === undefined || Astro.props.favicon === undefined;
13
+ const site = needsSite ? await getSite().catch(() => null) : null;
14
+ const properties = seoProperties({
15
+ ...Astro.props,
16
+ image: Astro.props.image ?? site?.social_image?.src,
17
+ favicon: Astro.props.favicon ?? site?.favicon ?? undefined,
18
+ });
14
19
  ---
15
20
 
16
21
  <SEO {...properties} />
package/dist/astro.d.ts CHANGED
@@ -29,6 +29,18 @@ type AceImageData = {
29
29
  content_type: string;
30
30
  };
31
31
 
32
+ /** The site's favicon as the CMS Ace public API returns it: the original, and square PNG copies. */
33
+ type AceFavicon = {
34
+ id: number;
35
+ /** The untouched original. */
36
+ url: string;
37
+ /** A small square PNG (48 pixels), for the browser tab and search results. */
38
+ icon: string;
39
+ /** A square PNG (180 pixels), for a phone's home screen. */
40
+ apple_touch_icon: string;
41
+ content_type: string;
42
+ };
43
+
32
44
  /** The site the request is for, as the CMS Ace public API returns it. */
33
45
  type AceSite = {
34
46
  id: number;
@@ -37,6 +49,18 @@ type AceSite = {
37
49
  handle: string;
38
50
  /** The picture the site's owners chose to show in search results and social posts; null if none. */
39
51
  social_image: AceImageData | null;
52
+ /** The small picture the site's owners chose for browser tabs; null if none. */
53
+ favicon: AceFavicon | null;
54
+ };
55
+
56
+ /** A redirect the CMS has for an address, as its public API returns it. */
57
+ type AceRedirect = {
58
+ /** The old address the redirect is for. */
59
+ from: string;
60
+ /** Where it goes: a path on the site, or a full web address. */
61
+ to: string;
62
+ /** 301 is permanent; 302 is temporary. */
63
+ status: 301 | 302;
40
64
  };
41
65
 
42
66
  /** One link of a menu as the public API returns it, with its address already worked out. */
@@ -115,6 +139,7 @@ declare const ace: {
115
139
  limit?: number;
116
140
  }): Promise<AceEntry<AceValuesOf<Handle>>[]>;
117
141
  getMenu(handle: string): Promise<AceMenu | null>;
142
+ getRedirect(address: string): Promise<AceRedirect | null>;
118
143
  getSite(): Promise<AceSite | null>;
119
144
  getContentTypes(): Promise<AceContentTypeSchema[]>;
120
145
  };
package/dist/astro.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createAceClient, loadMenuLinks, loadPage } from './chunk-EKZRLWNO.js';
1
+ import { createAceClient, loadMenuLinks, loadPage } from './chunk-PXT3H7C6.js';
2
2
  import './chunk-M5WR5TCR.js';
3
3
  import './chunk-YLO6IAXF.js';
4
4
  import { ACE_SITE_DOMAIN, ACE_API_URL } from 'astro:env/server';
@@ -89,6 +89,15 @@ function createAceClient({
89
89
  getMenu(handle) {
90
90
  return get(`/public/menus/${encodeURIComponent(handle)}`);
91
91
  },
92
+ /**
93
+ * Where an address the site has no page for should redirect to, if the site's owners set that up; null if not.
94
+ * `address` is the path with its query string, e.g. "/old-page?id=7".
95
+ */
96
+ async getRedirect(address) {
97
+ const redirect = await get(`/public/redirects?from=${encodeURIComponent(address)}`);
98
+ const goesSomewhereSafe = redirect !== null && typeof redirect.to === "string" && (redirect.to.startsWith("/") && !redirect.to.startsWith("//") && !redirect.to.includes("\\") || /^https?:\/\/[^\s]+$/i.test(redirect.to)) && (redirect.status === 301 || redirect.status === 302);
99
+ return goesSomewhereSafe ? redirect : null;
100
+ },
92
101
  /** The site itself: its name, and the social image its owners chose (null if none). */
93
102
  getSite() {
94
103
  return get("/public/site");
@@ -132,12 +141,12 @@ async function loadPage(client, { slug, url }) {
132
141
  if (slug && address && isHomeAddress(address)) {
133
142
  return { kind: "redirect", status: url.search ? 302 : 301, location: `/${url.search}`, headers };
134
143
  }
135
- if (!address) return { kind: "not-found", status: 404, headers };
144
+ if (!address) return notFoundOrRedirect(client, url, previewToken, headers);
136
145
  try {
137
146
  const entry = await client.getEntry(address.contentTypeHandle, address.entryHandle, {
138
147
  previewToken
139
148
  });
140
- return entry ? { kind: "entry", status: 200, entry, headers } : { kind: "not-found", status: 404, headers };
149
+ return entry ? { kind: "entry", status: 200, entry, headers } : notFoundOrRedirect(client, url, previewToken, headers);
141
150
  } catch (error) {
142
151
  if (error instanceof AcePreviewExpiredError) return { kind: "preview-expired", status: 403, headers };
143
152
  if (error instanceof AceUnavailableError) {
@@ -147,5 +156,17 @@ async function loadPage(client, { slug, url }) {
147
156
  throw error;
148
157
  }
149
158
  }
159
+ async function notFoundOrRedirect(client, url, previewToken, headers) {
160
+ if (previewToken) return { kind: "not-found", status: 404, headers };
161
+ try {
162
+ const redirect = await client.getRedirect(`${url.pathname}${url.search}`);
163
+ if (redirect) return { kind: "redirect", status: redirect.status, location: redirect.to, headers };
164
+ } catch (error) {
165
+ if (!(error instanceof AceUnavailableError)) throw error;
166
+ console.error(error.message);
167
+ return { kind: "unavailable", status: 503, headers };
168
+ }
169
+ return { kind: "not-found", status: 404, headers };
170
+ }
150
171
 
151
172
  export { AcePreviewExpiredError, AceUnavailableError, createAceClient, isCurrentPage, linksFromMenu, loadMenuLinks, loadPage };
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-YCQRJ56U.js");
13
- const { runCli } = await import("./main-6O2IIVHO.js");
12
+ const { createRealEnvironment } = await import("./environment-LTPUSFAK.js");
13
+ const { runCli } = await import("./main-IKZ3DQLC.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.9.0" : "development";
148
+ var aceKitVersion = true ? "0.11.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
@@ -84,6 +84,16 @@ type AceNavigationLink = {
84
84
  children?: AceNavigationLink[];
85
85
  };
86
86
 
87
+ /** A redirect the CMS has for an address, as its public API returns it. */
88
+ type AceRedirect = {
89
+ /** The old address the redirect is for. */
90
+ from: string;
91
+ /** Where it goes: a path on the site, or a full web address. */
92
+ to: string;
93
+ /** 301 is permanent; 302 is temporary. */
94
+ status: 301 | 302;
95
+ };
96
+
87
97
  /** An image as the CMS Ace public API returns it. Use `src` and `srcset` in an `<img>`. */
88
98
  type AceImageData = {
89
99
  id: number;
@@ -99,6 +109,18 @@ type AceImageData = {
99
109
  content_type: string;
100
110
  };
101
111
 
112
+ /** The site's favicon as the CMS Ace public API returns it: the original, and square PNG copies. */
113
+ type AceFavicon = {
114
+ id: number;
115
+ /** The untouched original. */
116
+ url: string;
117
+ /** A small square PNG (48 pixels), for the browser tab and search results. */
118
+ icon: string;
119
+ /** A square PNG (180 pixels), for a phone's home screen. */
120
+ apple_touch_icon: string;
121
+ content_type: string;
122
+ };
123
+
102
124
  /** The site the request is for, as the CMS Ace public API returns it. */
103
125
  type AceSite = {
104
126
  id: number;
@@ -107,6 +129,8 @@ type AceSite = {
107
129
  handle: string;
108
130
  /** The picture the site's owners chose to show in search results and social posts; null if none. */
109
131
  social_image: AceImageData | null;
132
+ /** The small picture the site's owners chose for browser tabs; null if none. */
133
+ favicon: AceFavicon | null;
110
134
  };
111
135
 
112
136
  /**
@@ -129,6 +153,11 @@ declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplem
129
153
  }): Promise<AceEntry<AceValuesOf<Handle>>[]>;
130
154
  /** One of the site's navigation menus by its handle, with every link worked out; null if the site has none. */
131
155
  getMenu(handle: string): Promise<AceMenu | null>;
156
+ /**
157
+ * Where an address the site has no page for should redirect to, if the site's owners set that up; null if not.
158
+ * `address` is the path with its query string, e.g. "/old-page?id=7".
159
+ */
160
+ getRedirect(address: string): Promise<AceRedirect | null>;
132
161
  /** The site itself: its name, and the social image its owners chose (null if none). */
133
162
  getSite(): Promise<AceSite | null>;
134
163
  /** What the site's content types and fields are, which is what type generation reads. */
@@ -401,7 +430,8 @@ type AcePage<Values = Record<string, unknown>> =
401
430
  * - `slug` is the part of the path after the site's root, e.g. "pages/about" ("" or undefined for the home page).
402
431
  * - `url` is the full request address; it carries the preview token the CMS puts in its preview frame.
403
432
  *
404
- * Addresses are `/{content type}/{entry}` as on Shopify, and `/` is the home entry. A page shown with a preview
433
+ * Addresses are `/{content type}/{entry}` as on Shopify, and `/` is the home entry. An address with no page is
434
+ * redirected if the site's owners set up a redirect for it (in the CMS), and otherwise not found. A page shown with a preview
405
435
  * token is never cached or indexed (the answer's `headers` say so).
406
436
  */
407
437
  declare function loadPage<Values = Record<string, unknown>>(client: AceClient, { slug, url }: {
@@ -478,6 +508,11 @@ type AceSeoOptions = {
478
508
  * social image chosen in the site's settings in the CMS (if any).
479
509
  */
480
510
  image?: string;
511
+ /**
512
+ * The site's favicon. In the Astro `<AceSeo>`, leaving it out uses the favicon chosen in the site's settings in the
513
+ * CMS (if any); it then adds `<link rel="icon">` and `<link rel="apple-touch-icon">`.
514
+ */
515
+ favicon?: Pick<AceFavicon, "icon" | "apple_touch_icon">;
481
516
  /** Tells search engines to leave the page out, as for a draft shown in preview. */
482
517
  noindex?: boolean;
483
518
  };
@@ -486,6 +521,11 @@ type Meta = {
486
521
  name?: string;
487
522
  content: string;
488
523
  };
524
+ type Link = {
525
+ rel: string;
526
+ href: string;
527
+ sizes?: string;
528
+ };
489
529
  /** What `astro-seo`'s `<SEO>` takes (only the parts used here). */
490
530
  type AceSeoProperties = {
491
531
  title?: string;
@@ -500,6 +540,7 @@ type AceSeoProperties = {
500
540
  };
501
541
  extend: {
502
542
  meta: Meta[];
543
+ link?: Link[];
503
544
  };
504
545
  };
505
546
  /**
@@ -551,4 +592,4 @@ type AceSection = {
551
592
  blocks: AceSectionBlock[];
552
593
  };
553
594
 
554
- 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, type AceSite, 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 };
595
+ export { type AceAddress, type AceClient, type AceClientOptions, type AceContentTypeHandle, type AceContentTypeSchema, type AceContentTypes, type AceDateFormat, type AceEntry, type AceFavicon, type AceFieldSchema, type AceImageAttributes, type AceImageData, type AceImageOptions, type AceLinkAttributes, type AceLinkData, type AceMenu, type AceMenuItem, type AceNavigationLink, type AcePage, AcePreviewExpiredError, type AceRedirect, type AceRichTextDocument, type AceRichTextMark, type AceRichTextNode, type AceSection, type AceSectionBlock, type AceSeoOptions, type AceSeoProperties, type AceSite, 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
@@ -1,7 +1,7 @@
1
1
  export { pageNotices } from './chunk-XPZY5R5A.js';
2
2
  export { headingLevel, imageAttributes, isInternalPath, linkAddress, linkAttributes, listStart, renderRichText, renderTextArea, richTextImageSizes } from './chunk-FVR55GIU.js';
3
3
  export { listenForPreviewRefresh, refreshPageContent } from './chunk-YN7LJWGI.js';
4
- export { AcePreviewExpiredError, AceUnavailableError, createAceClient, isCurrentPage, linksFromMenu, loadMenuLinks, loadPage } from './chunk-EKZRLWNO.js';
4
+ export { AcePreviewExpiredError, AceUnavailableError, createAceClient, isCurrentPage, linksFromMenu, loadMenuLinks, loadPage } from './chunk-PXT3H7C6.js';
5
5
  export { addressFromPath, homeAddress, isHomeAddress, pathOfAddress, visitablePath } from './chunk-M5WR5TCR.js';
6
6
  export { addFramingPolicy, framingPolicy, isRefreshRequest, originOf, previewHeaders, previewParameter, previewTokenFrom, refreshMessageType, swapPageContent } from './chunk-YLO6IAXF.js';
7
7
 
@@ -160,7 +160,15 @@ function seoProperties(options) {
160
160
  ...description && { description },
161
161
  ...options.image && { image: options.image }
162
162
  },
163
- extend: { meta }
163
+ extend: {
164
+ meta,
165
+ ...options.favicon && {
166
+ link: [
167
+ { rel: "icon", href: options.favicon.icon, sizes: "48x48" },
168
+ { rel: "apple-touch-icon", href: options.favicon.apple_touch_icon }
169
+ ]
170
+ }
171
+ }
164
172
  };
165
173
  }
166
174
 
@@ -1367,6 +1367,15 @@ function createAceClient({
1367
1367
  getMenu(handle) {
1368
1368
  return get(`/public/menus/${encodeURIComponent(handle)}`);
1369
1369
  },
1370
+ /**
1371
+ * Where an address the site has no page for should redirect to, if the site's owners set that up; null if not.
1372
+ * `address` is the path with its query string, e.g. "/old-page?id=7".
1373
+ */
1374
+ async getRedirect(address) {
1375
+ const redirect = await get(`/public/redirects?from=${encodeURIComponent(address)}`);
1376
+ const goesSomewhereSafe = redirect !== null && typeof redirect.to === "string" && (redirect.to.startsWith("/") && !redirect.to.startsWith("//") && !redirect.to.includes("\\") || /^https?:\/\/[^\s]+$/i.test(redirect.to)) && (redirect.status === 301 || redirect.status === 302);
1377
+ return goesSomewhereSafe ? redirect : null;
1378
+ },
1370
1379
  /** The site itself: its name, and the social image its owners chose (null if none). */
1371
1380
  getSite() {
1372
1381
  return get("/public/site");
@@ -50,6 +50,18 @@ type AceImageData = {
50
50
  content_type: string;
51
51
  };
52
52
 
53
+ /** The site's favicon as the CMS Ace public API returns it: the original, and square PNG copies. */
54
+ type AceFavicon = {
55
+ id: number;
56
+ /** The untouched original. */
57
+ url: string;
58
+ /** A small square PNG (48 pixels), for the browser tab and search results. */
59
+ icon: string;
60
+ /** A square PNG (180 pixels), for a phone's home screen. */
61
+ apple_touch_icon: string;
62
+ content_type: string;
63
+ };
64
+
53
65
  /** The site the request is for, as the CMS Ace public API returns it. */
54
66
  type AceSite = {
55
67
  id: number;
@@ -58,6 +70,8 @@ type AceSite = {
58
70
  handle: string;
59
71
  /** The picture the site's owners chose to show in search results and social posts; null if none. */
60
72
  social_image: AceImageData | null;
73
+ /** The small picture the site's owners chose for browser tabs; null if none. */
74
+ favicon: AceFavicon | null;
61
75
  };
62
76
 
63
77
  /** One link of a menu as the public API returns it, with its address already worked out. */
@@ -112,6 +126,16 @@ type AceContentTypeSchema = {
112
126
  fields: AceFieldSchema[];
113
127
  };
114
128
 
129
+ /** A redirect the CMS has for an address, as its public API returns it. */
130
+ type AceRedirect = {
131
+ /** The old address the redirect is for. */
132
+ from: string;
133
+ /** Where it goes: a path on the site, or a full web address. */
134
+ to: string;
135
+ /** 301 is permanent; 302 is temporary. */
136
+ status: 301 | 302;
137
+ };
138
+
115
139
  /**
116
140
  * A client for the CMS Ace public API. It runs on the server only, so the visitor's browser never talks to the
117
141
  * API. The API finds the site from X-Ace-Site-Domain, which this client sets to the site's domain. X-Forwarded-Host
@@ -132,6 +156,11 @@ declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplem
132
156
  }): Promise<AceEntry<AceValuesOf<Handle>>[]>;
133
157
  /** One of the site's navigation menus by its handle, with every link worked out; null if the site has none. */
134
158
  getMenu(handle: string): Promise<AceMenu | null>;
159
+ /**
160
+ * Where an address the site has no page for should redirect to, if the site's owners set that up; null if not.
161
+ * `address` is the path with its query string, e.g. "/old-page?id=7".
162
+ */
163
+ getRedirect(address: string): Promise<AceRedirect | null>;
135
164
  /** The site itself: its name, and the social image its owners chose (null if none). */
136
165
  getSite(): Promise<AceSite | null>;
137
166
  /** What the site's content types and fields are, which is what type generation reads. */
@@ -1,4 +1,4 @@
1
- import { createAceClient, loadMenuLinks, loadPage } from './chunk-EKZRLWNO.js';
1
+ import { createAceClient, loadMenuLinks, loadPage } from './chunk-PXT3H7C6.js';
2
2
  import './chunk-M5WR5TCR.js';
3
3
  import { hostedAdminOrigin, hostedApiUrl } from './chunk-N5DIMG3X.js';
4
4
  import { previewTokenFrom, addFramingPolicy } from './chunk-YLO6IAXF.js';
package/docs/astro.md CHANGED
@@ -46,7 +46,7 @@ if (page.kind === "redirect") return Astro.redirect(page.location, page.status);
46
46
  {page.kind === "entry" ? <h1>{page.entry.title}</h1> : <AceNotice page={page} />}
47
47
  ```
48
48
 
49
- Addresses are `/{content type}/{entry}` as on Shopify, `/` is the `home` entry of `pages`, and `/pages/home` redirects to `/`. `page.kind` is `entry`, `redirect`, `not-found`, `unavailable` or `preview-expired`. `AceNotice` draws a heading and a message for the last three; draw your own if you prefer.
49
+ Addresses are `/{content type}/{entry}` as on Shopify, `/` is the `home` entry of `pages`, and `/pages/home` redirects to `/`. An address with no page is redirected too if the site's owners set up a redirect for it in the CMS (Redirects), so old addresses from a site you moved keep working. `page.kind` is `entry`, `redirect`, `not-found`, `unavailable` or `preview-expired`. `AceNotice` draws a heading and a message for the last three; draw your own if you prefer.
50
50
 
51
51
  ## Reading content
52
52
 
@@ -69,7 +69,7 @@ export default function Page({ loaderData: page }: Route.ComponentProps) {
69
69
  }
70
70
  ```
71
71
 
72
- The loader reads the address (`/{content type}/{entry}`, and `/` is the `home` entry of `pages`), loads the entry and answers with the right status and headers: a missing page is a real 404, an unreachable CMS a 503, an expired preview a 403, `/pages/home` redirects to `/`, and a preview is never cached or indexed. `page.kind` is `entry`, `not-found`, `unavailable` or `preview-expired`. `createMeta` titles the page from the entry (followed by the site's name), adds its description, and asks search engines to ignore a preview. With [typed content](./typed-content.md), `createPageLoader<PageValues>()` gives `page.entry.values` its real shape.
72
+ The loader reads the address (`/{content type}/{entry}`, and `/` is the `home` entry of `pages`), loads the entry and answers with the right status and headers: a missing page is a real 404, an unreachable CMS a 503, an expired preview a 403, `/pages/home` redirects to `/`, an address with no page follows the redirect the site's owners set up in the CMS (if any), and a preview is never cached or indexed. `page.kind` is `entry`, `not-found`, `unavailable` or `preview-expired`. `createMeta` titles the page from the entry (followed by the site's name), adds its description, and asks search engines to ignore a preview. With [typed content](./typed-content.md), `createPageLoader<PageValues>()` gives `page.entry.values` its real shape.
73
73
 
74
74
  **4. The root route** loads the header menu and keeps previews fresh:
75
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandboxsonny/ace-kit",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",