dh-remixer-sdk 0.0.45-fa836ab → 0.0.45
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/package.json +1 -1
- package/scripts/ssg-helmet/no-settings.test.mjs +21 -6
- package/scripts/ssg-helmet/robots-txt.mjs +2 -2
- package/scripts/ssg-helmet/seo-only-files.test.mjs +67 -0
- package/scripts/ssg-helmet/seo-settings.mjs +1 -3
- package/scripts/ssg-helmet/seo-settings.test.mjs +8 -0
- package/scripts/ssg-helmet/site-files.mjs +10 -1
- package/scripts/ssg-helmet/site-files.test.mjs +20 -3
- package/scripts/ssg-helmet/site-wide-canonical.test.mjs +8 -1
- package/templates/base/remixer/core.ts +17 -6
- package/templates/base/remixer/dpop.ts +2 -1
- package/templates/base/remixer/seo.tsx +17 -1
package/package.json
CHANGED
|
@@ -26,7 +26,11 @@ const DEVELOP_LD_JSON = (name, description) => ({
|
|
|
26
26
|
sameAs: [],
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
const ROUTE_TABLE = { routerFound: true, complete: true, hasCatchAll: true, patterns: ["/", "/about"] };
|
|
30
|
+
const notFoundPrerenders = [];
|
|
31
|
+
|
|
29
32
|
const read = (file) => fs.readFileSync(path.join(BUILD_DIR, file), "utf8");
|
|
33
|
+
const exists = (file) => fs.existsSync(path.join(BUILD_DIR, file));
|
|
30
34
|
const ldJson = (html) => JSON.parse(html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/)[1]);
|
|
31
35
|
let resolveRobotsTxt;
|
|
32
36
|
let buildSitemapXml;
|
|
@@ -45,7 +49,7 @@ before(async () => {
|
|
|
45
49
|
const { markStaticHead } = await import("./inject-metadata-head.mjs");
|
|
46
50
|
const { writeHelmetFile } = await import("./fs-utils.mjs");
|
|
47
51
|
const { parseHtmlForContactInfo, applyBusinessLdJson } = await import("./parse-html-for-contact-info.mjs");
|
|
48
|
-
const { writeSiteFiles } = await import("./site-files.mjs");
|
|
52
|
+
const { writeNotFoundPageInTime, writeSiteFiles } = await import("./site-files.mjs");
|
|
49
53
|
({ resolveRobotsTxt } = await import("./robots-txt.mjs"));
|
|
50
54
|
({ buildSitemapXml } = await import("./build-sitemap-xml.mjs"));
|
|
51
55
|
|
|
@@ -67,7 +71,11 @@ before(async () => {
|
|
|
67
71
|
await parseHtmlForContactInfo(html);
|
|
68
72
|
}
|
|
69
73
|
await applyBusinessLdJson();
|
|
70
|
-
await writeSiteFiles({ sitemapRoutes: ["/", "/about"], orphanRoutes: [], routeTable:
|
|
74
|
+
await writeSiteFiles({ sitemapRoutes: ["/", "/about"], orphanRoutes: [], routeTable: ROUTE_TABLE });
|
|
75
|
+
await writeNotFoundPageInTime({
|
|
76
|
+
routeTable: ROUTE_TABLE,
|
|
77
|
+
prerender: async (route) => (notFoundPrerenders.push(route), SPA_INDEX),
|
|
78
|
+
});
|
|
71
79
|
});
|
|
72
80
|
|
|
73
81
|
test("without a settings file the JSON-LD is develop's minus the invented price range, with no FAQPage", () => {
|
|
@@ -89,9 +97,16 @@ test("without a settings file the only head changes are the rebased loopback can
|
|
|
89
97
|
assert.match(about, /<!-- remixer:integrations:head --><meta name="google-site-verification" content="abc" \/><!-- \/remixer:integrations:head -->/);
|
|
90
98
|
});
|
|
91
99
|
|
|
92
|
-
test("without a settings file the site's own llms.txt is kept and
|
|
100
|
+
test("without a settings file the site's own llms.txt is kept and no llms additions are written", () => {
|
|
93
101
|
assert.equal(read("llms.txt"), "# Wilton House\n\n> Curated by the site.\n");
|
|
94
|
-
assert.
|
|
95
|
-
assert.
|
|
96
|
-
assert.
|
|
102
|
+
assert.equal(exists("llms-full.txt"), false);
|
|
103
|
+
assert.equal(exists("index.md"), false);
|
|
104
|
+
assert.equal(exists("about.md"), false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("without a settings file there is no 404.html, _routes.json or seo-review.json, and no not-found prerender", () => {
|
|
108
|
+
assert.equal(exists("404.html"), false);
|
|
109
|
+
assert.equal(exists("_routes.json"), false);
|
|
110
|
+
assert.equal(exists("seo-review.json"), false);
|
|
111
|
+
assert.deepEqual(notFoundPrerenders, []);
|
|
97
112
|
});
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { SEO_SETTINGS } from "./vars.mjs";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
hasSeoSettings,
|
|
6
6
|
isAccessAllowed,
|
|
7
7
|
isPrivateSite,
|
|
8
8
|
siteOrigin,
|
|
@@ -63,7 +63,7 @@ export function presetRobotsTxt(settings, origin) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function resolveRobotsTxt(existing, targetDomain, settings = {}) {
|
|
66
|
-
if (
|
|
66
|
+
if (hasSeoSettings(settings) || isPrivateSite(settings)) {
|
|
67
67
|
return presetRobotsTxt(settings, siteOrigin(settings, targetDomain));
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const BUILD_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ssg-seo-only-files-"));
|
|
8
|
+
process.env.REMIXER_BUILD_DIR = BUILD_DIR;
|
|
9
|
+
process.env.NEXT_PUBLIC_DOMAIN = "wiltonhouse.com";
|
|
10
|
+
delete process.env.REMIXER_SEO_SETTINGS;
|
|
11
|
+
delete process.env.GEO_CODER_API_KEY;
|
|
12
|
+
|
|
13
|
+
const { writeNotFoundPageInTime, writeSiteFiles } = await import("./site-files.mjs");
|
|
14
|
+
|
|
15
|
+
const SEO_ONLY_FILES = ["llms.txt", "llms-full.txt", "index.md", "about.md", "404.html", "_routes.json", "seo-review.json"];
|
|
16
|
+
const ROUTE_TABLE = { routerFound: true, complete: true, hasCatchAll: false, patterns: ["/", "/about"] };
|
|
17
|
+
|
|
18
|
+
const ALL_DEFAULT_SETTINGS = {
|
|
19
|
+
siteUrl: "https://wiltonhouse.com",
|
|
20
|
+
access: { search: true, assistants: true, training: true, llmsTxt: true, indexNow: true },
|
|
21
|
+
business: {
|
|
22
|
+
name: null,
|
|
23
|
+
type: null,
|
|
24
|
+
address: null,
|
|
25
|
+
phone: null,
|
|
26
|
+
email: null,
|
|
27
|
+
hours: [],
|
|
28
|
+
hoursNote: null,
|
|
29
|
+
priceRange: null,
|
|
30
|
+
serviceArea: null,
|
|
31
|
+
links: [],
|
|
32
|
+
siteIconUrl: null,
|
|
33
|
+
},
|
|
34
|
+
pages: {},
|
|
35
|
+
redirects: [],
|
|
36
|
+
private: false,
|
|
37
|
+
screenshots: true,
|
|
38
|
+
};
|
|
39
|
+
const PLATFORM_HOST_MARKER = { siteUrl: "https://wilton.example-cdn.com", private: true };
|
|
40
|
+
|
|
41
|
+
const LONG = "Honest cocktails and small plates in a warm room on Main Street. ".repeat(3);
|
|
42
|
+
const page = (title) =>
|
|
43
|
+
`<!doctype html><html lang="en"><head><title>${title}</title><meta name="description" content="${title} at Wilton House."></head><body><div id="root"><main><h1>${title}</h1><p>${LONG}</p></main></div></body></html>`;
|
|
44
|
+
|
|
45
|
+
async function seoOnlyFilesBuiltWith(settings) {
|
|
46
|
+
fs.rmSync(BUILD_DIR, { recursive: true, force: true });
|
|
47
|
+
fs.mkdirSync(path.join(BUILD_DIR, "about"), { recursive: true });
|
|
48
|
+
fs.writeFileSync(path.join(BUILD_DIR, "index.html"), page("Wilton House"));
|
|
49
|
+
fs.writeFileSync(path.join(BUILD_DIR, "about", "index.html"), page("About"));
|
|
50
|
+
|
|
51
|
+
await writeSiteFiles({ sitemapRoutes: ["/", "/about"], orphanRoutes: [], routeTable: ROUTE_TABLE }, settings);
|
|
52
|
+
await writeNotFoundPageInTime({ routeTable: ROUTE_TABLE, prerender: async () => null }, settings);
|
|
53
|
+
|
|
54
|
+
return SEO_ONLY_FILES.filter((file) => fs.existsSync(path.join(BUILD_DIR, file)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("an activated project with every setting at its default gets every SEO-only file", async () => {
|
|
58
|
+
assert.deepEqual(await seoOnlyFilesBuiltWith(ALL_DEFAULT_SETTINGS), SEO_ONLY_FILES);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the platform-host build of a project that never switched SEO on gets no SEO-only file", async () => {
|
|
62
|
+
assert.deepEqual(await seoOnlyFilesBuiltWith(PLATFORM_HOST_MARKER), []);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("an empty settings object gets no SEO-only file", async () => {
|
|
66
|
+
assert.deepEqual(await seoOnlyFilesBuiltWith({}), []);
|
|
67
|
+
});
|
|
@@ -51,7 +51,7 @@ export function pageSettings(settings, route) {
|
|
|
51
51
|
return key && isPlainObject(pages[key]) ? pages[key] : undefined;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export const hasSeoSettings = (settings) => isPlainObject(settings)
|
|
54
|
+
export const hasSeoSettings = (settings) => isPlainObject(settings?.access);
|
|
55
55
|
|
|
56
56
|
export const isPrivateSite = (settings) => settings?.private === true;
|
|
57
57
|
|
|
@@ -61,8 +61,6 @@ export const isRouteHidden = (settings, route) =>
|
|
|
61
61
|
export const isRouteIndexable = (settings, route) =>
|
|
62
62
|
!isPrivateSite(settings) && !isRouteHidden(settings, route);
|
|
63
63
|
|
|
64
|
-
export const hasAccessSettings = (settings) => isPlainObject(settings?.access);
|
|
65
|
-
|
|
66
64
|
export const isAccessAllowed = (settings, key) =>
|
|
67
65
|
settings?.access?.[key] !== false;
|
|
68
66
|
|
|
@@ -5,6 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import {
|
|
7
7
|
flattenRoutes,
|
|
8
|
+
hasSeoSettings,
|
|
8
9
|
isAccessAllowed,
|
|
9
10
|
isRouteIndexable,
|
|
10
11
|
normalizeRoutePath,
|
|
@@ -36,6 +37,13 @@ test("a valid settings file is read as is", () => {
|
|
|
36
37
|
assert.deepEqual(readSeoSettings(writeTemp(JSON.stringify(settings))), settings);
|
|
37
38
|
});
|
|
38
39
|
|
|
40
|
+
test("only the settings of a project that switched SEO on count as SEO settings", () => {
|
|
41
|
+
assert.equal(hasSeoSettings({}), false);
|
|
42
|
+
assert.equal(hasSeoSettings({ siteUrl: "https://wilton.example-cdn.com", private: true }), false);
|
|
43
|
+
assert.equal(hasSeoSettings({ siteUrl: "https://wiltonhouse.com", access: { search: true }, private: false }), true);
|
|
44
|
+
assert.equal(hasSeoSettings({ siteUrl: "https://wilton.example-cdn.com", access: { search: true }, private: true }), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
39
47
|
test("routes are normalised on both sides of the page lookup", () => {
|
|
40
48
|
const settings = { pages: { "/about/": { title: "About" }, "/": { title: "Home" } } };
|
|
41
49
|
|
|
@@ -16,6 +16,7 @@ import { STATIC_HEAD_ATTR } from "./inject-metadata-head.mjs";
|
|
|
16
16
|
import { OPTIONAL_WORK_RESERVE_MS, RUN_CLOCK } from "./run-clock.mjs";
|
|
17
17
|
import {
|
|
18
18
|
flattenRoutes,
|
|
19
|
+
hasSeoSettings,
|
|
19
20
|
isAccessAllowed,
|
|
20
21
|
isPrivateSite,
|
|
21
22
|
normalizeRoutePath,
|
|
@@ -150,6 +151,10 @@ export async function writeNotFoundPageInTime(
|
|
|
150
151
|
{ routeTable, prerender, clock = RUN_CLOCK },
|
|
151
152
|
settings = SEO_SETTINGS,
|
|
152
153
|
) {
|
|
154
|
+
if (!hasSeoSettings(settings)) {
|
|
155
|
+
console.log("[skipped] 404.html (SEO not switched on)");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
153
158
|
let prerendered = null;
|
|
154
159
|
if (routeTable?.hasCatchAll && clock.hasTimeFor(OPTIONAL_WORK_RESERVE_MS)) {
|
|
155
160
|
prerendered = await prerender(NOT_FOUND_PROBE_ROUTE).catch((err) => {
|
|
@@ -169,8 +174,12 @@ export async function writeSiteFiles(
|
|
|
169
174
|
await writeSitemapXml(sitemapRoutes, settings);
|
|
170
175
|
await writeRobotsTxt(REMIXER_BUILD_DIR, TARGET_DOMAIN, settings);
|
|
171
176
|
await optionalStep("_redirects.json", () => writeRedirectsJson(settings));
|
|
172
|
-
await optionalStep("_routes.json", () => writeRoutesJson(routeTable));
|
|
173
177
|
await optionalStep("IndexNow key", () => writeIndexNowKeyFile(settings));
|
|
178
|
+
if (!hasSeoSettings(settings)) {
|
|
179
|
+
console.log(`[skipped] _routes.json, llms files and ${REVIEW_FILE} (SEO not switched on)`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await optionalStep("_routes.json", () => writeRoutesJson(routeTable));
|
|
174
183
|
await optionalStep("llms.txt", () => writeLlmsFiles(flattenRoutes(sitemapRoutes), settings));
|
|
175
184
|
await optionalStep(REVIEW_FILE, () => writeSeoReview({ sitemapRoutes, orphanRoutes }, settings));
|
|
176
185
|
}
|
|
@@ -118,13 +118,29 @@ test("the IndexNow key file is written only when IndexNow is on and the key is w
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
const PRERENDERED_NOT_FOUND = `<html lang="en"><head><title>Not found | Site</title></head><body><h1>We could not find that page</h1></body></html>`;
|
|
121
|
+
const SWITCHED_ON = { access: { search: true, assistants: true, training: true, llmsTxt: true, indexNow: true } };
|
|
121
122
|
const clockAt = (elapsedMs) => createRunClock({ now: () => elapsedMs });
|
|
122
123
|
|
|
124
|
+
test("without SEO settings the not-found page is neither prerendered nor written", async () => {
|
|
125
|
+
fs.rmSync(path.join(BUILD_DIR, "404.html"), { force: true });
|
|
126
|
+
const rendered = [];
|
|
127
|
+
|
|
128
|
+
for (const settings of [{}, { siteUrl: "https://wilton.example-cdn.com", private: true }]) {
|
|
129
|
+
await writeNotFoundPageInTime(
|
|
130
|
+
{ routeTable: { hasCatchAll: true }, prerender: async (route) => (rendered.push(route), PRERENDERED_NOT_FOUND), clock: clockAt(0) },
|
|
131
|
+
settings,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(rendered, []);
|
|
136
|
+
assert.equal(existsInBuild("404.html"), false);
|
|
137
|
+
});
|
|
138
|
+
|
|
123
139
|
test("the not-found page is prerendered when the catch-all exists and enough time remains", async () => {
|
|
124
140
|
const rendered = [];
|
|
125
141
|
await writeNotFoundPageInTime(
|
|
126
142
|
{ routeTable: { hasCatchAll: true }, prerender: async (route) => (rendered.push(route), PRERENDERED_NOT_FOUND), clock: clockAt(300_000) },
|
|
127
|
-
|
|
143
|
+
SWITCHED_ON,
|
|
128
144
|
);
|
|
129
145
|
|
|
130
146
|
assert.deepEqual(rendered, [NOT_FOUND_PROBE_ROUTE]);
|
|
@@ -135,7 +151,7 @@ test("the not-found page falls back to the plain page when less than 120 seconds
|
|
|
135
151
|
const rendered = [];
|
|
136
152
|
await writeNotFoundPageInTime(
|
|
137
153
|
{ routeTable: { hasCatchAll: true }, prerender: async (route) => (rendered.push(route), PRERENDERED_NOT_FOUND), clock: clockAt(810_000 - 119_000) },
|
|
138
|
-
|
|
154
|
+
SWITCHED_ON,
|
|
139
155
|
);
|
|
140
156
|
|
|
141
157
|
assert.deepEqual(rendered, []);
|
|
@@ -145,7 +161,8 @@ test("the not-found page falls back to the plain page when less than 120 seconds
|
|
|
145
161
|
|
|
146
162
|
test("without a catch-all the plain page is written and nothing is prerendered", async () => {
|
|
147
163
|
const rendered = [];
|
|
148
|
-
|
|
164
|
+
fs.rmSync(path.join(BUILD_DIR, "404.html"), { force: true });
|
|
165
|
+
await writeNotFoundPageInTime({ routeTable: { hasCatchAll: false }, prerender: async (route) => rendered.push(route), clock: clockAt(0) }, SWITCHED_ON);
|
|
149
166
|
|
|
150
167
|
assert.deepEqual(rendered, []);
|
|
151
168
|
assert.match(readBuild("404.html"), /<h1>Page not found<\/h1>/);
|
|
@@ -38,7 +38,14 @@ const read = (file) => fs.readFileSync(path.join(BUILD_DIR, file), "utf8");
|
|
|
38
38
|
before(async () => {
|
|
39
39
|
fs.mkdirSync(path.join(BUILD_DIR, "assets"), { recursive: true });
|
|
40
40
|
fs.writeFileSync(path.join(BUILD_DIR, "index.html"), SPA_INDEX);
|
|
41
|
-
fs.writeFileSync(
|
|
41
|
+
fs.writeFileSync(
|
|
42
|
+
SETTINGS_FILE,
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
siteUrl: SITE_URL,
|
|
45
|
+
access: { search: true, assistants: true, training: true, llmsTxt: true, indexNow: true },
|
|
46
|
+
private: false,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
42
49
|
|
|
43
50
|
process.env.REMIXER_BUILD_DIR = BUILD_DIR;
|
|
44
51
|
process.env.REMIXER_SEO_SETTINGS = SETTINGS_FILE;
|
|
@@ -11,7 +11,7 @@ type RuntimeAuthHeaders = {
|
|
|
11
11
|
tokenSource: 'anonymous' | 'session';
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
async function runtimeAuthHeaders(url: string, method: string): Promise<RuntimeAuthHeaders> {
|
|
14
|
+
async function runtimeAuthHeaders(url: string, method: string, nonce?: string): Promise<RuntimeAuthHeaders> {
|
|
15
15
|
const session = await getProjectSession();
|
|
16
16
|
const tokenSource = session?.access_token ? 'session' : 'anonymous';
|
|
17
17
|
const token = session?.access_token ?? await getAnonymousToken();
|
|
@@ -19,7 +19,7 @@ async function runtimeAuthHeaders(url: string, method: string): Promise<RuntimeA
|
|
|
19
19
|
return {
|
|
20
20
|
headers: {
|
|
21
21
|
Authorization: `Bearer ${token}`,
|
|
22
|
-
DPoP: await createDpopProof(url, method),
|
|
22
|
+
DPoP: await createDpopProof(url, method, nonce),
|
|
23
23
|
},
|
|
24
24
|
tokenSource,
|
|
25
25
|
};
|
|
@@ -34,8 +34,9 @@ export async function runtimeJsonFetch(
|
|
|
34
34
|
method: 'GET' | 'POST',
|
|
35
35
|
body?: RemixerRecordData,
|
|
36
36
|
retryOnUnauthorized = true,
|
|
37
|
+
dpopNonce?: string,
|
|
37
38
|
) {
|
|
38
|
-
const { headers, tokenSource } = await runtimeAuthHeaders(url, method);
|
|
39
|
+
const { headers, tokenSource } = await runtimeAuthHeaders(url, method, dpopNonce);
|
|
39
40
|
const response = await fetch(url, {
|
|
40
41
|
method,
|
|
41
42
|
headers: {
|
|
@@ -45,9 +46,19 @@ export async function runtimeJsonFetch(
|
|
|
45
46
|
body: body ? JSON.stringify(body) : undefined,
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
if (response.status === 401
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
if (response.status === 401) {
|
|
50
|
+
// RFC 9449 §9: the server challenges an expired/skewed proof with a DPoP-Nonce. Echo it back
|
|
51
|
+
// in a re-signed proof and retry once — the client's clock error becomes irrelevant. Gated on
|
|
52
|
+
// `!dpopNonce` so this never loops: a retry that already carried a nonce and still 401'd falls
|
|
53
|
+
// through to the token-retry path below instead.
|
|
54
|
+
const challengeNonce = response.headers.get('DPoP-Nonce');
|
|
55
|
+
if (challengeNonce && !dpopNonce) {
|
|
56
|
+
return runtimeJsonFetch(url, method, body, retryOnUnauthorized, challengeNonce);
|
|
57
|
+
}
|
|
58
|
+
if (retryOnUnauthorized && tokenSource === 'anonymous') {
|
|
59
|
+
clearAnonymousToken();
|
|
60
|
+
return runtimeJsonFetch(url, method, body, false);
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
return response;
|
|
@@ -104,7 +104,7 @@ export async function getThumbprint() {
|
|
|
104
104
|
return cachedThumbprint;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
export async function createDpopProof(url: string, method: string) {
|
|
107
|
+
export async function createDpopProof(url: string, method: string, nonce?: string) {
|
|
108
108
|
const { privateKey } = await getDpopKeyPair();
|
|
109
109
|
const publicJwk = await getPublicJwk();
|
|
110
110
|
const header = {
|
|
@@ -117,6 +117,7 @@ export async function createDpopProof(url: string, method: string) {
|
|
|
117
117
|
htu: url,
|
|
118
118
|
iat: Math.floor(Date.now() / 1000),
|
|
119
119
|
jti: crypto.randomUUID(),
|
|
120
|
+
...(nonce ? { nonce } : {}),
|
|
120
121
|
};
|
|
121
122
|
const signingInput = `${base64Url(new TextEncoder().encode(JSON.stringify(header)))}.${base64Url(new TextEncoder().encode(JSON.stringify(payload)))}`;
|
|
122
123
|
const signature = await crypto.subtle.sign(
|
|
@@ -113,7 +113,15 @@ function findPage(pages: Record<string, RemixerSeoPage> | undefined, path: strin
|
|
|
113
113
|
return key ? pages[key] : undefined;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
function hasPageOverrides() {
|
|
117
|
+
return Object.keys(getRemixerSeo()?.pages ?? {}).length > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isFramed() {
|
|
121
|
+
return window.parent !== window;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function RemixerSeoOverrides() {
|
|
117
125
|
const snapshot = useSyncExternalStore(subscribeToHead, headSnapshot, () => '/#0');
|
|
118
126
|
const seo = getRemixerSeo();
|
|
119
127
|
const page = findPage(seo?.pages, currentPath());
|
|
@@ -138,4 +146,12 @@ export function RemixerSeo() {
|
|
|
138
146
|
);
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
export function RemixerSeo() {
|
|
150
|
+
if (!hasPageOverrides() && !isFramed()) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return <RemixerSeoOverrides />;
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
export default RemixerSeo;
|