@akinon/pz-theme 2.0.105-rc.0 → 2.0.105
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 +7 -35
- package/jest/react-18-style-text.ts +66 -0
- package/package.json +4 -4
- package/src/__tests__/google-fonts-server-render.test.tsx +231 -0
- package/src/blocks/__tests__/carousel-ssr-style-text.test.tsx +151 -0
- package/src/blocks/__tests__/scoped-style-text.test.tsx +351 -0
- package/src/blocks/announcement-bar-block.tsx +10 -1
- package/src/blocks/button-block.tsx +4 -32
- package/src/blocks/collapsible-block.tsx +12 -2
- package/src/blocks/favourites-grid-block.tsx +15 -8
- package/src/blocks/icon-button-block.tsx +11 -2
- package/src/blocks/icon-group-block.tsx +9 -2
- package/src/blocks/product-gallery-block.tsx +19 -7
- package/src/blocks/product-grid-block.tsx +40 -32
- package/src/blocks/product-recommendations-block.tsx +15 -8
- package/src/blocks/store-locator-block.tsx +11 -4
- package/src/chrome/theme-google-fonts-links.tsx +85 -0
- package/src/index.ts +11 -0
- package/src/theme-placeholder-wrapper.tsx +26 -106
- package/src/theme-placeholder.tsx +2 -0
- package/src/utils/__tests__/google-fonts.test.ts +276 -0
- package/src/utils/carousel-ssr.tsx +18 -1
- package/src/utils/css-safety.ts +8 -0
- package/src/utils/google-fonts.ts +254 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
MOBILE_MEDIA_QUERY_COMPACT,
|
|
13
13
|
TABLET_MEDIA_QUERY_COMPACT
|
|
14
14
|
} from '../utils/breakpoints';
|
|
15
|
+
import { neutralizeStyleTagBreakout } from '../utils/css-safety';
|
|
15
16
|
import ThemeBlock, { Block } from '../theme-block';
|
|
16
17
|
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
17
18
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
@@ -945,43 +946,50 @@ const ProductGridBlock = ({
|
|
|
945
946
|
{...partAttrs('list')}
|
|
946
947
|
style={{ display: 'grid', gap: 'var(--product-grid-gap, 16px)' }}
|
|
947
948
|
>
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
currentPage
|
|
959
|
-
) +
|
|
960
|
-
`}` +
|
|
961
|
-
// After the mobile query and only when a tablet count was authored,
|
|
962
|
-
// so a grid without one emits the two rules it always emitted.
|
|
963
|
-
(columnsTablet !== null
|
|
964
|
-
? `${TABLET_MEDIA_QUERY_COMPACT}{[data-product-grid-id="${gridId}"]{grid-template-columns:repeat(${columnsTablet},minmax(0,1fr));}` +
|
|
949
|
+
{/* Verbatim, not a text child: React 18's server renderer would escape
|
|
950
|
+
the selectors' quotes (see `css-safety.ts`). */}
|
|
951
|
+
<style
|
|
952
|
+
dangerouslySetInnerHTML={{
|
|
953
|
+
__html: neutralizeStyleTagBreakout(
|
|
954
|
+
`[data-product-grid-id="${gridId}"]{grid-template-columns:repeat(${baseColumns},minmax(0,1fr));}` +
|
|
955
|
+
buildInsertionRules(gridId, placedInsertions, baseColumns, currentPage) +
|
|
956
|
+
`${MOBILE_MEDIA_QUERY_COMPACT}{[data-product-grid-id="${gridId}"]{grid-template-columns:repeat(${columnsMobile},minmax(0,1fr));}` +
|
|
957
|
+
// Inside the same query as the mobile template, so a tile is
|
|
958
|
+
// measured against the column count it actually sits in.
|
|
965
959
|
buildInsertionRules(
|
|
966
960
|
gridId,
|
|
967
961
|
placedInsertions,
|
|
968
|
-
|
|
962
|
+
columnsMobile,
|
|
969
963
|
currentPage
|
|
970
964
|
) +
|
|
971
|
-
`}`
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
965
|
+
`}` +
|
|
966
|
+
// After the mobile query and only when a tablet count was
|
|
967
|
+
// authored, so a grid without one emits the two rules it always
|
|
968
|
+
// emitted.
|
|
969
|
+
(columnsTablet !== null
|
|
970
|
+
? `${TABLET_MEDIA_QUERY_COMPACT}{[data-product-grid-id="${gridId}"]{grid-template-columns:repeat(${columnsTablet},minmax(0,1fr));}` +
|
|
971
|
+
buildInsertionRules(
|
|
972
|
+
gridId,
|
|
973
|
+
placedInsertions,
|
|
974
|
+
columnsTablet,
|
|
975
|
+
currentPage
|
|
976
|
+
) +
|
|
977
|
+
`}`
|
|
978
|
+
: '') +
|
|
979
|
+
// Last and unconditional, so an explicit choice outranks both the
|
|
980
|
+
// authored count and the mobile override above it.
|
|
981
|
+
(chosenColumns !== null
|
|
982
|
+
? `[data-product-grid-id="${gridId}"]{grid-template-columns:repeat(${chosenColumns},minmax(0,1fr));}` +
|
|
983
|
+
buildInsertionRules(
|
|
984
|
+
gridId,
|
|
985
|
+
placedInsertions,
|
|
986
|
+
chosenColumns,
|
|
987
|
+
currentPage
|
|
988
|
+
)
|
|
989
|
+
: '')
|
|
990
|
+
)
|
|
991
|
+
}}
|
|
992
|
+
/>
|
|
985
993
|
|
|
986
994
|
{/* A designer who marked a child as the row layout gets their own card
|
|
987
995
|
repeated per product; otherwise the built-in card above renders. */}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
buildCommerceItem,
|
|
18
18
|
emitCommerceEvent
|
|
19
19
|
} from '../utils/analytics';
|
|
20
|
+
import { neutralizeStyleTagBreakout } from '../utils/css-safety';
|
|
20
21
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
21
22
|
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
22
23
|
import { DESIGNER_SAMPLE_PRODUCT } from './item-template-utils';
|
|
@@ -1160,14 +1161,20 @@ const ProductRecommendationsBlock = ({
|
|
|
1160
1161
|
// overlay exists at all, so a strip without quick view is untouched.
|
|
1161
1162
|
tabIndex={showQuickView ? -1 : undefined}
|
|
1162
1163
|
>
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1164
|
+
{/* Verbatim, not a text child: React 18's server renderer would escape
|
|
1165
|
+
the selector's quotes (see `css-safety.ts`). */}
|
|
1166
|
+
<style
|
|
1167
|
+
dangerouslySetInnerHTML={{
|
|
1168
|
+
__html: neutralizeStyleTagBreakout(
|
|
1169
|
+
`[data-product-recommendations-id="${trackId}"] [data-product-recommendations-track]{grid-auto-columns:${cardWidth(
|
|
1170
|
+
columnsDesktop
|
|
1171
|
+
)};}` +
|
|
1172
|
+
`@media (max-width:1023px){[data-product-recommendations-id="${trackId}"] [data-product-recommendations-track]{grid-auto-columns:${cardWidth(
|
|
1173
|
+
columnsMobile
|
|
1174
|
+
)};}}`
|
|
1175
|
+
)
|
|
1176
|
+
}}
|
|
1177
|
+
/>
|
|
1171
1178
|
|
|
1172
1179
|
{(() => {
|
|
1173
1180
|
const inlineHeader = headerLayout === 'inline';
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { useGetRetailStoreStockMutation } from '@akinon/next/data/client/product';
|
|
15
15
|
import { useLocalization } from '@akinon/next/hooks';
|
|
16
16
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
17
|
+
import { neutralizeStyleTagBreakout } from '../utils/css-safety';
|
|
17
18
|
import { useOverlay } from '../hooks/use-overlay';
|
|
18
19
|
import { formatDecimal } from './commerce-format';
|
|
19
20
|
import {
|
|
@@ -1060,10 +1061,16 @@ const StoreLocatorBlock = ({
|
|
|
1060
1061
|
)}
|
|
1061
1062
|
|
|
1062
1063
|
{isGrid && (
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1064
|
+
// Verbatim, not a text child: React 18's server renderer would escape
|
|
1065
|
+
// the selector's quotes (see `css-safety.ts`).
|
|
1066
|
+
<style
|
|
1067
|
+
dangerouslySetInnerHTML={{
|
|
1068
|
+
__html: neutralizeStyleTagBreakout(
|
|
1069
|
+
`[data-store-locator-id="${listId}"]{grid-template-columns:repeat(${baseColumns},minmax(0,1fr));}` +
|
|
1070
|
+
`@media (max-width:1023px){[data-store-locator-id="${listId}"]{grid-template-columns:repeat(${columnsMobile},minmax(0,1fr));}}`
|
|
1071
|
+
)
|
|
1072
|
+
}}
|
|
1073
|
+
/>
|
|
1067
1074
|
)}
|
|
1068
1075
|
|
|
1069
1076
|
<ListTag
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as ReactDOM from 'react-dom';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildGoogleFontLinkId,
|
|
6
|
+
buildGoogleFontStylesheetUrl,
|
|
7
|
+
collectGoogleFontFamilies,
|
|
8
|
+
GOOGLE_FONTS_CSS_ORIGIN,
|
|
9
|
+
GOOGLE_FONTS_FILES_ORIGIN
|
|
10
|
+
} from '../utils/google-fonts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The Google families a composition uses, linked from the server render.
|
|
14
|
+
*
|
|
15
|
+
* Section CSS names these families with no fallback stack, and their
|
|
16
|
+
* stylesheets used to be appended only by client effects — so until hydration
|
|
17
|
+
* the page painted in the browser's default serif and swapped the real faces
|
|
18
|
+
* in seconds later. Linked here, the stylesheets are part of the document
|
|
19
|
+
* before first paint: the move ZERO-5004 made for uploaded faces
|
|
20
|
+
* (theme-fonts-link.tsx), applied to Google's.
|
|
21
|
+
*
|
|
22
|
+
* Rendered by every placeholder. React hoists a `precedence` stylesheet into
|
|
23
|
+
* the head and keeps one per href, so a family the header, body and footer all
|
|
24
|
+
* use is still requested once.
|
|
25
|
+
*/
|
|
26
|
+
export const THEME_GOOGLE_FONTS_PRECEDENCE = 'pz-theme-google-fonts';
|
|
27
|
+
|
|
28
|
+
type PreconnectFn = (href: string, options?: { crossOrigin?: string }) => void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Spread onto the link rather than written as a JSX attribute. Brands
|
|
32
|
+
* transpile this file from `src` and type-check it against their own
|
|
33
|
+
* @types/react; 18.x does not declare `precedence`, so `next build` would fail
|
|
34
|
+
* on a prop the renderer itself accepts. The rendered element is identical.
|
|
35
|
+
*/
|
|
36
|
+
const HOIST_AS_RESOURCE: Record<string, string> = {
|
|
37
|
+
precedence: THEME_GOOGLE_FONTS_PRECEDENCE
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const ThemeGoogleFontsLinks = ({
|
|
41
|
+
sections,
|
|
42
|
+
settings
|
|
43
|
+
}: {
|
|
44
|
+
sections: ReadonlyArray<{ styles?: unknown; blocks?: unknown }> | null;
|
|
45
|
+
settings: Record<string, unknown> | null;
|
|
46
|
+
}) => {
|
|
47
|
+
const families = collectGoogleFontFamilies(sections, settings);
|
|
48
|
+
|
|
49
|
+
if (families.length === 0) return null;
|
|
50
|
+
|
|
51
|
+
// Hints go through React's resource API rather than as <link> elements:
|
|
52
|
+
// React dedupes them across placeholders and emits them ahead of the
|
|
53
|
+
// stylesheets, where plain preconnect elements repeat once per placeholder.
|
|
54
|
+
// Face files are CORS fetches, so the gstatic connection must be anonymous
|
|
55
|
+
// or the browser cannot reuse it. Every React that hoists `precedence`
|
|
56
|
+
// (Next's vendored canary, React 19) has this API.
|
|
57
|
+
const preconnect = (ReactDOM as unknown as { preconnect?: PreconnectFn })
|
|
58
|
+
.preconnect;
|
|
59
|
+
|
|
60
|
+
if (typeof preconnect === 'function') {
|
|
61
|
+
preconnect(GOOGLE_FONTS_CSS_ORIGIN);
|
|
62
|
+
preconnect(GOOGLE_FONTS_FILES_ORIGIN, { crossOrigin: 'anonymous' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
{families.map((family) => {
|
|
68
|
+
const id = buildGoogleFontLinkId(family);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<link
|
|
72
|
+
key={id}
|
|
73
|
+
rel="stylesheet"
|
|
74
|
+
href={buildGoogleFontStylesheetUrl(family)}
|
|
75
|
+
id={id}
|
|
76
|
+
{...HOIST_AS_RESOURCE}
|
|
77
|
+
// Marks the links the server owns, the way `data-pz-theme-fonts`
|
|
78
|
+
// marks the uploaded-face stylesheet.
|
|
79
|
+
data-pz-google-font="true"
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,17 @@ export {
|
|
|
36
36
|
THEME_FONTS_CSS_PATH
|
|
37
37
|
} from './chrome/theme-fonts-link';
|
|
38
38
|
export { buildCustomFontFaceCss } from './utils/custom-fonts';
|
|
39
|
+
export {
|
|
40
|
+
ThemeGoogleFontsLinks,
|
|
41
|
+
THEME_GOOGLE_FONTS_PRECEDENCE
|
|
42
|
+
} from './chrome/theme-google-fonts-links';
|
|
43
|
+
export {
|
|
44
|
+
buildGoogleFontLinkId,
|
|
45
|
+
buildGoogleFontStylesheetUrl,
|
|
46
|
+
collectGoogleFontFamilies,
|
|
47
|
+
loadGoogleFontStylesheet,
|
|
48
|
+
resolveGoogleFontFamily
|
|
49
|
+
} from './utils/google-fonts';
|
|
39
50
|
export {
|
|
40
51
|
armHistoryRestoreGuard,
|
|
41
52
|
buildRestoreGuardScript,
|
|
@@ -33,6 +33,10 @@ import { parseEditorMessage, postToEditor } from './editor-bridge';
|
|
|
33
33
|
import { useBrowseMode } from './browse-mode';
|
|
34
34
|
import { neutralizeStyleTagBreakout } from './utils/css-safety';
|
|
35
35
|
import { buildCustomFontFaceCss } from './utils/custom-fonts';
|
|
36
|
+
import {
|
|
37
|
+
collectGoogleFontFamilies,
|
|
38
|
+
loadGoogleFontStylesheet
|
|
39
|
+
} from './utils/google-fonts';
|
|
36
40
|
|
|
37
41
|
// Re-exported for the tests and for a brand that builds the faces itself; it
|
|
38
42
|
// lives in ./utils/custom-fonts because a server route has to call it too and
|
|
@@ -200,41 +204,6 @@ export const buildThemeTypographyCss = (
|
|
|
200
204
|
return declarations.length ? `:root{${declarations.join(';')}}` : '';
|
|
201
205
|
};
|
|
202
206
|
|
|
203
|
-
// Helper function to load Google Font
|
|
204
|
-
const loadGoogleFont = (fontFamily: string, fontWeight?: string) => {
|
|
205
|
-
if (!fontFamily || fontFamily === 'inherit') return;
|
|
206
|
-
|
|
207
|
-
const cleanFontFamily = fontFamily.replace(/['"]/g, '').trim();
|
|
208
|
-
|
|
209
|
-
// Skip system fonts
|
|
210
|
-
const systemFonts = [
|
|
211
|
-
'Arial',
|
|
212
|
-
'Helvetica',
|
|
213
|
-
'Times New Roman',
|
|
214
|
-
'Georgia',
|
|
215
|
-
'Courier New',
|
|
216
|
-
'Verdana'
|
|
217
|
-
];
|
|
218
|
-
if (systemFonts.includes(cleanFontFamily)) return;
|
|
219
|
-
|
|
220
|
-
const weights = fontWeight || '100;300;400;500;600;700;800;900';
|
|
221
|
-
const fontParam = `${cleanFontFamily.replace(/\s+/g, '+')}:wght@${weights}`;
|
|
222
|
-
const fontUrl = `https://fonts.googleapis.com/css2?family=${fontParam}&display=swap`;
|
|
223
|
-
|
|
224
|
-
const linkId = `google-font-${cleanFontFamily
|
|
225
|
-
.replace(/\s+/g, '-')
|
|
226
|
-
.toLowerCase()}`;
|
|
227
|
-
|
|
228
|
-
// Check if already loaded
|
|
229
|
-
if (document.getElementById(linkId)) return;
|
|
230
|
-
|
|
231
|
-
const link = document.createElement('link');
|
|
232
|
-
link.id = linkId;
|
|
233
|
-
link.href = fontUrl;
|
|
234
|
-
link.rel = 'stylesheet';
|
|
235
|
-
document.head.appendChild(link);
|
|
236
|
-
};
|
|
237
|
-
|
|
238
207
|
interface ThemePlaceholderWrapperProps {
|
|
239
208
|
slug: string;
|
|
240
209
|
initialSections: Section[];
|
|
@@ -693,34 +662,17 @@ export default function ThemePlaceholderWrapper({
|
|
|
693
662
|
});
|
|
694
663
|
}, [resolvedPageContext, isDesigner]);
|
|
695
664
|
|
|
696
|
-
//
|
|
665
|
+
// The composition's Google fonts. ThemePlaceholder already links them from
|
|
666
|
+
// the server (chrome/theme-google-fonts-links.tsx) and the loader stands down
|
|
667
|
+
// for those; this covers a wrapper mounted some other way. Same collector as
|
|
668
|
+
// the server, so the two cannot disagree about which families a page needs.
|
|
697
669
|
useEffect(() => {
|
|
698
670
|
if (typeof window === 'undefined') return;
|
|
699
671
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
blocks.forEach((block) => {
|
|
705
|
-
const fontFamily =
|
|
706
|
-
block.styles?.['font-family']?.desktop ||
|
|
707
|
-
block.styles?.['font-family']?.mobile;
|
|
708
|
-
if (fontFamily && fontFamily !== 'inherit') {
|
|
709
|
-
loadGoogleFont(fontFamily);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Recursively check nested blocks
|
|
713
|
-
if (block.blocks) {
|
|
714
|
-
loadFontsFromBlocks(block.blocks as Section['blocks']);
|
|
715
|
-
}
|
|
716
|
-
});
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
// Load fonts from all initial sections
|
|
720
|
-
initialSections.forEach((section) => {
|
|
721
|
-
loadFontsFromBlocks(section.blocks);
|
|
722
|
-
});
|
|
723
|
-
}, [initialSections]);
|
|
672
|
+
collectGoogleFontFamilies(initialSections, initialThemeSettings).forEach(
|
|
673
|
+
(family) => loadGoogleFontStylesheet(family)
|
|
674
|
+
);
|
|
675
|
+
}, [initialSections, initialThemeSettings]);
|
|
724
676
|
|
|
725
677
|
// Handle responsive breakpoint detection when not in iframe (e.g. production site)
|
|
726
678
|
useEffect(() => {
|
|
@@ -836,51 +788,19 @@ export default function ThemePlaceholderWrapper({
|
|
|
836
788
|
setThemeSettings(theme.settings);
|
|
837
789
|
}
|
|
838
790
|
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
Array.isArray(theme
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
);
|
|
852
|
-
uniqueFonts.forEach((font) => loadGoogleFont(font));
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Load fonts from block styles (for text blocks with custom fonts)
|
|
856
|
-
if (theme?.placeholders) {
|
|
857
|
-
// Recursive function to load fonts from nested blocks
|
|
858
|
-
const loadFontsFromBlocks = (blocks: any[]) => {
|
|
859
|
-
blocks?.forEach((block) => {
|
|
860
|
-
const fontFamily =
|
|
861
|
-
block.styles?.['font-family']?.desktop ||
|
|
862
|
-
block.styles?.['font-family']?.mobile;
|
|
863
|
-
if (fontFamily && fontFamily !== 'inherit') {
|
|
864
|
-
loadGoogleFont(fontFamily);
|
|
865
|
-
}
|
|
866
|
-
// Recursively check nested blocks
|
|
867
|
-
if (block.blocks && block.blocks.length > 0) {
|
|
868
|
-
loadFontsFromBlocks(block.blocks);
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
theme.placeholders.forEach(
|
|
874
|
-
(p: {
|
|
875
|
-
sections?: Array<{
|
|
876
|
-
blocks?: Array<any>;
|
|
877
|
-
}>;
|
|
878
|
-
}) => {
|
|
879
|
-
p.sections?.forEach((section) => {
|
|
880
|
-
loadFontsFromBlocks(section.blocks || []);
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
);
|
|
791
|
+
// Google fonts the incoming theme uses: its typography variants and
|
|
792
|
+
// every placeholder's section and block styles.
|
|
793
|
+
if (theme?.settings || theme?.placeholders) {
|
|
794
|
+
const incomingSections = Array.isArray(theme?.placeholders)
|
|
795
|
+
? theme.placeholders.flatMap(
|
|
796
|
+
(p: { sections?: unknown[] }) => p?.sections ?? []
|
|
797
|
+
)
|
|
798
|
+
: [];
|
|
799
|
+
|
|
800
|
+
collectGoogleFontFamilies(
|
|
801
|
+
incomingSections,
|
|
802
|
+
theme?.settings
|
|
803
|
+
).forEach((family) => loadGoogleFontStylesheet(family));
|
|
884
804
|
}
|
|
885
805
|
|
|
886
806
|
if (theme?.placeholders) {
|
|
@@ -966,7 +886,7 @@ export default function ThemePlaceholderWrapper({
|
|
|
966
886
|
case 'LOAD_FONT': {
|
|
967
887
|
const { fontFamily, fontWeight } = messageData || {};
|
|
968
888
|
if (fontFamily) {
|
|
969
|
-
|
|
889
|
+
loadGoogleFontStylesheet(fontFamily, fontWeight);
|
|
970
890
|
}
|
|
971
891
|
break;
|
|
972
892
|
}
|
|
@@ -2,6 +2,7 @@ import 'server-only';
|
|
|
2
2
|
|
|
3
3
|
import ThemePlaceholderWrapper from './theme-placeholder-wrapper';
|
|
4
4
|
import { ThemeFontsLink } from './chrome/theme-fonts-link';
|
|
5
|
+
import { ThemeGoogleFontsLinks } from './chrome/theme-google-fonts-links';
|
|
5
6
|
import { loadPlaceholderSections } from './load-placeholder-sections';
|
|
6
7
|
import { generateThemeCSS } from './utils';
|
|
7
8
|
|
|
@@ -26,6 +27,7 @@ export default async function ThemePlaceholder({
|
|
|
26
27
|
return (
|
|
27
28
|
<>
|
|
28
29
|
<ThemeFontsLink settings={themeSettings} />
|
|
30
|
+
<ThemeGoogleFontsLinks sections={sections} settings={themeSettings} />
|
|
29
31
|
{themeCSS && <style dangerouslySetInnerHTML={{ __html: themeCSS }} />}
|
|
30
32
|
<ThemePlaceholderWrapper
|
|
31
33
|
slug={slug}
|