bsmnt 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +36 -4
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +7 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +7 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +72 -34
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +3 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +2 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +7 -1
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +36 -4
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +72 -34
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +3 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +2 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +5 -0
- package/src/templates/next-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
- package/src/templates/next-default/biome.json +2 -1
- package/src/templates/next-default/components/ui/image/index.tsx +20 -12
- package/src/templates/next-default/next.config.ts +10 -4
- package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
- package/src/templates/next-experiments/biome.json +2 -1
- package/src/templates/next-experiments/components/ui/image/index.tsx +20 -12
- package/src/templates/next-experiments/next.config.ts +10 -4
- package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -0
- package/src/templates/next-pagebuilder/biome.json +2 -1
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +5 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
- package/src/templates/next-pagebuilder/components/ui/image/index.tsx +20 -12
- package/src/templates/next-pagebuilder/next.config.ts +4 -3
- package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
- package/src/templates/next-pagebuilder-cache/biome.json +2 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +5 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +1 -0
- package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +21 -12
- package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
- package/src/templates/next-pagebuilder-cache/next.config.ts +4 -3
- package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
- package/src/templates/next-webgl/biome.json +2 -1
- package/src/templates/next-webgl/components/ui/image/index.tsx +20 -12
- package/src/templates/next-webgl/next.config.ts +10 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useClient } from "sanity";
|
|
3
|
+
import { apiVersion } from "@/lib/integrations/sanity/env";
|
|
4
|
+
|
|
5
|
+
const PREVIEW_ASSETS_QUERY = `*[
|
|
6
|
+
_type == "studioPreview" &&
|
|
7
|
+
key in $keys &&
|
|
8
|
+
defined(preview.asset._ref)
|
|
9
|
+
] | order(_updatedAt desc) {
|
|
10
|
+
key,
|
|
11
|
+
"url": preview.asset->url
|
|
12
|
+
}`;
|
|
13
|
+
|
|
14
|
+
type PreviewDocument = {
|
|
15
|
+
key: string | null;
|
|
16
|
+
url: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type StudioPreviewUrls = Record<string, string>;
|
|
20
|
+
|
|
21
|
+
const SEPARATOR = " ";
|
|
22
|
+
|
|
23
|
+
/** `undefined` while in flight, so callers can tell loading from "no preview". */
|
|
24
|
+
export function useStudioPreviewUrls(keys: readonly string[]) {
|
|
25
|
+
const client = useClient({ apiVersion });
|
|
26
|
+
const [urls, setUrls] = useState<StudioPreviewUrls>();
|
|
27
|
+
// Keys off contents, not array identity — callers pass a freshly mapped array.
|
|
28
|
+
const keyList = keys.join(SEPARATOR);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!keyList) {
|
|
32
|
+
setUrls({});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let cancelled = false;
|
|
37
|
+
|
|
38
|
+
client
|
|
39
|
+
.fetch<PreviewDocument[]>(PREVIEW_ASSETS_QUERY, {
|
|
40
|
+
keys: keyList.split(SEPARATOR),
|
|
41
|
+
})
|
|
42
|
+
.then((documents) => {
|
|
43
|
+
if (cancelled) return;
|
|
44
|
+
const next: StudioPreviewUrls = {};
|
|
45
|
+
for (const document of documents) {
|
|
46
|
+
if (document.key && document.url && !next[document.key]) {
|
|
47
|
+
next[document.key] = document.url;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
setUrls(next);
|
|
51
|
+
})
|
|
52
|
+
.catch(() => {
|
|
53
|
+
if (!cancelled) setUrls({});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
cancelled = true;
|
|
58
|
+
};
|
|
59
|
+
}, [client, keyList]);
|
|
60
|
+
|
|
61
|
+
return urls;
|
|
62
|
+
}
|
|
@@ -9,15 +9,35 @@ export type PageBuilderDocumentType =
|
|
|
9
9
|
| "blogContent"
|
|
10
10
|
| (typeof singletonComponents)[number]["schemaType"];
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* A block declares two orthogonal axes. `pageTypes` is which page documents may
|
|
14
|
+
* host it and drives validation; `category` is what the block structurally is
|
|
15
|
+
* and drives the insert menu. Never collapse them back into one field — that is
|
|
16
|
+
* what made the menu a single flat list.
|
|
17
|
+
*/
|
|
18
|
+
export type PageBuilderGroupName = "generic" | "blogEntry";
|
|
19
|
+
|
|
20
|
+
export const componentCategories = [
|
|
21
|
+
{ name: "hero", title: "Hero" },
|
|
22
|
+
{ name: "text", title: "Text" },
|
|
23
|
+
{ name: "list", title: "List" },
|
|
24
|
+
{ name: "content", title: "Content" },
|
|
25
|
+
{ name: "misc", title: "Miscellaneous" },
|
|
26
|
+
] as const satisfies ReadonlyArray<{ name: string; title: string }>;
|
|
27
|
+
|
|
28
|
+
export type ComponentCategory = (typeof componentCategories)[number]["name"];
|
|
29
|
+
|
|
12
30
|
type PageBuilderReferenceMember = {
|
|
13
31
|
documentType: PageBuilderDocumentType;
|
|
14
32
|
name: string;
|
|
15
33
|
title: string;
|
|
16
34
|
documentId?: string;
|
|
17
|
-
|
|
35
|
+
pageTypes: readonly PageBuilderGroupName[];
|
|
36
|
+
category: ComponentCategory;
|
|
18
37
|
};
|
|
19
38
|
|
|
20
|
-
export
|
|
39
|
+
export const blockPreviewKey = (schemaTypeName: string) =>
|
|
40
|
+
`block.${schemaTypeName}`;
|
|
21
41
|
|
|
22
42
|
export const pageBuilderPageTypes: Array<{
|
|
23
43
|
title: string;
|
|
@@ -41,31 +61,36 @@ const reusablePageBuilderReferenceMembers: PageBuilderReferenceMember[] = [
|
|
|
41
61
|
documentType: "hero",
|
|
42
62
|
name: "heroReference",
|
|
43
63
|
title: "Heading",
|
|
44
|
-
|
|
64
|
+
pageTypes: ["generic"],
|
|
65
|
+
category: "hero",
|
|
45
66
|
},
|
|
46
67
|
{
|
|
47
68
|
documentType: "description",
|
|
48
69
|
name: "descriptionReference",
|
|
49
70
|
title: "Description",
|
|
50
|
-
|
|
71
|
+
pageTypes: ["generic"],
|
|
72
|
+
category: "text",
|
|
51
73
|
},
|
|
52
74
|
{
|
|
53
75
|
documentType: "richTextBlock",
|
|
54
76
|
name: "richTextBlockReference",
|
|
55
77
|
title: "Rich Text",
|
|
56
|
-
|
|
78
|
+
pageTypes: ["generic"],
|
|
79
|
+
category: "text",
|
|
57
80
|
},
|
|
58
81
|
{
|
|
59
82
|
documentType: "gatedContent",
|
|
60
83
|
name: "gatedContentReference",
|
|
61
84
|
title: "Gated Content",
|
|
62
|
-
|
|
85
|
+
pageTypes: ["generic"],
|
|
86
|
+
category: "misc",
|
|
63
87
|
},
|
|
64
88
|
{
|
|
65
89
|
documentType: "blogContent",
|
|
66
90
|
name: "blogContentReference",
|
|
67
91
|
title: "Blog Content",
|
|
68
|
-
|
|
92
|
+
pageTypes: ["blogEntry"],
|
|
93
|
+
category: "content",
|
|
69
94
|
},
|
|
70
95
|
];
|
|
71
96
|
|
|
@@ -75,7 +100,8 @@ const singletonPageBuilderReferenceMembers: PageBuilderReferenceMember[] =
|
|
|
75
100
|
name: `${component.schemaType}Reference`,
|
|
76
101
|
title: component.title,
|
|
77
102
|
documentId: component.documentId,
|
|
78
|
-
|
|
103
|
+
pageTypes: ["generic"],
|
|
104
|
+
category: component.category,
|
|
79
105
|
}));
|
|
80
106
|
|
|
81
107
|
export const pageBuilderReferenceMembers: PageBuilderReferenceMember[] = [
|
|
@@ -93,25 +119,37 @@ export const getPageBuilderReferenceMemberByDocumentType = (
|
|
|
93
119
|
(member) => member.documentType === documentType,
|
|
94
120
|
);
|
|
95
121
|
|
|
96
|
-
const
|
|
122
|
+
const isMemberVisibleForPageType = (
|
|
123
|
+
member: PageBuilderReferenceMember,
|
|
97
124
|
pageType: PageBuilderGroupName | undefined,
|
|
98
|
-
|
|
99
|
-
) => (pageType ?? "generic") === group;
|
|
125
|
+
) => member.pageTypes.includes(pageType ?? "generic");
|
|
100
126
|
|
|
101
127
|
export const getVisiblePageBuilderReferenceMembers = (
|
|
102
128
|
pageType: PageBuilderGroupName | undefined,
|
|
103
129
|
) =>
|
|
104
130
|
pageBuilderReferenceMembers.filter((member) =>
|
|
105
|
-
member
|
|
131
|
+
isMemberVisibleForPageType(member, pageType),
|
|
106
132
|
);
|
|
107
133
|
|
|
108
|
-
export const
|
|
134
|
+
export const getVisiblePageBuilderMemberNames = (
|
|
109
135
|
pageType: PageBuilderGroupName | undefined,
|
|
110
136
|
) =>
|
|
111
|
-
|
|
112
|
-
(
|
|
137
|
+
new Set(
|
|
138
|
+
getVisiblePageBuilderReferenceMembers(pageType).map((member) => member.name),
|
|
113
139
|
);
|
|
114
140
|
|
|
141
|
+
export const getVisiblePageBuilderInsertMenuGroups = (
|
|
142
|
+
pageType: PageBuilderGroupName | undefined,
|
|
143
|
+
) => {
|
|
144
|
+
const visible = getVisiblePageBuilderMemberNames(pageType);
|
|
145
|
+
return pageBuilderInsertMenuGroups
|
|
146
|
+
.map((group) => ({
|
|
147
|
+
...group,
|
|
148
|
+
of: group.of.filter((name) => visible.has(name)),
|
|
149
|
+
}))
|
|
150
|
+
.filter((group) => group.of.length > 0);
|
|
151
|
+
};
|
|
152
|
+
|
|
115
153
|
export const getInvalidPageBuilderDocumentTypes = (
|
|
116
154
|
pageType: PageBuilderGroupName | undefined,
|
|
117
155
|
documentTypes: PageBuilderDocumentType[],
|
|
@@ -121,24 +159,24 @@ export const getInvalidPageBuilderDocumentTypes = (
|
|
|
121
159
|
|
|
122
160
|
if (!member) return false;
|
|
123
161
|
|
|
124
|
-
return !member
|
|
125
|
-
isPageBuilderGroupVisible(pageType, group),
|
|
126
|
-
);
|
|
162
|
+
return !isMemberVisibleForPageType(member, pageType);
|
|
127
163
|
});
|
|
128
164
|
|
|
129
|
-
export const pageBuilderInsertMenuGroups
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
name
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
export const pageBuilderInsertMenuGroups: Array<{
|
|
166
|
+
name: ComponentCategory;
|
|
167
|
+
title: string;
|
|
168
|
+
of: string[];
|
|
169
|
+
}> = componentCategories.map(({ name, title }) => ({
|
|
170
|
+
name,
|
|
171
|
+
title,
|
|
172
|
+
of: pageBuilderReferenceMembers
|
|
173
|
+
.filter((member) => member.category === name)
|
|
174
|
+
.map((member) => member.name),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
export const pageBuilderBlockPreviewOptions = pageBuilderReferenceMembers.map(
|
|
178
|
+
(member) => ({
|
|
179
|
+
title: `Block — ${member.title} · ${member.name}`,
|
|
180
|
+
value: blockPreviewKey(member.name),
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
@@ -4,3 +4,4 @@ export { gatedContent } from "./reusable/gated-content";
|
|
|
4
4
|
export { hero } from "./reusable/hero";
|
|
5
5
|
export { richTextBlock } from "./reusable/rich-text-block";
|
|
6
6
|
export { blogCollection } from "./singleton/content-collection";
|
|
7
|
+
export { studioPreview } from "./studio-preview";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
import { pageBuilderCardIcon } from "@/lib/integrations/sanity/icons";
|
|
3
|
+
import { pageBuilderBlockPreviewOptions } from "@/lib/integrations/sanity/page-builder-config";
|
|
4
|
+
|
|
5
|
+
export const studioPreview = defineType({
|
|
6
|
+
name: "studioPreview",
|
|
7
|
+
title: "Component Preview",
|
|
8
|
+
type: "document",
|
|
9
|
+
icon: pageBuilderCardIcon,
|
|
10
|
+
fields: [
|
|
11
|
+
defineField({
|
|
12
|
+
name: "key",
|
|
13
|
+
title: "Component",
|
|
14
|
+
type: "string",
|
|
15
|
+
options: { list: pageBuilderBlockPreviewOptions },
|
|
16
|
+
validation: (Rule) => Rule.required(),
|
|
17
|
+
}),
|
|
18
|
+
defineField({
|
|
19
|
+
name: "preview",
|
|
20
|
+
title: "Preview image",
|
|
21
|
+
type: "image",
|
|
22
|
+
options: { hotspot: true },
|
|
23
|
+
validation: (Rule) => Rule.required(),
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
preview: {
|
|
27
|
+
select: { key: "key", media: "preview" },
|
|
28
|
+
prepare({ key, media }) {
|
|
29
|
+
return {
|
|
30
|
+
title:
|
|
31
|
+
pageBuilderBlockPreviewOptions.find((option) => option.value === key)
|
|
32
|
+
?.title ?? "Component Preview",
|
|
33
|
+
media,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
gatedContent,
|
|
16
16
|
hero,
|
|
17
17
|
richTextBlock,
|
|
18
|
+
studioPreview,
|
|
18
19
|
} from "./components";
|
|
19
20
|
import { author } from "./content/author";
|
|
20
21
|
import { blogCategory } from "./content/blog-category";
|
|
@@ -77,6 +78,7 @@ export {
|
|
|
77
78
|
richText,
|
|
78
79
|
softwareApplicationJsonLd,
|
|
79
80
|
structuredData,
|
|
81
|
+
studioPreview,
|
|
80
82
|
videoJsonLd,
|
|
81
83
|
};
|
|
82
84
|
|
|
@@ -113,6 +115,7 @@ export const schema: { types: SchemaTypeDefinition[] } = {
|
|
|
113
115
|
description,
|
|
114
116
|
richTextBlock,
|
|
115
117
|
gatedContent,
|
|
118
|
+
studioPreview,
|
|
116
119
|
|
|
117
120
|
// Layout types (singletons for site-wide layout)
|
|
118
121
|
companyData,
|
|
@@ -6,12 +6,14 @@ import {
|
|
|
6
6
|
structuredDataIcon,
|
|
7
7
|
} from "./icons";
|
|
8
8
|
|
|
9
|
+
// `category` must be one of `componentCategories` in page-builder-config.ts.
|
|
9
10
|
export const singletonComponents = [
|
|
10
11
|
{
|
|
11
12
|
schemaType: "blogCollection",
|
|
12
13
|
documentId: "blogCollection",
|
|
13
14
|
title: "Blog Collection",
|
|
14
15
|
icon: pageListIcon,
|
|
16
|
+
category: "list",
|
|
15
17
|
},
|
|
16
18
|
] as const;
|
|
17
19
|
|
package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
gatedContentIcon,
|
|
18
18
|
heroIcon,
|
|
19
19
|
layoutFolderIcon,
|
|
20
|
+
pageBuilderCardIcon,
|
|
20
21
|
pageFolderIcon,
|
|
21
22
|
pageIcon,
|
|
22
23
|
pagesFolderIcon,
|
|
@@ -301,6 +302,10 @@ export const structure: StructureResolver = (S, context) =>
|
|
|
301
302
|
S.documentTypeListItem("blogContent")
|
|
302
303
|
.title("Blog Content")
|
|
303
304
|
.icon(contentIcon),
|
|
305
|
+
S.divider(),
|
|
306
|
+
S.documentTypeListItem("studioPreview")
|
|
307
|
+
.title("Component Previews")
|
|
308
|
+
.icon(pageBuilderCardIcon),
|
|
304
309
|
]),
|
|
305
310
|
),
|
|
306
311
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
`<Image $attrs />`,
|
|
5
|
+
`<SanityImage $attrs />`
|
|
6
|
+
} as $image where {
|
|
7
|
+
$attrs <: contains `fill`,
|
|
8
|
+
! $attrs <: contains `fill={false}`,
|
|
9
|
+
! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
|
|
10
|
+
register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
|
|
11
|
+
}
|
|
@@ -234,7 +234,8 @@
|
|
|
234
234
|
"plugins": [
|
|
235
235
|
"./.biome/plugins/no-anchor-element.grit",
|
|
236
236
|
"./.biome/plugins/no-unnecessary-forwardref.grit",
|
|
237
|
-
"./.biome/plugins/no-relative-parent-imports.grit"
|
|
237
|
+
"./.biome/plugins/no-relative-parent-imports.grit",
|
|
238
|
+
"./.biome/plugins/no-fill-without-sizes.grit"
|
|
238
239
|
],
|
|
239
240
|
"vcs": {
|
|
240
241
|
"clientKind": "git",
|
|
@@ -98,7 +98,6 @@ const getFinalPlaceholder = (
|
|
|
98
98
|
* Enhanced Image component with responsive sizing and automatic optimizations.
|
|
99
99
|
*
|
|
100
100
|
* Always use this component instead of next/image directly. Provides:
|
|
101
|
-
* - Automatic responsive sizes generation
|
|
102
101
|
* - Smart blur placeholders with aspect ratio support
|
|
103
102
|
* - Performance optimizations (lazy loading by default)
|
|
104
103
|
* - Priority flag for LCP images
|
|
@@ -147,14 +146,14 @@ export function Image({
|
|
|
147
146
|
style,
|
|
148
147
|
className,
|
|
149
148
|
objectFit = "cover",
|
|
150
|
-
quality =
|
|
149
|
+
quality = 75,
|
|
151
150
|
alt = "",
|
|
152
151
|
fill,
|
|
153
152
|
block = !fill,
|
|
154
|
-
width
|
|
155
|
-
height
|
|
156
|
-
mobileSize
|
|
157
|
-
desktopSize
|
|
153
|
+
width,
|
|
154
|
+
height,
|
|
155
|
+
mobileSize,
|
|
156
|
+
desktopSize,
|
|
158
157
|
sizes,
|
|
159
158
|
src,
|
|
160
159
|
unoptimized,
|
|
@@ -164,9 +163,18 @@ export function Image({
|
|
|
164
163
|
priority = false,
|
|
165
164
|
...props
|
|
166
165
|
}: ImageProps) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
const responsiveSizes =
|
|
167
|
+
mobileSize || desktopSize
|
|
168
|
+
? buildImageSizes({
|
|
169
|
+
base: mobileSize ?? "100vw",
|
|
170
|
+
desktop: desktopSize ?? mobileSize ?? "100vw",
|
|
171
|
+
})
|
|
172
|
+
: undefined
|
|
173
|
+
|
|
174
|
+
const hasIntrinsicSize = width !== undefined && height !== undefined
|
|
175
|
+
const finalSizes = sizes ?? responsiveSizes ?? (hasIntrinsicSize ? undefined : "100vw")
|
|
176
|
+
const finalWidth = width ?? (block ? 1 : undefined)
|
|
177
|
+
const finalHeight = height ?? (block ? 1 : undefined)
|
|
170
178
|
|
|
171
179
|
// Early return after hooks
|
|
172
180
|
if (!src) return null
|
|
@@ -194,8 +202,8 @@ export function Image({
|
|
|
194
202
|
<NextImage
|
|
195
203
|
ref={ref}
|
|
196
204
|
fill={!block}
|
|
197
|
-
{...(
|
|
198
|
-
{...(
|
|
205
|
+
{...(finalWidth !== undefined && { width: finalWidth })}
|
|
206
|
+
{...(finalHeight !== undefined && { height: finalHeight })}
|
|
199
207
|
priority={priority}
|
|
200
208
|
quality={quality}
|
|
201
209
|
alt={alt}
|
|
@@ -204,7 +212,7 @@ export function Image({
|
|
|
204
212
|
...style,
|
|
205
213
|
}}
|
|
206
214
|
className={cn(className, block && "block w-full")}
|
|
207
|
-
|
|
215
|
+
{...(finalSizes && { sizes: finalSizes })}
|
|
208
216
|
src={src}
|
|
209
217
|
unoptimized={unoptimized || isSvg}
|
|
210
218
|
draggable={false}
|
|
@@ -77,12 +77,18 @@ const nextConfig: NextConfig = {
|
|
|
77
77
|
],
|
|
78
78
|
},
|
|
79
79
|
images: {
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
remotePatterns: [
|
|
81
|
+
{
|
|
82
|
+
protocol: "https",
|
|
83
|
+
hostname: "cdn.sanity.io",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
82
86
|
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
|
|
83
|
-
qualities: [90],
|
|
87
|
+
qualities: [75, 90, 100],
|
|
84
88
|
formats: ["image/avif", "image/webp"],
|
|
85
|
-
deviceSizes: [
|
|
89
|
+
deviceSizes: [
|
|
90
|
+
640, 750, 828, 1080, 1200, 1320, 1620, 1920, 2048, 2560, 3024, 3456, 3840,
|
|
91
|
+
],
|
|
86
92
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
|
87
93
|
},
|
|
88
94
|
headers: async () => [
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
`<Image $attrs />`,
|
|
5
|
+
`<SanityImage $attrs />`
|
|
6
|
+
} as $image where {
|
|
7
|
+
$attrs <: contains `fill`,
|
|
8
|
+
! $attrs <: contains `fill={false}`,
|
|
9
|
+
! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
|
|
10
|
+
register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
|
|
11
|
+
}
|
|
@@ -229,7 +229,8 @@
|
|
|
229
229
|
"plugins": [
|
|
230
230
|
"./.biome/plugins/no-anchor-element.grit",
|
|
231
231
|
"./.biome/plugins/no-unnecessary-forwardref.grit",
|
|
232
|
-
"./.biome/plugins/no-relative-parent-imports.grit"
|
|
232
|
+
"./.biome/plugins/no-relative-parent-imports.grit",
|
|
233
|
+
"./.biome/plugins/no-fill-without-sizes.grit"
|
|
233
234
|
],
|
|
234
235
|
"vcs": {
|
|
235
236
|
"clientKind": "git",
|
|
@@ -98,7 +98,6 @@ const getFinalPlaceholder = (
|
|
|
98
98
|
* Enhanced Image component with responsive sizing and automatic optimizations.
|
|
99
99
|
*
|
|
100
100
|
* Always use this component instead of next/image directly. Provides:
|
|
101
|
-
* - Automatic responsive sizes generation
|
|
102
101
|
* - Smart blur placeholders with aspect ratio support
|
|
103
102
|
* - Performance optimizations (lazy loading by default)
|
|
104
103
|
* - Priority flag for LCP images
|
|
@@ -147,14 +146,14 @@ export function Image({
|
|
|
147
146
|
style,
|
|
148
147
|
className,
|
|
149
148
|
objectFit = "cover",
|
|
150
|
-
quality =
|
|
149
|
+
quality = 75,
|
|
151
150
|
alt = "",
|
|
152
151
|
fill,
|
|
153
152
|
block = !fill,
|
|
154
|
-
width
|
|
155
|
-
height
|
|
156
|
-
mobileSize
|
|
157
|
-
desktopSize
|
|
153
|
+
width,
|
|
154
|
+
height,
|
|
155
|
+
mobileSize,
|
|
156
|
+
desktopSize,
|
|
158
157
|
sizes,
|
|
159
158
|
src,
|
|
160
159
|
unoptimized,
|
|
@@ -164,9 +163,18 @@ export function Image({
|
|
|
164
163
|
priority = false,
|
|
165
164
|
...props
|
|
166
165
|
}: ImageProps) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
const responsiveSizes =
|
|
167
|
+
mobileSize || desktopSize
|
|
168
|
+
? buildImageSizes({
|
|
169
|
+
base: mobileSize ?? "100vw",
|
|
170
|
+
desktop: desktopSize ?? mobileSize ?? "100vw",
|
|
171
|
+
})
|
|
172
|
+
: undefined
|
|
173
|
+
|
|
174
|
+
const hasIntrinsicSize = width !== undefined && height !== undefined
|
|
175
|
+
const finalSizes = sizes ?? responsiveSizes ?? (hasIntrinsicSize ? undefined : "100vw")
|
|
176
|
+
const finalWidth = width ?? (block ? 1 : undefined)
|
|
177
|
+
const finalHeight = height ?? (block ? 1 : undefined)
|
|
170
178
|
|
|
171
179
|
// Early return after hooks
|
|
172
180
|
if (!src) return null
|
|
@@ -194,8 +202,8 @@ export function Image({
|
|
|
194
202
|
<NextImage
|
|
195
203
|
ref={ref}
|
|
196
204
|
fill={!block}
|
|
197
|
-
{...(
|
|
198
|
-
{...(
|
|
205
|
+
{...(finalWidth !== undefined && { width: finalWidth })}
|
|
206
|
+
{...(finalHeight !== undefined && { height: finalHeight })}
|
|
199
207
|
priority={priority}
|
|
200
208
|
quality={quality}
|
|
201
209
|
alt={alt}
|
|
@@ -204,7 +212,7 @@ export function Image({
|
|
|
204
212
|
...style,
|
|
205
213
|
}}
|
|
206
214
|
className={cn(className, block && "block w-full")}
|
|
207
|
-
|
|
215
|
+
{...(finalSizes && { sizes: finalSizes })}
|
|
208
216
|
src={src}
|
|
209
217
|
unoptimized={unoptimized || isSvg}
|
|
210
218
|
draggable={false}
|
|
@@ -77,12 +77,18 @@ const nextConfig: NextConfig = {
|
|
|
77
77
|
],
|
|
78
78
|
},
|
|
79
79
|
images: {
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
remotePatterns: [
|
|
81
|
+
{
|
|
82
|
+
protocol: "https",
|
|
83
|
+
hostname: "cdn.sanity.io",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
82
86
|
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
|
|
83
|
-
qualities: [90],
|
|
87
|
+
qualities: [75, 90, 100],
|
|
84
88
|
formats: ["image/avif", "image/webp"],
|
|
85
|
-
deviceSizes: [
|
|
89
|
+
deviceSizes: [
|
|
90
|
+
640, 750, 828, 1080, 1200, 1320, 1620, 1920, 2048, 2560, 3024, 3456, 3840,
|
|
91
|
+
],
|
|
86
92
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
|
87
93
|
},
|
|
88
94
|
headers: async () => [
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
`<Image $attrs />`,
|
|
5
|
+
`<SanityImage $attrs />`
|
|
6
|
+
} as $image where {
|
|
7
|
+
$attrs <: contains `fill`,
|
|
8
|
+
! $attrs <: contains `fill={false}`,
|
|
9
|
+
! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
|
|
10
|
+
register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
|
|
11
|
+
}
|
|
@@ -247,7 +247,8 @@
|
|
|
247
247
|
"plugins": [
|
|
248
248
|
"./.biome/plugins/no-anchor-element.grit",
|
|
249
249
|
"./.biome/plugins/no-unnecessary-forwardref.grit",
|
|
250
|
-
"./.biome/plugins/no-relative-parent-imports.grit"
|
|
250
|
+
"./.biome/plugins/no-relative-parent-imports.grit",
|
|
251
|
+
"./.biome/plugins/no-fill-without-sizes.grit"
|
|
251
252
|
],
|
|
252
253
|
"vcs": {
|
|
253
254
|
"clientKind": "git",
|
package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx
CHANGED
|
@@ -28,6 +28,9 @@ export function ArticleContent({ block }: ArticleContentProps) {
|
|
|
28
28
|
<SanityImage
|
|
29
29
|
image={author.avatar}
|
|
30
30
|
alt={author.name || ""}
|
|
31
|
+
width={40}
|
|
32
|
+
height={40}
|
|
33
|
+
maxWidth={80}
|
|
31
34
|
className="size-10 rounded-full object-cover"
|
|
32
35
|
/>
|
|
33
36
|
) : null}
|
|
@@ -50,6 +53,8 @@ export function ArticleContent({ block }: ArticleContentProps) {
|
|
|
50
53
|
image={thumbnail}
|
|
51
54
|
alt={thumbnail.alt || ""}
|
|
52
55
|
priority
|
|
56
|
+
quality={90}
|
|
57
|
+
sizes="(min-width: 896px) 896px, 100vw"
|
|
53
58
|
className="aspect-video w-full object-cover"
|
|
54
59
|
/>
|
|
55
60
|
</div>
|
|
@@ -14,6 +14,7 @@ export const RelatedPostItem = ({ post }: { post: ArticleRelatedPost }) => {
|
|
|
14
14
|
<SanityImage
|
|
15
15
|
image={post.metadata.image}
|
|
16
16
|
alt={post.title || ""}
|
|
17
|
+
sizes="(min-width: 768px) 768px, 100vw"
|
|
17
18
|
className="aspect-[16/9] w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
18
19
|
/>
|
|
19
20
|
</div>
|