fontdue-js 3.0.0-alpha9 → 3.0.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 +18 -0
- package/README.md +182 -13
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
- package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
- package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CartQuery.graphql.js +9 -3
- package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
- package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
- package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
- package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
- package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
- package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.d.ts +18 -0
- package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.js +56 -0
- package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
- package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
- package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
- package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
- package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
- package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
- package/dist/__tests__/createFontdueFetch.test.js +276 -0
- package/dist/__tests__/imageLoader.test.js +62 -0
- package/dist/__tests__/metricFallback.test.js +74 -0
- package/dist/__tests__/networkFetch.test.js +125 -3
- package/dist/__tests__/nextAdapter.test.js +175 -60
- package/dist/__tests__/preview.test.js +217 -0
- package/dist/__tests__/previewServer.test.js +118 -0
- package/dist/__tests__/previewState.test.js +63 -0
- package/dist/__tests__/serverConfig.test.js +62 -0
- package/dist/components/BuyButton/index.d.ts +2 -2
- package/dist/components/BuyButton/index.js +3 -3
- package/dist/components/CharacterViewer/index.d.ts +2 -2
- package/dist/components/CharacterViewer/index.js +20 -11
- package/dist/components/ConfigContext.d.ts +21 -2
- package/dist/components/ConfigContext.js +12 -2
- package/dist/components/ConnectionErrorToolbar.d.ts +1 -0
- package/dist/components/ConnectionErrorToolbar.js +106 -0
- package/dist/components/FontdueAdminToolbar/index.d.ts +2 -0
- package/dist/components/FontdueAdminToolbar/index.js +299 -0
- package/dist/components/FontdueAdminToolbar/previewState.d.ts +7 -0
- package/dist/components/FontdueAdminToolbar/previewState.js +58 -0
- package/dist/components/FontdueContextProvider/index.js +6 -4
- package/dist/components/FontdueProvider/index.js +6 -1
- package/dist/components/FontdueProvider/index.server.d.ts +1 -0
- package/dist/components/FontdueProvider/index.server.js +10 -0
- package/dist/components/NewsletterSignup/index.d.ts +2 -2
- package/dist/components/NewsletterSignup/index.js +2 -2
- package/dist/components/Root/index.js +16 -2
- package/dist/components/TestFontsForm/index.d.ts +2 -2
- package/dist/components/TestFontsForm/index.js +2 -2
- package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
- package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
- package/dist/components/TypeTesters/index.d.ts +2 -2
- package/dist/components/TypeTesters/index.js +3 -3
- package/dist/components/useFontStyle.d.ts +1 -0
- package/dist/components/useFontStyle.js +12 -3
- package/dist/corsError.d.ts +1 -5
- package/dist/corsError.js +23 -13
- package/dist/data/unicodeNamesUrl.d.ts +2 -0
- package/dist/data/unicodeNamesUrl.js +18 -0
- package/dist/data/unicodeNamesVersion.d.ts +1 -0
- package/dist/data/unicodeNamesVersion.js +4 -0
- package/dist/fallbackFontData.d.ts +2 -0
- package/dist/fallbackFontData.js +10 -0
- package/dist/fontdue.css +231 -4
- package/dist/loadFontdueProviderQuery.d.ts +2 -1
- package/dist/loadFontdueProviderQuery.js +5 -2
- package/dist/metricFallback.d.ts +48 -0
- package/dist/metricFallback.js +98 -0
- package/dist/next/image-loader.js +22 -3
- package/dist/next/index.d.ts +1 -2
- package/dist/next/index.js +14 -6
- package/dist/next/registerSingleTenantResolver.d.ts +1 -0
- package/dist/next/registerSingleTenantResolver.js +35 -0
- package/dist/next/revalidate.js +1 -1
- package/dist/next/tenant.d.ts +4 -4
- package/dist/next/tenant.js +89 -58
- package/dist/preview/constants.d.ts +9 -0
- package/dist/preview/constants.js +117 -0
- package/dist/preview/index.d.ts +53 -0
- package/dist/preview/index.js +190 -0
- package/dist/preview/server.d.ts +20 -0
- package/dist/preview/server.js +89 -0
- package/dist/relay/environment.d.ts +8 -0
- package/dist/relay/environment.js +81 -35
- package/dist/relay/loadSerializableQuery.d.ts +13 -3
- package/dist/relay/loadSerializableQuery.js +2 -0
- package/dist/relay/serverConfig.d.ts +5 -7
- package/dist/relay/serverConfig.js +83 -8
- package/dist/scripts/publishUnicodeData.js +68 -0
- package/dist/scripts/updateUnicodeData.js +41 -6
- package/dist/server/index.d.ts +37 -0
- package/dist/server/index.js +160 -0
- package/package.json +5 -1
- package/types/next-headers.d.ts +9 -0
- package/types/next-navigation.d.ts +4 -0
- package/vitest.config.ts +5 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-family metric-matched fallback fonts.
|
|
3
|
+
*
|
|
4
|
+
* The generic `Fallback` font renders a dot for every codepoint but has fixed
|
|
5
|
+
* metrics, so while a real font loads (or for glyphs it lacks) the dots don't
|
|
6
|
+
* match the real font's line height or average width — the text reflows when
|
|
7
|
+
* the real font swaps in.
|
|
8
|
+
*
|
|
9
|
+
* This builds, per family, a fallback `FontFace` from the *same* dot-font bytes
|
|
10
|
+
* but with `size-adjust` + ascent/descent/line-gap overrides derived from that
|
|
11
|
+
* font's metrics, so the placeholder dots already occupy the right box. One
|
|
12
|
+
* shared buffer backs every face. The maths is the capsize / `next/font`
|
|
13
|
+
* formulation: with `size-adjust: S`, the override percentages are the target
|
|
14
|
+
* ratios divided by S (a metric overridden as a % of font-size is then scaled
|
|
15
|
+
* by `size-adjust`, so we pre-divide to land back on the target ratio).
|
|
16
|
+
*/
|
|
17
|
+
declare global {
|
|
18
|
+
interface FontFaceDescriptors {
|
|
19
|
+
sizeAdjust?: string;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export interface TargetMetrics {
|
|
23
|
+
readonly unitsPerEm: number;
|
|
24
|
+
readonly ascender: number;
|
|
25
|
+
readonly descender: number;
|
|
26
|
+
readonly lineGap: number | null;
|
|
27
|
+
readonly avgCharWidth?: number | null;
|
|
28
|
+
}
|
|
29
|
+
/** The font-family name of the metric-matched fallback for `family`. */
|
|
30
|
+
export declare function metricFallbackFamily(family: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* The FontFace descriptors that make the dot font match `metrics`, or `null`
|
|
33
|
+
* if the metrics are unusable. `size-adjust` matches the average character
|
|
34
|
+
* width; the overrides are the target ratios divided by `size-adjust` (it
|
|
35
|
+
* scales overridden metrics, so we pre-divide). Without `avgCharWidth` there's
|
|
36
|
+
* no `size-adjust` and the overrides are the raw target ratios.
|
|
37
|
+
*
|
|
38
|
+
* Pure (no FontFace/DOM) so the maths is unit-testable.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fallbackDescriptors(metrics: TargetMetrics | null | undefined): FontFaceDescriptors | null;
|
|
41
|
+
/**
|
|
42
|
+
* Register (once) a metric-matched fallback face for `family`, derived from the
|
|
43
|
+
* dot font + `metrics`. Safe to call repeatedly and on every render. Returns
|
|
44
|
+
* the family name to put in the CSS stack, or `null` when it can't be built
|
|
45
|
+
* (no metrics, or no FontFace API / SSR) so the caller uses the generic
|
|
46
|
+
* `Fallback`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ensureMetricFallback(family: string, metrics: TargetMetrics | null | undefined): string | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { FALLBACK_FONT_WOFF2_BASE64, FALLBACK_ADVANCE_PER_EM } from './fallbackFontData.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-family metric-matched fallback fonts.
|
|
5
|
+
*
|
|
6
|
+
* The generic `Fallback` font renders a dot for every codepoint but has fixed
|
|
7
|
+
* metrics, so while a real font loads (or for glyphs it lacks) the dots don't
|
|
8
|
+
* match the real font's line height or average width — the text reflows when
|
|
9
|
+
* the real font swaps in.
|
|
10
|
+
*
|
|
11
|
+
* This builds, per family, a fallback `FontFace` from the *same* dot-font bytes
|
|
12
|
+
* but with `size-adjust` + ascent/descent/line-gap overrides derived from that
|
|
13
|
+
* font's metrics, so the placeholder dots already occupy the right box. One
|
|
14
|
+
* shared buffer backs every face. The maths is the capsize / `next/font`
|
|
15
|
+
* formulation: with `size-adjust: S`, the override percentages are the target
|
|
16
|
+
* ratios divided by S (a metric overridden as a % of font-size is then scaled
|
|
17
|
+
* by `size-adjust`, so we pre-divide to land back on the target ratio).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// `sizeAdjust` is a real FontFace descriptor (Chrome 92+, Firefox 92+,
|
|
21
|
+
// Safari 17+) but is missing from the bundled lib.dom.d.ts FontFaceDescriptors.
|
|
22
|
+
|
|
23
|
+
/** The font-family name of the metric-matched fallback for `family`. */
|
|
24
|
+
export function metricFallbackFamily(family) {
|
|
25
|
+
return `${family} fallback`;
|
|
26
|
+
}
|
|
27
|
+
let sharedBuffer = null;
|
|
28
|
+
const registered = new Set();
|
|
29
|
+
function getBuffer() {
|
|
30
|
+
if (!sharedBuffer) {
|
|
31
|
+
const binary = atob(FALLBACK_FONT_WOFF2_BASE64);
|
|
32
|
+
const bytes = new Uint8Array(binary.length);
|
|
33
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
34
|
+
sharedBuffer = bytes.buffer;
|
|
35
|
+
}
|
|
36
|
+
return sharedBuffer;
|
|
37
|
+
}
|
|
38
|
+
function pct(value) {
|
|
39
|
+
return `${value * 100}%`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The FontFace descriptors that make the dot font match `metrics`, or `null`
|
|
44
|
+
* if the metrics are unusable. `size-adjust` matches the average character
|
|
45
|
+
* width; the overrides are the target ratios divided by `size-adjust` (it
|
|
46
|
+
* scales overridden metrics, so we pre-divide). Without `avgCharWidth` there's
|
|
47
|
+
* no `size-adjust` and the overrides are the raw target ratios.
|
|
48
|
+
*
|
|
49
|
+
* Pure (no FontFace/DOM) so the maths is unit-testable.
|
|
50
|
+
*/
|
|
51
|
+
export function fallbackDescriptors(metrics) {
|
|
52
|
+
if (!metrics || !(metrics.unitsPerEm > 0)) return null;
|
|
53
|
+
const {
|
|
54
|
+
unitsPerEm: upem,
|
|
55
|
+
ascender,
|
|
56
|
+
descender,
|
|
57
|
+
lineGap,
|
|
58
|
+
avgCharWidth
|
|
59
|
+
} = metrics;
|
|
60
|
+
const descriptors = {
|
|
61
|
+
display: 'block'
|
|
62
|
+
};
|
|
63
|
+
let sizeAdjust = 1;
|
|
64
|
+
if (avgCharWidth && avgCharWidth > 0) {
|
|
65
|
+
sizeAdjust = avgCharWidth / upem / FALLBACK_ADVANCE_PER_EM;
|
|
66
|
+
descriptors.sizeAdjust = pct(sizeAdjust);
|
|
67
|
+
}
|
|
68
|
+
descriptors.ascentOverride = pct(ascender / upem / sizeAdjust);
|
|
69
|
+
descriptors.descentOverride = pct(Math.abs(descender) / upem / sizeAdjust);
|
|
70
|
+
descriptors.lineGapOverride = pct((lineGap ?? 0) / upem / sizeAdjust);
|
|
71
|
+
return descriptors;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Register (once) a metric-matched fallback face for `family`, derived from the
|
|
76
|
+
* dot font + `metrics`. Safe to call repeatedly and on every render. Returns
|
|
77
|
+
* the family name to put in the CSS stack, or `null` when it can't be built
|
|
78
|
+
* (no metrics, or no FontFace API / SSR) so the caller uses the generic
|
|
79
|
+
* `Fallback`.
|
|
80
|
+
*/
|
|
81
|
+
export function ensureMetricFallback(family, metrics) {
|
|
82
|
+
const descriptors = fallbackDescriptors(metrics);
|
|
83
|
+
if (!descriptors) return null;
|
|
84
|
+
if (typeof FontFace === 'undefined' || typeof document === 'undefined') {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const name = metricFallbackFamily(family);
|
|
88
|
+
if (registered.has(name)) return name;
|
|
89
|
+
registered.add(name);
|
|
90
|
+
try {
|
|
91
|
+
const face = new FontFace(name, getBuffer(), descriptors);
|
|
92
|
+
face.load().then(() => document.fonts.add(face)).catch(() => registered.delete(name));
|
|
93
|
+
} catch {
|
|
94
|
+
registered.delete(name);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return name;
|
|
98
|
+
}
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
// NEXT_PUBLIC_FONTDUE_IMAGE_HOST is set; see config.ts.
|
|
6
6
|
//
|
|
7
7
|
// NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS (comma-separated hostnames) should
|
|
8
|
-
// mirror the transformation host's allowed-origins list.
|
|
9
|
-
//
|
|
8
|
+
// mirror the transformation host's allowed-origins list. A leading "*."
|
|
9
|
+
// wildcard matches any subdomain ("*.fontdue.xyz" → "cdn.fontdue.xyz", not
|
|
10
|
+
// the apex or a lookalike); a scheme/port on an entry is ignored. Sources on
|
|
11
|
+
// other hosts — e.g. the /logo endpoint a site serves from its own (possibly
|
|
10
12
|
// customer-owned) domain, which can't be allowlisted — are served as-is
|
|
11
13
|
// rather than as transform URLs Cloudflare would refuse (ERROR 9401).
|
|
12
14
|
//
|
|
@@ -14,6 +16,23 @@
|
|
|
14
16
|
// reads at build time, so it must stay dependency-free, and the variables
|
|
15
17
|
// have to be present when `next build` runs (not just at serve time).
|
|
16
18
|
|
|
19
|
+
// Reduce an allowlist entry to a bare hostname (or "*."-prefixed wildcard),
|
|
20
|
+
// tolerating an optional scheme, path, or port so "https://*.fontdue.xyz"
|
|
21
|
+
// behaves like "*.fontdue.xyz".
|
|
22
|
+
function originHostname(entry) {
|
|
23
|
+
return entry.trim().replace(/^https?:\/\//i, '').split('/')[0].split(':')[0];
|
|
24
|
+
}
|
|
25
|
+
function matchesOrigin(hostname, entry) {
|
|
26
|
+
const origin = originHostname(entry);
|
|
27
|
+
if (!origin) return false;
|
|
28
|
+
if (origin.startsWith('*.')) {
|
|
29
|
+
// ".fontdue.xyz" — endsWith plus the strict length check requires at
|
|
30
|
+
// least one label before the dot, so the apex and lookalikes don't match.
|
|
31
|
+
const suffix = origin.slice(1);
|
|
32
|
+
return hostname.endsWith(suffix) && hostname.length > suffix.length;
|
|
33
|
+
}
|
|
34
|
+
return origin === hostname;
|
|
35
|
+
}
|
|
17
36
|
function transformable(src) {
|
|
18
37
|
const origins = process.env.NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS;
|
|
19
38
|
if (!origins) return true;
|
|
@@ -23,7 +42,7 @@ function transformable(src) {
|
|
|
23
42
|
} catch {
|
|
24
43
|
return false;
|
|
25
44
|
}
|
|
26
|
-
return origins.split(',').some(
|
|
45
|
+
return origins.split(',').some(entry => matchesOrigin(hostname, entry));
|
|
27
46
|
}
|
|
28
47
|
export default function fontdueImageLoader(_ref) {
|
|
29
48
|
let {
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { setFontdueServerConfig, getFontdueServerConfig, type FontdueServerConfig, } from '../relay/serverConfig.js';
|
|
1
|
+
export { __prepareFontdueRender, type FontdueEndpoint } from './tenant.js';
|
package/dist/next/index.js
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
// The config-time wrapper lives in 'fontdue-js/next/config' and the deploy
|
|
3
3
|
// hook route handler in 'fontdue-js/next/revalidate'.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
5
|
+
// Single-tenant apps need NO per-render setup and nothing from this entrypoint:
|
|
6
|
+
// mounting <FontdueProvider> registers the ambient resolver that configures
|
|
7
|
+
// every server fetch — your own (createFontdueFetch) and the embedded
|
|
8
|
+
// components' preloads, including the admin preview token (see
|
|
9
|
+
// ../components/FontdueProvider/index.server.tsx -> registerSingleTenantResolver).
|
|
10
|
+
//
|
|
11
|
+
// __prepareFontdueRender is the internal multi-tenant API (double-underscored)
|
|
12
|
+
// used only by the Fontdue-hosted next-template — not part of the foundry
|
|
13
|
+
// surface — and FontdueEndpoint is its return type. The remaining tenant/config
|
|
14
|
+
// helpers (isMultiTenant, isValidDomain, endpointForDomain, fontdueServerConfig,
|
|
15
|
+
// configureFontdueRender, setFontdueServerConfig) are deliberately not
|
|
16
|
+
// re-exported here; the modules that need them import them directly from
|
|
17
|
+
// './tenant.js' / '../relay/serverConfig.js'.
|
|
18
|
+
export { __prepareFontdueRender } from './tenant.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Registers the single-tenant ambient config resolver (env URL +
|
|
2
|
+
// draftMode()/cookies() preview token, resolved per fetch) so a single-tenant
|
|
3
|
+
// foundry that mounts <FontdueProvider> never has to call a per-render setup
|
|
4
|
+
// function. The FontdueProvider react-server entrypoint imports this module as
|
|
5
|
+
// a side effect — in the call-free model the app no longer imports
|
|
6
|
+
// 'fontdue-js/next' itself, so the provider mount is the only thing guaranteed
|
|
7
|
+
// to wire the resolver up.
|
|
8
|
+
//
|
|
9
|
+
// Why this lives apart from ./tenant.ts: it statically imports ONLY the
|
|
10
|
+
// browser-safe registerAmbientConfigResolver. The Next request APIs
|
|
11
|
+
// (next/headers, next/navigation) the resolver needs sit behind a lazy
|
|
12
|
+
// `import('./tenant.js')` in the resolver body, which runs only when a server
|
|
13
|
+
// fetch actually resolves config — i.e. only under Next. That keeps next/* off
|
|
14
|
+
// the provider entrypoint's eager import graph, so mounting <FontdueProvider>
|
|
15
|
+
// stays safe for any non-Next RSC consumer. (Today only Next resolves the
|
|
16
|
+
// react-server export condition, but the decoupling makes that a structural
|
|
17
|
+
// invariant rather than an incidental one.)
|
|
18
|
+
import { registerAmbientConfigResolver } from '../relay/serverConfig.js';
|
|
19
|
+
registerAmbientConfigResolver(async () => {
|
|
20
|
+
// Loaded lazily so next/headers + next/navigation never enter the
|
|
21
|
+
// provider's static graph; config is read per fetch from Next's request
|
|
22
|
+
// context (draftMode()/cookies()), which also covers soft navigations that
|
|
23
|
+
// re-render only the page segment.
|
|
24
|
+
const {
|
|
25
|
+
isMultiTenant,
|
|
26
|
+
singleTenantUrl,
|
|
27
|
+
buildRenderConfig
|
|
28
|
+
} = await import('./tenant.js');
|
|
29
|
+
// Multi-tenant drives config through the React.cache slot
|
|
30
|
+
// (__prepareFontdueRender), which needs the per-request route param, so the
|
|
31
|
+
// resolver stays out of its way. The slot also wins the merge, so a
|
|
32
|
+
// multi-tenant __prepareFontdueRender call still overrides this when present.
|
|
33
|
+
if (isMultiTenant || !singleTenantUrl) return undefined;
|
|
34
|
+
return buildRenderConfig(new URL(singleTenantUrl).host);
|
|
35
|
+
});
|
package/dist/next/revalidate.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// receive the tenant in the URL, e.g.
|
|
8
8
|
// POST /api/revalidate?domain=acme.fontdue.com
|
|
9
9
|
// and only that tenant's cache is purged (pages and embed data share the
|
|
10
|
-
// per-domain tag — see
|
|
10
|
+
// per-domain tag — see endpointForDomain/fontdueServerConfig in ./tenant).
|
|
11
11
|
// Single-tenant deployments use the parameterless form and purge everything
|
|
12
12
|
// carrying the 'graphql' tag.
|
|
13
13
|
|
package/dist/next/tenant.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FontdueServerConfig } from '../relay/serverConfig.js';
|
|
2
2
|
export declare const isMultiTenant: boolean;
|
|
3
|
+
export declare const singleTenantUrl: string | undefined;
|
|
3
4
|
export declare function isValidDomain(domain: string): boolean;
|
|
4
5
|
export interface FontdueEndpoint {
|
|
5
6
|
/** The site domain this endpoint resolves. */
|
|
@@ -14,13 +15,12 @@ export interface FontdueEndpoint {
|
|
|
14
15
|
*/
|
|
15
16
|
tags: string[];
|
|
16
17
|
}
|
|
17
|
-
export declare function
|
|
18
|
+
export declare function endpointForDomain(domain: string): FontdueEndpoint;
|
|
18
19
|
export declare function fontdueServerConfig(domain: string): FontdueServerConfig;
|
|
19
20
|
export declare function configureFontdueRender(domain: string): FontdueEndpoint | null;
|
|
20
21
|
interface RenderProps {
|
|
21
22
|
params: Promise<Record<string, string | string[] | undefined>>;
|
|
22
23
|
}
|
|
23
|
-
export declare function
|
|
24
|
-
export declare function
|
|
25
|
-
export declare function generateStaticParams(): Promise<never[]>;
|
|
24
|
+
export declare function buildRenderConfig(domain: string): Promise<FontdueServerConfig>;
|
|
25
|
+
export declare function __prepareFontdueRender(props: RenderProps): Promise<FontdueEndpoint>;
|
|
26
26
|
export {};
|
package/dist/next/tenant.js
CHANGED
|
@@ -20,14 +20,17 @@
|
|
|
20
20
|
//
|
|
21
21
|
// The fontdue-js components embedded in pages fetch the same way: their
|
|
22
22
|
// server-side preloads read the per-render config set by
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// __prepareFontdueRender (multi-tenant) or, in single-tenant apps, the ambient
|
|
24
|
+
// resolver that <FontdueProvider> registers — no per-render setup call — and in
|
|
25
|
+
// the browser they fetch the relative /graphql on the page's own origin, so
|
|
26
|
+
// multi-tenant mode needs no NEXT_PUBLIC_FONTDUE_URL at all.
|
|
26
27
|
|
|
27
|
-
import { notFound } from 'next/navigation';
|
|
28
|
-
import {
|
|
28
|
+
import { notFound, unstable_rethrow } from 'next/navigation';
|
|
29
|
+
import { cookies, draftMode } from 'next/headers';
|
|
30
|
+
import { setFontdueServerConfig } from '../relay/serverConfig.js';
|
|
31
|
+
import { PREVIEW_TOKEN_COOKIE, previewAuthHeaders } from '../preview/index.js';
|
|
29
32
|
export const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
|
|
30
|
-
const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
|
|
33
|
+
export const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
|
|
31
34
|
const internalOrigin = process.env.FONTDUE_ORIGIN;
|
|
32
35
|
const proxySecret = process.env.FONTDUE_PROXY_SECRET;
|
|
33
36
|
|
|
@@ -39,7 +42,7 @@ export function isValidDomain(domain) {
|
|
|
39
42
|
}
|
|
40
43
|
// Where to fetch GraphQL for a given tenant domain, plus any headers needed
|
|
41
44
|
// for the Fontdue server to resolve that tenant.
|
|
42
|
-
export function
|
|
45
|
+
export function endpointForDomain(domain) {
|
|
43
46
|
const tags = ['graphql', `graphql:${domain}`];
|
|
44
47
|
if (!isMultiTenant) {
|
|
45
48
|
return {
|
|
@@ -84,12 +87,11 @@ export function fontdueServerConfig(domain) {
|
|
|
84
87
|
const {
|
|
85
88
|
origin,
|
|
86
89
|
headers
|
|
87
|
-
} =
|
|
90
|
+
} = endpointForDomain(domain);
|
|
88
91
|
return {
|
|
89
92
|
url: origin,
|
|
90
93
|
headers,
|
|
91
|
-
cacheTags: [`graphql:${domain}`]
|
|
92
|
-
domain
|
|
94
|
+
cacheTags: [`graphql:${domain}`]
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -104,66 +106,95 @@ export function fontdueServerConfig(domain) {
|
|
|
104
106
|
export function configureFontdueRender(domain) {
|
|
105
107
|
if (!isValidDomain(domain)) return null;
|
|
106
108
|
setFontdueServerConfig(fontdueServerConfig(domain));
|
|
107
|
-
return
|
|
109
|
+
return endpointForDomain(domain);
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
// The props every page, layout and generateMetadata receives. Any params
|
|
111
113
|
// object is structurally compatible; only `domain` is read.
|
|
112
114
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
// Build this render's server config for `domain` (origin, headers, per-site
|
|
116
|
+
// cache tags) and install it for the rest of the pass. When a logged-in admin
|
|
117
|
+
// is in preview (Next draft mode), fold the token into the headers and drop the
|
|
118
|
+
// cache tags, so every server fetch in the render — the app's own and the
|
|
119
|
+
// embedded components' preloads — reveals hidden fonts and stays live
|
|
120
|
+
// (uncached, never landing in a shared cache).
|
|
121
|
+
export async function buildRenderConfig(domain) {
|
|
122
|
+
const config = fontdueServerConfig(domain);
|
|
123
|
+
const previewHeaders = await readPreviewHeaders();
|
|
124
|
+
return previewHeaders ? {
|
|
125
|
+
...config,
|
|
126
|
+
headers: {
|
|
127
|
+
...config.headers,
|
|
128
|
+
...previewHeaders
|
|
129
|
+
},
|
|
130
|
+
cacheTags: undefined
|
|
131
|
+
} : config;
|
|
132
|
+
}
|
|
133
|
+
async function applyRenderConfig(domain) {
|
|
134
|
+
setFontdueServerConfig(await buildRenderConfig(domain));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Internal multi-tenant API (double-underscored, not part of the public
|
|
138
|
+
// foundry surface): the one line at the top of every page, layout and
|
|
139
|
+
// generateMetadata body in the multi-tenant [domain] route tree (the
|
|
140
|
+
// Fontdue-hosted service):
|
|
124
141
|
//
|
|
125
|
-
//
|
|
126
|
-
// re-renders just the page segment, and the per-render store starts empty
|
|
127
|
-
// on every pass. Forgetting it is loud, not subtle: currentFontdueEndpoint
|
|
128
|
-
// throws in multi-tenant mode when no render config was set.
|
|
142
|
+
// await __prepareFontdueRender(props);
|
|
129
143
|
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
144
|
+
// Reads the request's site from the route param, 404s anything that isn't a
|
|
145
|
+
// plain hostname, and configures the render (including preview) for that site.
|
|
146
|
+
// It runs per entry point: a soft navigation re-renders only the page segment
|
|
147
|
+
// with a fresh render store. Route handlers are not React renders — use the
|
|
148
|
+
// returned endpoint explicitly there.
|
|
149
|
+
export async function __prepareFontdueRender(props) {
|
|
134
150
|
const {
|
|
135
151
|
domain
|
|
136
152
|
} = await props.params;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return
|
|
153
|
+
if (typeof domain !== 'string' || !isValidDomain(domain)) notFound();
|
|
154
|
+
await applyRenderConfig(domain);
|
|
155
|
+
return endpointForDomain(domain);
|
|
140
156
|
}
|
|
141
157
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
// The admin preview token for this render, as Authorization headers, or
|
|
159
|
+
// undefined when not previewing. Draft mode gates it: reading draftMode().
|
|
160
|
+
// isEnabled is static-safe (it returns false during static generation and only
|
|
161
|
+
// forces dynamic rendering when actually enabled), and the token cookie is only
|
|
162
|
+
// read in the preview branch.
|
|
163
|
+
//
|
|
164
|
+
// The catch handles two very different throws, and the difference is load-
|
|
165
|
+
// bearing for preview:
|
|
166
|
+
//
|
|
167
|
+
// 1. Next's dynamic-rendering bailout. When an admin is previewing, reading
|
|
168
|
+
// cookies() during a prerender/static pass throws a control-flow error
|
|
169
|
+
// (DynamicServerError / prerender interrupt) whose whole purpose is to
|
|
170
|
+
// tell Next "abandon the static render and go dynamic." This is the ONLY
|
|
171
|
+
// signal that takes a route in generateStaticParams off the full-route
|
|
172
|
+
// cache for the preview request. Swallowing it (as this used to) let Next
|
|
173
|
+
// serve the cached, token-less *public* render in preview, so the embedded
|
|
174
|
+
// components' own server preloads (createNetworkFetch) never saw the token
|
|
175
|
+
// — hidden fonts stayed hidden and @required(THROW) embeds like
|
|
176
|
+
// <BuyButton> crashed the server render (FD-712). unstable_rethrow re-
|
|
177
|
+
// throws exactly these control-flow errors so the route bails to a
|
|
178
|
+
// dynamic, token-bearing render. Public renders never hit this branch
|
|
179
|
+
// (draftMode().isEnabled is false → early return), so they stay static.
|
|
180
|
+
// 2. A genuine "no request scope" throw (older runtimes calling
|
|
181
|
+
// draftMode()/cookies() with no request at all): not a control-flow error,
|
|
182
|
+
// so unstable_rethrow is a no-op and we fall through to "not previewing".
|
|
183
|
+
async function readPreviewHeaders() {
|
|
184
|
+
try {
|
|
185
|
+
var _await$cookies$get;
|
|
186
|
+
if (!(await draftMode()).isEnabled) return undefined;
|
|
187
|
+
const token = (_await$cookies$get = (await cookies()).get(PREVIEW_TOKEN_COOKIE)) === null || _await$cookies$get === void 0 ? void 0 : _await$cookies$get.value;
|
|
188
|
+
return token ? previewAuthHeaders(token) : undefined;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
unstable_rethrow(error);
|
|
191
|
+
return undefined;
|
|
155
192
|
}
|
|
156
|
-
return fontdueEndpoint(new URL(requireSingleTenantUrl()).host);
|
|
157
193
|
}
|
|
158
194
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// static-on-demand rendering (generated on first request, then cached until
|
|
166
|
-
// revalidated) instead of fully dynamic rendering.
|
|
167
|
-
export async function generateStaticParams() {
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
195
|
+
// The single-tenant ambient resolver that lets a foundry skip per-render setup
|
|
196
|
+
// is registered from ./registerSingleTenantResolver.ts (imported as a side
|
|
197
|
+
// effect by the FontdueProvider react-server entrypoint), not here — so this
|
|
198
|
+
// module's static next/* imports stay off that entrypoint's eager graph. The
|
|
199
|
+
// resolver lazy-imports buildRenderConfig + singleTenantUrl + isMultiTenant
|
|
200
|
+
// from this module at fetch time.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const PREVIEW_TOKEN_COOKIE = "fontdue_preview_token";
|
|
2
|
+
export declare const PREVIEW_MARKER_COOKIE = "fontdue_preview";
|
|
3
|
+
export declare const PREVIEW_ENDPOINT = "/api/preview";
|
|
4
|
+
export declare const DEFAULT_PREVIEW_TTL_MS: number;
|
|
5
|
+
export declare const PREVIEW_MARKER_GRACE_MS: number;
|
|
6
|
+
export declare const PREVIEW_HEADER = "fontdue-preview";
|
|
7
|
+
export declare function hasPreviewMarkerCookie(): boolean;
|
|
8
|
+
export declare function getPreviewExpiry(): number | undefined;
|
|
9
|
+
export declare function setPreviewMarkerCookie(on: boolean | number): void;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Cookie + endpoint conventions for the admin preview contract.
|
|
2
|
+
//
|
|
3
|
+
// Kept in their own module (no Web/Node APIs) so the client-side toolbar can
|
|
4
|
+
// import them without pulling in the server-only route handler — see ./index.
|
|
5
|
+
|
|
6
|
+
// httpOnly cookie carrying the logged-in admin's short-lived admin token.
|
|
7
|
+
// Only ever read server-side, where it's forwarded as
|
|
8
|
+
// `Authorization: Bearer <token>` so GraphQL reveals hidden (unpublished) fonts.
|
|
9
|
+
export const PREVIEW_TOKEN_COOKIE = 'fontdue_preview_token';
|
|
10
|
+
|
|
11
|
+
// Readable twin of the token cookie. Lets the client toolbar tell preview is
|
|
12
|
+
// on without exposing the token itself.
|
|
13
|
+
export const PREVIEW_MARKER_COOKIE = 'fontdue_preview';
|
|
14
|
+
|
|
15
|
+
// Default path the toolbar POSTs (enter) / DELETEs (exit) to. Relative, so it
|
|
16
|
+
// hits the storefront's own origin. Override per app via
|
|
17
|
+
// FontdueConfig.preview.endpoint (and mount the route to match).
|
|
18
|
+
export const PREVIEW_ENDPOINT = '/api/preview';
|
|
19
|
+
|
|
20
|
+
// Canonical preview-token lifetime, in ms. The token is a stateless
|
|
21
|
+
// `Phoenix.Token` with a fixed 1-hour `max_age` (see the backend
|
|
22
|
+
// `Fontage.Token` `:admin_token` `verify` — `max_age: 60 * 60`). It is the
|
|
23
|
+
// source of truth; this constant only exists as a *fallback* for when the
|
|
24
|
+
// `createAdminToken` mutation hasn't yet been taught to return its real
|
|
25
|
+
// `expiresAt` (the backend half ships separately). Keep these two in lockstep.
|
|
26
|
+
export const DEFAULT_PREVIEW_TTL_MS = 60 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
// How long past the token's expiry the readable marker cookie should live.
|
|
29
|
+
// The marker outlives the token on purpose: once the token lapses, the toolbar
|
|
30
|
+
// must still find the marker so it can show the "preview expired" warning and
|
|
31
|
+
// offer "re-enter" — rather than the cookie silently vanishing and the toolbar
|
|
32
|
+
// snapping back to "not previewing" (which would hide the very problem from the
|
|
33
|
+
// admin). A few hours of grace comfortably covers a backgrounded tab.
|
|
34
|
+
export const PREVIEW_MARKER_GRACE_MS = 4 * 60 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
// Request header storefront fetches send to declare preview intent explicitly.
|
|
37
|
+
// The GraphQL server reveals hidden (unpublished) fonts only when this is
|
|
38
|
+
// "true" AND the request is an authenticated admin — so merely *being* logged
|
|
39
|
+
// in (an admin session cookie riding a storefront fetch) never leaks hidden
|
|
40
|
+
// fonts outside preview. "false" forces the public view even for an admin; an
|
|
41
|
+
// absent header (older fontdue-js clients) keeps the legacy "any admin sees
|
|
42
|
+
// hidden" behavior, so older integrations are unaffected. See the matching
|
|
43
|
+
// server logic in FontageWeb.Schema.Context.
|
|
44
|
+
export const PREVIEW_HEADER = 'fontdue-preview';
|
|
45
|
+
|
|
46
|
+
// The marker cookie's *value* now encodes the token's expiry as epoch-ms, e.g.
|
|
47
|
+
// `fontdue_preview=1718640000000`. Previously it was a bare `1`; that legacy
|
|
48
|
+
// value still parses (as 1ms after the epoch → always "expired"), so any stale
|
|
49
|
+
// `=1` cookie left from an older client degrades safely to the warning state
|
|
50
|
+
// rather than reading as a live preview. Empty / unset means "not previewing".
|
|
51
|
+
|
|
52
|
+
// Raw value of the marker cookie on the current document, or undefined when the
|
|
53
|
+
// cookie is absent or we're not in a browser. Browser-only and guarded so it
|
|
54
|
+
// stays inert during server rendering.
|
|
55
|
+
function readPreviewMarkerValue() {
|
|
56
|
+
if (typeof document === 'undefined') return undefined;
|
|
57
|
+
for (const part of document.cookie.split('; ')) {
|
|
58
|
+
const idx = part.indexOf('=');
|
|
59
|
+
if (idx === -1) continue;
|
|
60
|
+
if (part.slice(0, idx) !== PREVIEW_MARKER_COOKIE) continue;
|
|
61
|
+
const value = part.slice(idx + 1);
|
|
62
|
+
return value === '' ? undefined : value;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Whether the readable preview marker cookie is present on the current
|
|
68
|
+
// document. Lets the admin toolbar and the client-side Relay network layer tell
|
|
69
|
+
// that an admin entered preview, so storefront fetches can send PREVIEW_HEADER
|
|
70
|
+
// "true" while the marker is set and "false" otherwise. Presence — not
|
|
71
|
+
// freshness — gates the header: a just-expired token still needs its render to
|
|
72
|
+
// stay out of the public cache until the admin re-enters or exits, and the
|
|
73
|
+
// server independently rejects a dead token, so a marker with a past expiry
|
|
74
|
+
// never actually leaks hidden fonts.
|
|
75
|
+
export function hasPreviewMarkerCookie() {
|
|
76
|
+
return readPreviewMarkerValue() !== undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Expiry (epoch-ms) stored in the marker cookie, or undefined when the marker
|
|
80
|
+
// is absent or its value isn't a finite number. The toolbar compares this to
|
|
81
|
+
// `Date.now()` to choose between the "previewing" and "preview expired" states.
|
|
82
|
+
export function getPreviewExpiry() {
|
|
83
|
+
const value = readPreviewMarkerValue();
|
|
84
|
+
if (value === undefined) return undefined;
|
|
85
|
+
const expiry = Number(value);
|
|
86
|
+
return Number.isFinite(expiry) ? expiry : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set or clear the readable preview marker cookie directly in the browser.
|
|
90
|
+
//
|
|
91
|
+
// The script-tag (CDN) embed has no server preview route to broker an httpOnly
|
|
92
|
+
// token through — and needs none: it is client-only, so its GraphQL fetches
|
|
93
|
+
// carry the admin's Fontdue session cookie cross-origin (SameSite=None) and the
|
|
94
|
+
// server reveals hidden fonts on `fontdue-preview: true` alone. This marker is
|
|
95
|
+
// what flips the client-side Relay layer to send that header (see
|
|
96
|
+
// hasPreviewMarkerCookie). It is NOT a credential — the admin session still
|
|
97
|
+
// gates the reveal, so a non-admin setting it just sees the public view.
|
|
98
|
+
//
|
|
99
|
+
// When entering (`on` truthy), `on` is the token's expiry as epoch-ms; the
|
|
100
|
+
// cookie stores it and is given a Max-Age that outlives the token by
|
|
101
|
+
// PREVIEW_MARKER_GRACE_MS so the toolbar can reach the "expired" warning state.
|
|
102
|
+
// Passing `false` clears the cookie. (A bare `true` keeps older call sites
|
|
103
|
+
// working by falling back to now + DEFAULT_PREVIEW_TTL_MS.)
|
|
104
|
+
//
|
|
105
|
+
// Secure is set on https (the marker stays JS-readable; Secure only restricts
|
|
106
|
+
// the transport) and omitted on http so it still persists in local dev.
|
|
107
|
+
export function setPreviewMarkerCookie(on) {
|
|
108
|
+
if (typeof document === 'undefined') return;
|
|
109
|
+
const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
|
|
110
|
+
if (on === false) {
|
|
111
|
+
document.cookie = `${PREVIEW_MARKER_COOKIE}=; Path=/; SameSite=Lax${secure}; Max-Age=0`;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const expiresAt = typeof on === 'number' ? on : Date.now() + DEFAULT_PREVIEW_TTL_MS;
|
|
115
|
+
const maxAgeSeconds = Math.max(0, Math.ceil((expiresAt - Date.now() + PREVIEW_MARKER_GRACE_MS) / 1000));
|
|
116
|
+
document.cookie = `${PREVIEW_MARKER_COOKIE}=${expiresAt}; Path=/; SameSite=Lax${secure}; Max-Age=${maxAgeSeconds}`;
|
|
117
|
+
}
|