@sandboxsonny/ace-kit 0.11.0 → 0.12.1
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/dist/astro.d.ts +44 -1
- package/dist/astro.js +4 -2
- package/dist/{chunk-PXT3H7C6.js → chunk-RW4CVLES.js} +61 -1
- package/dist/cli.js +2 -2
- package/dist/{environment-LTPUSFAK.js → environment-CXO6CVNR.js} +1 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +1 -1
- package/dist/{main-IKZ3DQLC.js → main-4TEJQB6N.js} +6 -0
- package/dist/react-router-server.d.ts +45 -1
- package/dist/react-router-server.js +10 -2
- package/docs/api-reference.md +16 -14
- package/docs/astro.md +18 -0
- package/docs/react-router.md +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sandboxsonny/ace-kit
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ea4efe1: Fix the addresses in `sitemap.xml` and `robots.txt` on a site behind a proxy. They were built from the address the server was reached at, which behind a proxy such as Railway is the server's own internal address (`http://localhost:8080`). They now use the site's domain in the CMS (`ACE_SITE_DOMAIN`) over https, and fall back to the request's address only for a site on `localhost` or a `.test` domain. Also adds `client.siteDomain`.
|
|
8
|
+
|
|
9
|
+
## 0.12.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 4d64734: Add a sitemap and `robots.txt`. `getSitemapResponse` and `getRobotsResponse` (Astro), and `sitemapLoader` and `robotsLoader` (React Router), each make the file in one line. The sitemap lists the site's live pages from the CMS, leaving out content types whose entries aren't pages of their own, with when each last changed. Also adds `client.getSitemap()`, and the pure `sitemapXml`, `robotsTxt` and `publicOrigin` helpers.
|
|
14
|
+
|
|
3
15
|
## 0.11.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/astro.d.ts
CHANGED
|
@@ -53,6 +53,14 @@ type AceSite = {
|
|
|
53
53
|
favicon: AceFavicon | null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
/** One page of the site as the CMS lists it for the sitemap. */
|
|
57
|
+
type AceSitemapEntry = {
|
|
58
|
+
/** The address visitors use: "/" for the home entry, otherwise "/{content type}/{entry}". */
|
|
59
|
+
path: string;
|
|
60
|
+
/** When the entry was last changed, as an ISO date and time. */
|
|
61
|
+
updated_at: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
56
64
|
/** A redirect the CMS has for an address, as its public API returns it. */
|
|
57
65
|
type AceRedirect = {
|
|
58
66
|
/** The old address the redirect is for. */
|
|
@@ -122,6 +130,13 @@ type AceValuesOf<Handle extends string> = Handle extends keyof AceContentTypes
|
|
|
122
130
|
? AceContentTypes[Handle]
|
|
123
131
|
: Record<string, unknown>;
|
|
124
132
|
|
|
133
|
+
type RobotsOptions = {
|
|
134
|
+
/** Asks every crawler to stay away, for a site that isn't live yet. */
|
|
135
|
+
disallowAll?: boolean;
|
|
136
|
+
/** The site's address, e.g. `https://example.com`. Left out, the address the request came to is used. */
|
|
137
|
+
origin?: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
125
140
|
/** What to do with a request for a page: show an entry, redirect, or explain. */
|
|
126
141
|
type AcePage<Values = Record<string, unknown>> =
|
|
127
142
|
| { kind: "entry"; status: 200; entry: AceEntry<Values>; headers: Record<string, string> }
|
|
@@ -132,6 +147,7 @@ type AcePage<Values = Record<string, unknown>> =
|
|
|
132
147
|
|
|
133
148
|
/** The client for this site, made from the integration's settings. Most pages only need the functions below. */
|
|
134
149
|
declare const ace: {
|
|
150
|
+
siteDomain: string;
|
|
135
151
|
getEntry<Handle extends AceContentTypeHandle>(contentTypeHandle: Handle, handle: string, options?: {
|
|
136
152
|
previewToken?: string | null;
|
|
137
153
|
}): Promise<AceEntry<AceValuesOf<Handle>> | null>;
|
|
@@ -140,6 +156,7 @@ declare const ace: {
|
|
|
140
156
|
}): Promise<AceEntry<AceValuesOf<Handle>>[]>;
|
|
141
157
|
getMenu(handle: string): Promise<AceMenu | null>;
|
|
142
158
|
getRedirect(address: string): Promise<AceRedirect | null>;
|
|
159
|
+
getSitemap(): Promise<AceSitemapEntry[]>;
|
|
143
160
|
getSite(): Promise<AceSite | null>;
|
|
144
161
|
getContentTypes(): Promise<AceContentTypeSchema[]>;
|
|
145
162
|
};
|
|
@@ -188,5 +205,31 @@ declare function getPage<Values = Record<string, unknown>>(astro: {
|
|
|
188
205
|
headers: Headers;
|
|
189
206
|
};
|
|
190
207
|
}): Promise<AcePage<Values>>;
|
|
208
|
+
/**
|
|
209
|
+
* The response for `/sitemap.xml`: the site's live pages (of the content types that have pages), with when each last
|
|
210
|
+
* changed. Make `src/pages/sitemap.xml.ts` with
|
|
211
|
+
*
|
|
212
|
+
* import { getSitemapResponse } from "@sandboxsonny/ace-kit/astro";
|
|
213
|
+
* export const GET = ({ url }) => getSitemapResponse({ url });
|
|
214
|
+
*
|
|
215
|
+
* The addresses are the site's domain (`ACE_SITE_DOMAIN`) over https, or the address of the request for a site on localhost; pass `{ origin: "https://example.com" }` to choose.
|
|
216
|
+
*/
|
|
217
|
+
declare const getSitemapResponse: (astro: {
|
|
218
|
+
url: URL;
|
|
219
|
+
}, options?: {
|
|
220
|
+
origin?: string;
|
|
221
|
+
}) => Promise<Response>;
|
|
222
|
+
/**
|
|
223
|
+
* The response for `/robots.txt`, which lets crawlers in and points them at the sitemap. Make `src/pages/robots.txt.ts`
|
|
224
|
+
* with
|
|
225
|
+
*
|
|
226
|
+
* import { getRobotsResponse } from "@sandboxsonny/ace-kit/astro";
|
|
227
|
+
* export const GET = ({ url }) => getRobotsResponse({ url });
|
|
228
|
+
*
|
|
229
|
+
* Pass `{ disallowAll: true }` for a site that isn't live yet.
|
|
230
|
+
*/
|
|
231
|
+
declare const getRobotsResponse: (astro: {
|
|
232
|
+
url: URL;
|
|
233
|
+
}, options?: RobotsOptions) => Response;
|
|
191
234
|
|
|
192
|
-
export { ace, applyPage, getEntries, getEntry, getMenu, getMenuLinks, getPage, getSite };
|
|
235
|
+
export { ace, applyPage, getEntries, getEntry, getMenu, getMenuLinks, getPage, getRobotsResponse, getSite, getSitemapResponse };
|
package/dist/astro.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAceClient, loadMenuLinks, loadPage } from './chunk-
|
|
1
|
+
import { createAceClient, loadMenuLinks, loadPage, sitemapResponse, robotsResponse } from './chunk-RW4CVLES.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';
|
|
@@ -21,5 +21,7 @@ async function getPage(astro) {
|
|
|
21
21
|
applyPage(astro.response, page);
|
|
22
22
|
return page;
|
|
23
23
|
}
|
|
24
|
+
var getSitemapResponse = (astro, options = {}) => sitemapResponse(ace, astro.url, options);
|
|
25
|
+
var getRobotsResponse = (astro, options = {}) => robotsResponse(astro.url, options, ace.siteDomain);
|
|
24
26
|
|
|
25
|
-
export { ace, applyPage, getEntries, getEntry, getMenu, getMenuLinks, getPage, getSite };
|
|
27
|
+
export { ace, applyPage, getEntries, getEntry, getMenu, getMenuLinks, getPage, getRobotsResponse, getSite, getSitemapResponse };
|
|
@@ -67,6 +67,8 @@ function createAceClient({
|
|
|
67
67
|
return value;
|
|
68
68
|
}
|
|
69
69
|
return {
|
|
70
|
+
/** The domain of the site this client reads, as the CMS knows it. */
|
|
71
|
+
siteDomain,
|
|
70
72
|
/**
|
|
71
73
|
* One live entry of a content type, by its handle; null if the site has no such entry. With a preview token the
|
|
72
74
|
* entry the token was made for comes back with its draft applied (never cached); a token that has run out throws
|
|
@@ -98,6 +100,10 @@ function createAceClient({
|
|
|
98
100
|
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
101
|
return goesSomewhereSafe ? redirect : null;
|
|
100
102
|
},
|
|
103
|
+
/** The site's live pages for its sitemap, by path. Pages of content types that have no pages of their own are left out. */
|
|
104
|
+
async getSitemap() {
|
|
105
|
+
return await get("/public/sitemap") ?? [];
|
|
106
|
+
},
|
|
101
107
|
/** The site itself: its name, and the social image its owners chose (null if none). */
|
|
102
108
|
getSite() {
|
|
103
109
|
return get("/public/site");
|
|
@@ -169,4 +175,58 @@ async function notFoundOrRedirect(client, url, previewToken, headers) {
|
|
|
169
175
|
return { kind: "not-found", status: 404, headers };
|
|
170
176
|
}
|
|
171
177
|
|
|
172
|
-
|
|
178
|
+
// src/helpers/sitemap.ts
|
|
179
|
+
var escapeXml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
180
|
+
var isLocalHost = (host) => host === "localhost" || /\.(localhost|test|local)$/.test(host) || /^[\d.]+$|:/.test(host);
|
|
181
|
+
function publicOrigin(url, siteDomain) {
|
|
182
|
+
const domain = siteDomain?.trim().toLowerCase();
|
|
183
|
+
if (domain && !isLocalHost(domain)) return `https://${domain}`;
|
|
184
|
+
return isLocalHost(url.hostname) ? url.origin : `https://${url.host}`;
|
|
185
|
+
}
|
|
186
|
+
function sitemapXml(entries, origin) {
|
|
187
|
+
const base = origin.replace(/\/+$/, "");
|
|
188
|
+
const urls = entries.map((entry) => {
|
|
189
|
+
const lastChanged = Number.isNaN(Date.parse(entry.updated_at)) ? "" : new Date(entry.updated_at).toISOString();
|
|
190
|
+
return ` <url>
|
|
191
|
+
<loc>${escapeXml(`${base}${entry.path}`)}</loc>${lastChanged ? `
|
|
192
|
+
<lastmod>${lastChanged}</lastmod>` : ""}
|
|
193
|
+
</url>`;
|
|
194
|
+
}).join("\n");
|
|
195
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
196
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
197
|
+
${urls}${urls ? "\n" : ""}</urlset>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
function robotsTxt({ origin, disallowAll = false }) {
|
|
201
|
+
if (disallowAll) return "User-agent: *\nDisallow: /\n";
|
|
202
|
+
return `User-agent: *
|
|
203
|
+
Allow: /
|
|
204
|
+
|
|
205
|
+
Sitemap: ${origin.replace(/\/+$/, "")}/sitemap.xml
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
async function sitemapResponse(client, url, options = {}) {
|
|
209
|
+
try {
|
|
210
|
+
const entries = await client.getSitemap();
|
|
211
|
+
return new Response(sitemapXml(entries, options.origin ?? publicOrigin(url, client.siteDomain)), {
|
|
212
|
+
headers: { "Content-Type": "application/xml; charset=utf-8", "Cache-Control": "public, max-age=300" }
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!(error instanceof AceUnavailableError)) throw error;
|
|
216
|
+
console.error(error.message);
|
|
217
|
+
return new Response("The sitemap is unavailable right now.", {
|
|
218
|
+
status: 503,
|
|
219
|
+
headers: { "Content-Type": "text/plain; charset=utf-8", "Retry-After": "300" }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function robotsResponse(url, options = {}, siteDomain) {
|
|
224
|
+
return new Response(
|
|
225
|
+
robotsTxt({ origin: options.origin ?? publicOrigin(url, siteDomain), disallowAll: options.disallowAll }),
|
|
226
|
+
{
|
|
227
|
+
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "public, max-age=3600" }
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export { AcePreviewExpiredError, AceUnavailableError, createAceClient, isCurrentPage, linksFromMenu, loadMenuLinks, loadPage, publicOrigin, robotsResponse, robotsTxt, sitemapResponse, sitemapXml };
|
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-CXO6CVNR.js");
|
|
13
|
+
const { runCli } = await import("./main-4TEJQB6N.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.12.1" : "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
|
@@ -133,12 +133,22 @@ type AceSite = {
|
|
|
133
133
|
favicon: AceFavicon | null;
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
/** One page of the site as the CMS lists it for the sitemap. */
|
|
137
|
+
type AceSitemapEntry = {
|
|
138
|
+
/** The address visitors use: "/" for the home entry, otherwise "/{content type}/{entry}". */
|
|
139
|
+
path: string;
|
|
140
|
+
/** When the entry was last changed, as an ISO date and time. */
|
|
141
|
+
updated_at: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
136
144
|
/**
|
|
137
145
|
* A client for the CMS Ace public API. It runs on the server only, so the visitor's browser never talks to the
|
|
138
146
|
* API. The API finds the site from X-Ace-Site-Domain, which this client sets to the site's domain. X-Forwarded-Host
|
|
139
147
|
* is sent too, for an API that predates the first (some platforms' proxies rewrite X-Forwarded-Host on the way in).
|
|
140
148
|
*/
|
|
141
149
|
declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplementation, now, }: AceClientOptions): {
|
|
150
|
+
/** The domain of the site this client reads, as the CMS knows it. */
|
|
151
|
+
siteDomain: string;
|
|
142
152
|
/**
|
|
143
153
|
* One live entry of a content type, by its handle; null if the site has no such entry. With a preview token the
|
|
144
154
|
* entry the token was made for comes back with its draft applied (never cached); a token that has run out throws
|
|
@@ -158,6 +168,8 @@ declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplem
|
|
|
158
168
|
* `address` is the path with its query string, e.g. "/old-page?id=7".
|
|
159
169
|
*/
|
|
160
170
|
getRedirect(address: string): Promise<AceRedirect | null>;
|
|
171
|
+
/** The site's live pages for its sitemap, by path. Pages of content types that have no pages of their own are left out. */
|
|
172
|
+
getSitemap(): Promise<AceSitemapEntry[]>;
|
|
161
173
|
/** The site itself: its name, and the social image its owners chose (null if none). */
|
|
162
174
|
getSite(): Promise<AceSite | null>;
|
|
163
175
|
/** What the site's content types and fields are, which is what type generation reads. */
|
|
@@ -551,6 +563,36 @@ type AceSeoProperties = {
|
|
|
551
563
|
*/
|
|
552
564
|
declare function seoProperties(options: AceSeoOptions): AceSeoProperties;
|
|
553
565
|
|
|
566
|
+
/**
|
|
567
|
+
* The address the site's pages are at, for the full addresses in a sitemap. It is the site's domain in the CMS
|
|
568
|
+
* (`ACE_SITE_DOMAIN`) over https, because behind a proxy the server only sees its own internal address
|
|
569
|
+
* (`localhost:8080` on Railway), so the request can't say. For a site on `localhost` (or a `.test` domain), which
|
|
570
|
+
* has no public address, it is the address the request came to. Pass `origin` to the response helpers to choose it.
|
|
571
|
+
*/
|
|
572
|
+
declare function publicOrigin(url: URL, siteDomain?: string): string;
|
|
573
|
+
/** The sitemap file for a site: one `<url>` per page, with when it last changed. `origin` is e.g. `https://example.com`. */
|
|
574
|
+
declare function sitemapXml(entries: AceSitemapEntry[], origin: string): string;
|
|
575
|
+
type RobotsOptions = {
|
|
576
|
+
/** Asks every crawler to stay away, for a site that isn't live yet. */
|
|
577
|
+
disallowAll?: boolean;
|
|
578
|
+
/** The site's address, e.g. `https://example.com`. Left out, the address the request came to is used. */
|
|
579
|
+
origin?: string;
|
|
580
|
+
};
|
|
581
|
+
/** The `robots.txt` for a site: everything may be crawled, and the sitemap is pointed at. */
|
|
582
|
+
declare function robotsTxt({ origin, disallowAll }: {
|
|
583
|
+
origin: string;
|
|
584
|
+
disallowAll?: boolean;
|
|
585
|
+
}): string;
|
|
586
|
+
/**
|
|
587
|
+
* The response for `/sitemap.xml`: the site's live pages from the CMS. If the CMS can't be reached it answers 503, so a
|
|
588
|
+
* crawler tries again later instead of reading an empty sitemap.
|
|
589
|
+
*/
|
|
590
|
+
declare function sitemapResponse(client: AceClient, url: URL, options?: {
|
|
591
|
+
origin?: string;
|
|
592
|
+
}): Promise<Response>;
|
|
593
|
+
/** The response for `/robots.txt`. */
|
|
594
|
+
declare function robotsResponse(url: URL, options?: RobotsOptions, siteDomain?: string): Response;
|
|
595
|
+
|
|
554
596
|
/**
|
|
555
597
|
* A text area field's text as HTML. With `multiline` (the default) blank lines start new paragraphs (`<p>`) and a
|
|
556
598
|
* single new line is a line break; without it the text is one line, with no element around it. Every piece of text
|
|
@@ -592,4 +634,4 @@ type AceSection = {
|
|
|
592
634
|
blocks: AceSectionBlock[];
|
|
593
635
|
};
|
|
594
636
|
|
|
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 };
|
|
637
|
+
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, type AceSitemapEntry, AceUnavailableError, type AceValuesOf, type BlockDefinition, type RobotsOptions, 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, publicOrigin, refreshMessageType, refreshPageContent, renderRichText, renderTextArea, richTextImageSizes, robotsResponse, robotsTxt, sectionTypesPayload, seoDescriptionHandle, seoProperties, seoTitleHandle, sitemapResponse, sitemapXml, 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, publicOrigin, robotsResponse, robotsTxt, sitemapResponse, sitemapXml } from './chunk-RW4CVLES.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
|
|
|
@@ -1345,6 +1345,8 @@ function createAceClient({
|
|
|
1345
1345
|
return value;
|
|
1346
1346
|
}
|
|
1347
1347
|
return {
|
|
1348
|
+
/** The domain of the site this client reads, as the CMS knows it. */
|
|
1349
|
+
siteDomain,
|
|
1348
1350
|
/**
|
|
1349
1351
|
* One live entry of a content type, by its handle; null if the site has no such entry. With a preview token the
|
|
1350
1352
|
* entry the token was made for comes back with its draft applied (never cached); a token that has run out throws
|
|
@@ -1376,6 +1378,10 @@ function createAceClient({
|
|
|
1376
1378
|
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
1379
|
return goesSomewhereSafe ? redirect : null;
|
|
1378
1380
|
},
|
|
1381
|
+
/** The site's live pages for its sitemap, by path. Pages of content types that have no pages of their own are left out. */
|
|
1382
|
+
async getSitemap() {
|
|
1383
|
+
return await get("/public/sitemap") ?? [];
|
|
1384
|
+
},
|
|
1379
1385
|
/** The site itself: its name, and the social image its owners chose (null if none). */
|
|
1380
1386
|
getSite() {
|
|
1381
1387
|
return get("/public/site");
|
|
@@ -136,12 +136,22 @@ type AceRedirect = {
|
|
|
136
136
|
status: 301 | 302;
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
+
/** One page of the site as the CMS lists it for the sitemap. */
|
|
140
|
+
type AceSitemapEntry = {
|
|
141
|
+
/** The address visitors use: "/" for the home entry, otherwise "/{content type}/{entry}". */
|
|
142
|
+
path: string;
|
|
143
|
+
/** When the entry was last changed, as an ISO date and time. */
|
|
144
|
+
updated_at: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
139
147
|
/**
|
|
140
148
|
* A client for the CMS Ace public API. It runs on the server only, so the visitor's browser never talks to the
|
|
141
149
|
* API. The API finds the site from X-Ace-Site-Domain, which this client sets to the site's domain. X-Forwarded-Host
|
|
142
150
|
* is sent too, for an API that predates the first (some platforms' proxies rewrite X-Forwarded-Host on the way in).
|
|
143
151
|
*/
|
|
144
152
|
declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplementation, now, }: AceClientOptions): {
|
|
153
|
+
/** The domain of the site this client reads, as the CMS knows it. */
|
|
154
|
+
siteDomain: string;
|
|
145
155
|
/**
|
|
146
156
|
* One live entry of a content type, by its handle; null if the site has no such entry. With a preview token the
|
|
147
157
|
* entry the token was made for comes back with its draft applied (never cached); a token that has run out throws
|
|
@@ -161,6 +171,8 @@ declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplem
|
|
|
161
171
|
* `address` is the path with its query string, e.g. "/old-page?id=7".
|
|
162
172
|
*/
|
|
163
173
|
getRedirect(address: string): Promise<AceRedirect | null>;
|
|
174
|
+
/** The site's live pages for its sitemap, by path. Pages of content types that have no pages of their own are left out. */
|
|
175
|
+
getSitemap(): Promise<AceSitemapEntry[]>;
|
|
164
176
|
/** The site itself: its name, and the social image its owners chose (null if none). */
|
|
165
177
|
getSite(): Promise<AceSite | null>;
|
|
166
178
|
/** What the site's content types and fields are, which is what type generation reads. */
|
|
@@ -169,6 +181,13 @@ declare function createAceClient({ apiUrl, siteDomain, cacheSeconds, fetchImplem
|
|
|
169
181
|
/** The client `createAceClient` returns. */
|
|
170
182
|
type AceClient = ReturnType<typeof createAceClient>;
|
|
171
183
|
|
|
184
|
+
type RobotsOptions = {
|
|
185
|
+
/** Asks every crawler to stay away, for a site that isn't live yet. */
|
|
186
|
+
disallowAll?: boolean;
|
|
187
|
+
/** The site's address, e.g. `https://example.com`. Left out, the address the request came to is used. */
|
|
188
|
+
origin?: string;
|
|
189
|
+
};
|
|
190
|
+
|
|
172
191
|
/** What the root loader (`createRootLoader`) puts in the root route's data, for `useAceMenu` and `useAcePreview`. */
|
|
173
192
|
type AceRootData = {
|
|
174
193
|
ace: {
|
|
@@ -229,6 +248,31 @@ declare function createPageLoader<Values = Record<string, unknown>>(): ({ reques
|
|
|
229
248
|
entry: AceEntry<Values>;
|
|
230
249
|
headers: Record<string, string>;
|
|
231
250
|
}>>;
|
|
251
|
+
/**
|
|
252
|
+
* The loader for `/sitemap.xml`: the site's live pages (of the content types that have pages), with when each last
|
|
253
|
+
* changed. Add a route `route("sitemap.xml", "routes/sitemap.tsx")` (before the catch-all) whose file is
|
|
254
|
+
*
|
|
255
|
+
* export { sitemapLoader as loader } from "@sandboxsonny/ace-kit/react-router/server";
|
|
256
|
+
*
|
|
257
|
+
* It uses the site's domain (`ACE_SITE_DOMAIN`) over https, or the address of the request for a site on localhost; for another address use `createSitemapLoader({ origin })`.
|
|
258
|
+
*/
|
|
259
|
+
declare const createSitemapLoader: (options?: {
|
|
260
|
+
origin?: string;
|
|
261
|
+
}) => ({ request }: {
|
|
262
|
+
request: Request;
|
|
263
|
+
}) => Promise<Response>;
|
|
264
|
+
/** `createSitemapLoader()` with the defaults. */
|
|
265
|
+
declare const sitemapLoader: ({ request }: {
|
|
266
|
+
request: Request;
|
|
267
|
+
}) => Promise<Response>;
|
|
268
|
+
/** The loader for `/robots.txt`, which lets crawlers in and points them at the sitemap. See `createSitemapLoader`. */
|
|
269
|
+
declare const createRobotsLoader: (options?: RobotsOptions) => ({ request }: {
|
|
270
|
+
request: Request;
|
|
271
|
+
}) => Response;
|
|
272
|
+
/** `createRobotsLoader()` with the defaults. */
|
|
273
|
+
declare const robotsLoader: ({ request }: {
|
|
274
|
+
request: Request;
|
|
275
|
+
}) => Response;
|
|
232
276
|
/** `createPageLoader()` for a site that has not generated types for its content. */
|
|
233
277
|
declare const loader: ({ request }: {
|
|
234
278
|
request: Request;
|
|
@@ -281,4 +325,4 @@ declare const streamTimeout = 5000;
|
|
|
281
325
|
*/
|
|
282
326
|
declare function handleRequest(request: Request, responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, _loadContext: AppLoadContext): Promise<Response>;
|
|
283
327
|
|
|
284
|
-
export { createPageLoader, createRootLoader, getAceClient, getEntries, getEntry, getMenu, getMenuLinks, getSite, handleRequest, headers, loader, streamTimeout };
|
|
328
|
+
export { createPageLoader, createRobotsLoader, createRootLoader, createSitemapLoader, getAceClient, getEntries, getEntry, getMenu, getMenuLinks, getSite, handleRequest, headers, loader, robotsLoader, sitemapLoader, streamTimeout };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAceClient, loadMenuLinks, loadPage } from './chunk-
|
|
1
|
+
import { createAceClient, loadMenuLinks, loadPage, sitemapResponse, robotsResponse } from './chunk-RW4CVLES.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';
|
|
@@ -43,6 +43,14 @@ function createPageLoader() {
|
|
|
43
43
|
return data(page, { status: page.status, headers: page.headers });
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
var createSitemapLoader = (options = {}) => function loader2({ request }) {
|
|
47
|
+
return sitemapResponse(getAceClient(), new URL(request.url), options);
|
|
48
|
+
};
|
|
49
|
+
var sitemapLoader = createSitemapLoader();
|
|
50
|
+
var createRobotsLoader = (options = {}) => function loader2({ request }) {
|
|
51
|
+
return robotsResponse(new URL(request.url), options, getAceClient().siteDomain);
|
|
52
|
+
};
|
|
53
|
+
var robotsLoader = createRobotsLoader();
|
|
46
54
|
var loader = createPageLoader();
|
|
47
55
|
var headers = ({ loaderHeaders }) => loaderHeaders;
|
|
48
56
|
function createRootLoader({ menu, fallback = [] }) {
|
|
@@ -88,4 +96,4 @@ function handleRequest(request, responseStatusCode, responseHeaders, routerConte
|
|
|
88
96
|
});
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
export { createPageLoader, createRootLoader, getAceClient, getEntries, getEntry, getMenu, getMenuLinks, getSite, handleRequest, headers, loader, streamTimeout };
|
|
99
|
+
export { createPageLoader, createRobotsLoader, createRootLoader, createSitemapLoader, getAceClient, getEntries, getEntry, getMenu, getMenuLinks, getSite, handleRequest, headers, loader, robotsLoader, sitemapLoader, streamTimeout };
|
package/docs/api-reference.md
CHANGED
|
@@ -86,13 +86,14 @@ Types: `AceSection`, `AceSectionBlock`, `SectionDefinition`, `SectionFieldDefini
|
|
|
86
86
|
|
|
87
87
|
`@sandboxsonny/ace-kit/astro` (uses the integration's settings, so there is no client to create):
|
|
88
88
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
| `getPage(Astro)`
|
|
92
|
-
| `getEntry(contentType, handle)`, `getEntries(contentType, { limit? })`, `getMenu(handle)`
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
89
|
+
| | |
|
|
90
|
+
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `getPage(Astro)` | What to show for the page being requested; also sets the response's status and headers. A `redirect` is returned for you to act on with `Astro.redirect`. |
|
|
92
|
+
| `getEntry(contentType, handle)`, `getEntries(contentType, { limit? })`, `getMenu(handle)` | As on the client. |
|
|
93
|
+
| `getSitemapResponse({ url }, { origin? })`, `getRobotsResponse({ url }, { origin?, disallowAll? })` | The responses for `/sitemap.xml` and `/robots.txt` (see [Astro](./astro.md)). |
|
|
94
|
+
| `getMenuLinks(handle, { fallback? })` | A menu's links, or the fallback if the menu is missing or the CMS is down. |
|
|
95
|
+
| `applyPage(response, page)` | Puts a page's status and headers on a response. |
|
|
96
|
+
| `ace` | The client these use. |
|
|
96
97
|
|
|
97
98
|
Components, by file: `AceImage.astro`, `AceLink.astro`, `AceSections.astro`, `AceRichText.astro`, `AceTextArea.astro`, `AceMenu.astro`, `AceNotice.astro`, `AcePreviewRefresh.astro`.
|
|
98
99
|
|
|
@@ -110,13 +111,14 @@ Components, by file: `AceImage.astro`, `AceLink.astro`, `AceSections.astro`, `Ac
|
|
|
110
111
|
|
|
111
112
|
`@sandboxsonny/ace-kit/react-router/server` (server only):
|
|
112
113
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
| `createPageLoader<Values>()`, `loader`
|
|
116
|
-
| `headers`
|
|
117
|
-
| `createRootLoader({ menu, fallback? })`
|
|
118
|
-
| `handleRequest`, `streamTimeout`
|
|
119
|
-
| `
|
|
114
|
+
| | |
|
|
115
|
+
| ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
116
|
+
| `createPageLoader<Values>()`, `loader` | The loader for the catch-all route that draws every page. |
|
|
117
|
+
| `headers` | The route's `headers` export. |
|
|
118
|
+
| `createRootLoader({ menu, fallback? })` | The root loader: the menu's links and whether this is a preview. |
|
|
119
|
+
| `handleRequest`, `streamTimeout` | The server entry. |
|
|
120
|
+
| `sitemapLoader`, `robotsLoader`, `createSitemapLoader(options)`, `createRobotsLoader(options)` | The loaders for `/sitemap.xml` and `/robots.txt` (see [React Router](./react-router.md)). |
|
|
121
|
+
| `getEntry`, `getEntries`, `getMenu`, `getMenuLinks`, `getAceClient` | Read content from your own loaders. |
|
|
120
122
|
|
|
121
123
|
## React (`@sandboxsonny/ace-kit/react`)
|
|
122
124
|
|
package/docs/astro.md
CHANGED
|
@@ -48,6 +48,24 @@ if (page.kind === "redirect") return Astro.redirect(page.location, page.status);
|
|
|
48
48
|
|
|
49
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
|
+
## Sitemap and robots.txt
|
|
52
|
+
|
|
53
|
+
Search engines find a site's pages through `/sitemap.xml` and are pointed at it by `/robots.txt`. Both are one small file each; the sitemap lists the live pages from the CMS (only the content types that have "Entries have their own pages" ticked), with when each last changed.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// src/pages/sitemap.xml.ts
|
|
57
|
+
import { getSitemapResponse } from "@sandboxsonny/ace-kit/astro";
|
|
58
|
+
export const GET = ({ url }) => getSitemapResponse({ url });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/pages/robots.txt.ts
|
|
63
|
+
import { getRobotsResponse } from "@sandboxsonny/ace-kit/astro";
|
|
64
|
+
export const GET = ({ url }) => getRobotsResponse({ url });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Addresses in the sitemap are your site's domain (`ACE_SITE_DOMAIN`) over `https`, or the address of the request when the site is on `localhost`; pass `{ origin: "https://www.example.com" }` to either to choose. `getRobotsResponse({ url }, { disallowAll: true })` asks crawlers to stay away, for a site that isn't live yet. Delete any `robots.txt` in `public/`, which would take its place.
|
|
68
|
+
|
|
51
69
|
## Reading content
|
|
52
70
|
|
|
53
71
|
Anywhere in a page or component:
|
package/docs/react-router.md
CHANGED
|
@@ -102,6 +102,22 @@ From `@sandboxsonny/ace-kit/react-router` (these are safe in the browser):
|
|
|
102
102
|
- **AceImage** is an `<img>` with `src`, `srcset`, `sizes`, size and alt text. Set `sizes` to how wide it is shown, and `lazy={false}` for the image at the top of the page.
|
|
103
103
|
- **AceNotice** is the heading and message for a page that isn't an entry. Give it a `className`.
|
|
104
104
|
|
|
105
|
+
## Sitemap and robots.txt
|
|
106
|
+
|
|
107
|
+
Search engines find a site's pages through `/sitemap.xml` and are pointed at it by `/robots.txt`. Add two routes before the catch-all in `app/routes.ts`: `route("sitemap.xml", "routes/sitemap.tsx")` and `route("robots.txt", "routes/robots.tsx")`, whose files are one line each:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// app/routes/sitemap.tsx
|
|
111
|
+
export { sitemapLoader as loader } from "@sandboxsonny/ace-kit/react-router/server";
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// app/routes/robots.tsx
|
|
116
|
+
export { robotsLoader as loader } from "@sandboxsonny/ace-kit/react-router/server";
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The sitemap lists the live pages from the CMS (only the content types that have "Entries have their own pages" ticked), with when each last changed. Addresses are your site's domain (`ACE_SITE_DOMAIN`) over `https`, or the address of the request when the site is on `localhost`. For another address, or to keep crawlers away from a site that isn't live yet, use `createSitemapLoader({ origin })` and `createRobotsLoader({ origin, disallowAll })`.
|
|
120
|
+
|
|
105
121
|
## Reading content in your own loaders
|
|
106
122
|
|
|
107
123
|
From `@sandboxsonny/ace-kit/react-router/server`, with no client to create:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandboxsonny/ace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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",
|