@sandboxsonny/ace-kit 0.10.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 +6 -0
- package/dist/astro.d.ts +11 -0
- package/dist/astro.js +1 -1
- package/dist/{chunk-EKZRLWNO.js → chunk-PXT3H7C6.js} +23 -2
- package/dist/cli.js +2 -2
- package/dist/{environment-M3B4UZ5T.js → environment-LTPUSFAK.js} +1 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.js +1 -1
- package/dist/{main-6O2IIVHO.js → main-IKZ3DQLC.js} +9 -0
- package/dist/react-router-server.d.ts +15 -0
- package/dist/react-router-server.js +1 -1
- package/docs/astro.md +1 -1
- package/docs/react-router.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.10.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/astro.d.ts
CHANGED
|
@@ -53,6 +53,16 @@ type AceSite = {
|
|
|
53
53
|
favicon: AceFavicon | null;
|
|
54
54
|
};
|
|
55
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;
|
|
64
|
+
};
|
|
65
|
+
|
|
56
66
|
/** One link of a menu as the public API returns it, with its address already worked out. */
|
|
57
67
|
type AceMenuItem = {
|
|
58
68
|
label: string;
|
|
@@ -129,6 +139,7 @@ declare const ace: {
|
|
|
129
139
|
limit?: number;
|
|
130
140
|
}): Promise<AceEntry<AceValuesOf<Handle>>[]>;
|
|
131
141
|
getMenu(handle: string): Promise<AceMenu | null>;
|
|
142
|
+
getRedirect(address: string): Promise<AceRedirect | null>;
|
|
132
143
|
getSite(): Promise<AceSite | null>;
|
|
133
144
|
getContentTypes(): Promise<AceContentTypeSchema[]>;
|
|
134
145
|
};
|
package/dist/astro.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAceClient, loadMenuLinks, loadPage } from './chunk-
|
|
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
|
|
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 } :
|
|
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-
|
|
13
|
-
const { runCli } = await import("./main-
|
|
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.
|
|
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;
|
|
@@ -143,6 +153,11 @@ declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplem
|
|
|
143
153
|
}): Promise<AceEntry<AceValuesOf<Handle>>[]>;
|
|
144
154
|
/** One of the site's navigation menus by its handle, with every link worked out; null if the site has none. */
|
|
145
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>;
|
|
146
161
|
/** The site itself: its name, and the social image its owners chose (null if none). */
|
|
147
162
|
getSite(): Promise<AceSite | null>;
|
|
148
163
|
/** What the site's content types and fields are, which is what type generation reads. */
|
|
@@ -415,7 +430,8 @@ type AcePage<Values = Record<string, unknown>> =
|
|
|
415
430
|
* - `slug` is the part of the path after the site's root, e.g. "pages/about" ("" or undefined for the home page).
|
|
416
431
|
* - `url` is the full request address; it carries the preview token the CMS puts in its preview frame.
|
|
417
432
|
*
|
|
418
|
-
* Addresses are `/{content type}/{entry}` as on Shopify, and `/` is the home entry.
|
|
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
|
|
419
435
|
* token is never cached or indexed (the answer's `headers` say so).
|
|
420
436
|
*/
|
|
421
437
|
declare function loadPage<Values = Record<string, unknown>>(client: AceClient, { slug, url }: {
|
|
@@ -576,4 +592,4 @@ type AceSection = {
|
|
|
576
592
|
blocks: AceSectionBlock[];
|
|
577
593
|
};
|
|
578
594
|
|
|
579
|
-
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 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-
|
|
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
|
|
|
@@ -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");
|
|
@@ -126,6 +126,16 @@ type AceContentTypeSchema = {
|
|
|
126
126
|
fields: AceFieldSchema[];
|
|
127
127
|
};
|
|
128
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
|
+
|
|
129
139
|
/**
|
|
130
140
|
* A client for the CMS Ace public API. It runs on the server only, so the visitor's browser never talks to the
|
|
131
141
|
* API. The API finds the site from X-Ace-Site-Domain, which this client sets to the site's domain. X-Forwarded-Host
|
|
@@ -146,6 +156,11 @@ declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplem
|
|
|
146
156
|
}): Promise<AceEntry<AceValuesOf<Handle>>[]>;
|
|
147
157
|
/** One of the site's navigation menus by its handle, with every link worked out; null if the site has none. */
|
|
148
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>;
|
|
149
164
|
/** The site itself: its name, and the social image its owners chose (null if none). */
|
|
150
165
|
getSite(): Promise<AceSite | null>;
|
|
151
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-
|
|
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
|
|
package/docs/react-router.md
CHANGED
|
@@ -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.
|
|
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",
|