bsmnt 0.2.10 → 0.2.11
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/dist/configs/skills.d.ts +27 -0
- package/dist/configs/skills.d.ts.map +1 -0
- package/dist/configs/skills.js +18 -0
- package/dist/configs/skills.js.map +1 -0
- package/dist/configs/skills.json +26 -0
- package/dist/helpers/add/hooks-config.d.ts.map +1 -1
- package/dist/helpers/add/hooks-config.js +0 -6
- package/dist/helpers/add/hooks-config.js.map +1 -1
- package/dist/helpers/create/setup-agent.d.ts.map +1 -1
- package/dist/helpers/create/setup-agent.js +15 -5
- package/dist/helpers/create/setup-agent.js.map +1 -1
- package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
- package/dist/helpers/integrate/merge-config.js +1 -0
- package/dist/helpers/integrate/merge-config.js.map +1 -1
- package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
- package/dist/helpers/integrate/sanity/config.js +8 -2
- package/dist/helpers/integrate/sanity/config.js.map +1 -1
- package/dist/helpers/integrate/sanity/mergers/layout-merger.d.ts.map +1 -1
- package/dist/helpers/integrate/sanity/mergers/layout-merger.js +13 -12
- package/dist/helpers/integrate/sanity/mergers/layout-merger.js.map +1 -1
- package/dist/helpers/skills/index.d.ts +10 -0
- package/dist/helpers/skills/index.d.ts.map +1 -0
- package/dist/helpers/skills/index.js +136 -0
- package/dist/helpers/skills/index.js.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +2 -1
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/confirm-publish-action.ts +31 -0
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.config.ts +17 -0
- package/src/helpers/integrate/sanity/files/lib/utils/json-ld.tsx +249 -0
- package/src/template-hooks/config.js +0 -6
- package/src/templates/next-default/app/layout.tsx +18 -0
- package/src/templates/next-default/lib/hooks/use-device-detection.ts +1 -1
- package/src/templates/next-default/lib/hooks/use-media-breakpoint.ts +1 -1
- package/src/templates/next-default/lib/hooks/use-media.ts +29 -0
- package/src/templates/next-default/lib/utils/json-ld.tsx +199 -0
- package/src/templates/next-default/package.json +1 -1
- package/src/templates/next-default/tsconfig.json +1 -0
- package/src/templates/next-experiments/app/layout.tsx +18 -0
- package/src/templates/next-experiments/lib/hooks/use-device-detection.ts +1 -1
- package/src/templates/next-experiments/lib/hooks/use-media-breakpoint.ts +1 -1
- package/src/templates/next-experiments/lib/hooks/use-media.ts +29 -0
- package/src/templates/next-experiments/lib/utils/json-ld.tsx +199 -0
- package/src/templates/next-experiments/package.json +1 -1
- package/src/templates/next-experiments/tsconfig.json +1 -0
- package/src/templates/next-webgl/app/layout.tsx +18 -0
- package/src/templates/next-webgl/lib/hooks/use-device-detection.ts +1 -1
- package/src/templates/next-webgl/lib/hooks/use-media-breakpoint.ts +1 -1
- package/src/templates/next-webgl/lib/hooks/use-media.ts +29 -0
- package/src/templates/next-webgl/lib/utils/json-ld.tsx +199 -0
- package/src/templates/next-webgl/package.json +1 -1
- package/src/templates/next-webgl/tsconfig.json +1 -0
- package/plugins/no-anchor-element.grit +0 -11
- package/plugins/no-relative-parent-imports.grit +0 -6
- package/plugins/no-unnecessary-forwardref.grit +0 -5
- package/src/template-hooks/use-media.ts +0 -33
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { DocumentActionComponent, DocumentActionProps } from "sanity";
|
|
3
|
+
|
|
4
|
+
export function createConfirmPublishAction(
|
|
5
|
+
originalPublishAction: DocumentActionComponent,
|
|
6
|
+
): DocumentActionComponent {
|
|
7
|
+
const ConfirmPublishAction = (props: DocumentActionProps) => {
|
|
8
|
+
const original = originalPublishAction(props);
|
|
9
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
10
|
+
|
|
11
|
+
if (!original) return null;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
...original,
|
|
15
|
+
onHandle: () => setDialogOpen(true),
|
|
16
|
+
dialog: dialogOpen && {
|
|
17
|
+
type: "confirm" as const,
|
|
18
|
+
tone: "critical" as const,
|
|
19
|
+
message:
|
|
20
|
+
"This will make these changes live on the production site immediately. Continue?",
|
|
21
|
+
onCancel: () => setDialogOpen(false),
|
|
22
|
+
onConfirm: () => {
|
|
23
|
+
setDialogOpen(false);
|
|
24
|
+
original.onHandle?.();
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return ConfirmPublishAction;
|
|
31
|
+
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
presentationTool,
|
|
11
11
|
} from "sanity/presentation";
|
|
12
12
|
import { structureTool } from "sanity/structure";
|
|
13
|
+
import { media } from "sanity-plugin-media";
|
|
14
|
+
import { createConfirmPublishAction } from "./confirm-publish-action";
|
|
13
15
|
import { apiVersion, dataset, previewURL, projectId } from "./env";
|
|
14
16
|
import { schema } from "./schemas";
|
|
15
17
|
import { structure } from "./structure";
|
|
@@ -33,6 +35,8 @@ export default defineConfig({
|
|
|
33
35
|
plugins: [
|
|
34
36
|
// Presentation tool for visual editing
|
|
35
37
|
presentationTool({
|
|
38
|
+
name: "preview",
|
|
39
|
+
title: "Preview",
|
|
36
40
|
resolve: {
|
|
37
41
|
// Map routes to documents and GROQ filters
|
|
38
42
|
mainDocuments: defineDocuments([
|
|
@@ -70,5 +74,18 @@ export default defineConfig({
|
|
|
70
74
|
// Vision is for querying with GROQ from inside the Studio
|
|
71
75
|
// https://www.sanity.io/docs/the-vision-plugin
|
|
72
76
|
visionTool({ defaultApiVersion: apiVersion }),
|
|
77
|
+
media(),
|
|
73
78
|
],
|
|
79
|
+
document: {
|
|
80
|
+
actions: (prev) => {
|
|
81
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
82
|
+
if (!isProd) return prev;
|
|
83
|
+
|
|
84
|
+
return prev.map((action) =>
|
|
85
|
+
action.action === "publish"
|
|
86
|
+
? createConfirmPublishAction(action)
|
|
87
|
+
: action,
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
74
91
|
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Article,
|
|
3
|
+
BreadcrumbList,
|
|
4
|
+
Organization,
|
|
5
|
+
SearchAction,
|
|
6
|
+
Thing,
|
|
7
|
+
WebPage,
|
|
8
|
+
WebSite,
|
|
9
|
+
WithContext,
|
|
10
|
+
} from "schema-dts";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
|
|
14
|
+
|
|
15
|
+
function isAbsoluteUrl(value: string) {
|
|
16
|
+
return /^https?:\/\//.test(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveUrl(value?: string) {
|
|
20
|
+
if (!value) return APP_BASE_URL;
|
|
21
|
+
if (isAbsoluteUrl(value)) return value;
|
|
22
|
+
if (!APP_BASE_URL) return undefined;
|
|
23
|
+
|
|
24
|
+
return new URL(value, APP_BASE_URL).toString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* -------------------------------- Component ------------------------------- */
|
|
28
|
+
|
|
29
|
+
export function JsonLd<T extends Thing>({
|
|
30
|
+
data,
|
|
31
|
+
}: {
|
|
32
|
+
data: WithContext<T>;
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<script
|
|
36
|
+
type="application/ld+json"
|
|
37
|
+
dangerouslySetInnerHTML={{
|
|
38
|
+
__html: JSON.stringify(data).replace(/</g, "\\u003c"),
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* -------------------------------- Generators ------------------------------ */
|
|
45
|
+
|
|
46
|
+
interface WebSiteJsonLdOptions {
|
|
47
|
+
name: string;
|
|
48
|
+
url?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
/** URL to site-wide search (e.g. "/search?q={search_term_string}") */
|
|
51
|
+
searchUrl?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function generateWebSiteJsonLd(
|
|
55
|
+
options: WebSiteJsonLdOptions,
|
|
56
|
+
): WithContext<WebSite> {
|
|
57
|
+
const { name, url, description, searchUrl } = options;
|
|
58
|
+
const resolvedUrl = resolveUrl(url);
|
|
59
|
+
const resolvedSearchUrl = resolveUrl(searchUrl);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"@context": "https://schema.org",
|
|
63
|
+
"@type": "WebSite",
|
|
64
|
+
name,
|
|
65
|
+
...(resolvedUrl && { url: resolvedUrl }),
|
|
66
|
+
...(description && { description }),
|
|
67
|
+
...(resolvedSearchUrl && {
|
|
68
|
+
potentialAction: {
|
|
69
|
+
"@type": "SearchAction",
|
|
70
|
+
target: resolvedSearchUrl,
|
|
71
|
+
"query-input": "required name=search_term_string",
|
|
72
|
+
} as SearchAction & { "query-input": string },
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface OrganizationJsonLdOptions {
|
|
78
|
+
name: string;
|
|
79
|
+
url?: string;
|
|
80
|
+
logo?: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
sameAs?: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function generateOrganizationJsonLd(
|
|
86
|
+
options: OrganizationJsonLdOptions,
|
|
87
|
+
): WithContext<Organization> {
|
|
88
|
+
const { name, url, logo, description, sameAs } = options;
|
|
89
|
+
const resolvedUrl = resolveUrl(url);
|
|
90
|
+
const resolvedLogo = resolveUrl(logo);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"@context": "https://schema.org",
|
|
94
|
+
"@type": "Organization",
|
|
95
|
+
name,
|
|
96
|
+
...(resolvedUrl && { url: resolvedUrl }),
|
|
97
|
+
...(resolvedLogo && { logo: resolvedLogo }),
|
|
98
|
+
...(description && { description }),
|
|
99
|
+
...(sameAs && { sameAs }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface WebPageJsonLdOptions {
|
|
104
|
+
title: string;
|
|
105
|
+
url?: string;
|
|
106
|
+
description?: string;
|
|
107
|
+
image?: string;
|
|
108
|
+
datePublished?: string;
|
|
109
|
+
dateModified?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function generateWebPageJsonLd(
|
|
113
|
+
options: WebPageJsonLdOptions,
|
|
114
|
+
): WithContext<WebPage> {
|
|
115
|
+
const { title, url, description, image, datePublished, dateModified } =
|
|
116
|
+
options;
|
|
117
|
+
const resolvedUrl = resolveUrl(url);
|
|
118
|
+
const resolvedImage = resolveUrl(image);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"@context": "https://schema.org",
|
|
122
|
+
"@type": "WebPage",
|
|
123
|
+
name: title,
|
|
124
|
+
...(resolvedUrl && { url: resolvedUrl }),
|
|
125
|
+
...(description && { description }),
|
|
126
|
+
...(resolvedImage && { image: resolvedImage }),
|
|
127
|
+
...(datePublished && { datePublished }),
|
|
128
|
+
...(dateModified && { dateModified }),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface ArticleJsonLdOptions {
|
|
133
|
+
title: string;
|
|
134
|
+
url?: string;
|
|
135
|
+
description?: string;
|
|
136
|
+
image?: string;
|
|
137
|
+
datePublished?: string;
|
|
138
|
+
dateModified?: string;
|
|
139
|
+
authorName?: string;
|
|
140
|
+
authorUrl?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function generateArticleJsonLd(
|
|
144
|
+
options: ArticleJsonLdOptions,
|
|
145
|
+
): WithContext<Article> {
|
|
146
|
+
const {
|
|
147
|
+
title,
|
|
148
|
+
url,
|
|
149
|
+
description,
|
|
150
|
+
image,
|
|
151
|
+
datePublished,
|
|
152
|
+
dateModified,
|
|
153
|
+
authorName,
|
|
154
|
+
authorUrl,
|
|
155
|
+
} = options;
|
|
156
|
+
const resolvedUrl = resolveUrl(url);
|
|
157
|
+
const resolvedImage = resolveUrl(image);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"@context": "https://schema.org",
|
|
161
|
+
"@type": "Article",
|
|
162
|
+
headline: title,
|
|
163
|
+
...(resolvedUrl && { url: resolvedUrl }),
|
|
164
|
+
...(description && { description }),
|
|
165
|
+
...(resolvedImage && { image: resolvedImage }),
|
|
166
|
+
...(datePublished && { datePublished }),
|
|
167
|
+
...(dateModified && { dateModified }),
|
|
168
|
+
...(authorName && {
|
|
169
|
+
author: {
|
|
170
|
+
"@type": "Person",
|
|
171
|
+
name: authorName,
|
|
172
|
+
...(authorUrl && { url: authorUrl }),
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface BreadcrumbItem {
|
|
179
|
+
name: string;
|
|
180
|
+
url: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function generateBreadcrumbJsonLd(
|
|
184
|
+
items: BreadcrumbItem[],
|
|
185
|
+
): WithContext<BreadcrumbList> {
|
|
186
|
+
return {
|
|
187
|
+
"@context": "https://schema.org",
|
|
188
|
+
"@type": "BreadcrumbList",
|
|
189
|
+
itemListElement: items.map((item, index) => {
|
|
190
|
+
const resolvedItemUrl = resolveUrl(item.url);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"@type": "ListItem",
|
|
194
|
+
position: index + 1,
|
|
195
|
+
name: item.name,
|
|
196
|
+
...(resolvedItemUrl && { item: resolvedItemUrl }),
|
|
197
|
+
};
|
|
198
|
+
}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ----------------------------- Sanity Helpers ----------------------------- */
|
|
203
|
+
|
|
204
|
+
interface SanityDocument {
|
|
205
|
+
title?: string;
|
|
206
|
+
metadata?: {
|
|
207
|
+
title?: string;
|
|
208
|
+
description?: string;
|
|
209
|
+
image?: { asset?: { url?: string } };
|
|
210
|
+
};
|
|
211
|
+
_updatedAt?: string;
|
|
212
|
+
publishedAt?: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function generateSanityArticleJsonLd(options: {
|
|
216
|
+
document: SanityDocument;
|
|
217
|
+
url?: string;
|
|
218
|
+
authorName?: string;
|
|
219
|
+
authorUrl?: string;
|
|
220
|
+
}): WithContext<Article> {
|
|
221
|
+
const { document, url, authorName, authorUrl } = options;
|
|
222
|
+
|
|
223
|
+
return generateArticleJsonLd({
|
|
224
|
+
title: document.metadata?.title || document.title || "",
|
|
225
|
+
url,
|
|
226
|
+
description: document.metadata?.description,
|
|
227
|
+
image: document.metadata?.image?.asset?.url,
|
|
228
|
+
datePublished: document.publishedAt,
|
|
229
|
+
dateModified: document._updatedAt,
|
|
230
|
+
authorName,
|
|
231
|
+
authorUrl,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function generateSanityWebPageJsonLd(options: {
|
|
236
|
+
document: SanityDocument;
|
|
237
|
+
url?: string;
|
|
238
|
+
}): WithContext<WebPage> {
|
|
239
|
+
const { document, url } = options;
|
|
240
|
+
|
|
241
|
+
return generateWebPageJsonLd({
|
|
242
|
+
title: document.metadata?.title || document.title || "",
|
|
243
|
+
url,
|
|
244
|
+
description: document.metadata?.description,
|
|
245
|
+
image: document.metadata?.image?.asset?.url,
|
|
246
|
+
datePublished: document.publishedAt,
|
|
247
|
+
dateModified: document._updatedAt,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
@@ -6,6 +6,11 @@ import { fontsVariable } from "@/lib/styles/fonts";
|
|
|
6
6
|
import AppData from "@/package.json";
|
|
7
7
|
import "@/lib/styles/index.css";
|
|
8
8
|
import { cn } from "@/lib/styles/cn";
|
|
9
|
+
import {
|
|
10
|
+
JsonLd,
|
|
11
|
+
generateWebSiteJsonLd,
|
|
12
|
+
generateOrganizationJsonLd,
|
|
13
|
+
} from "@/lib/utils/json-ld";
|
|
9
14
|
|
|
10
15
|
const APP_NAME = AppData.name;
|
|
11
16
|
const APP_DEFAULT_TITLE = "Basement Starter";
|
|
@@ -86,6 +91,19 @@ export default async function Layout({ children }: PropsWithChildren) {
|
|
|
86
91
|
suppressHydrationWarning
|
|
87
92
|
>
|
|
88
93
|
<body>
|
|
94
|
+
<JsonLd
|
|
95
|
+
data={generateWebSiteJsonLd({
|
|
96
|
+
name: APP_DEFAULT_TITLE,
|
|
97
|
+
description: APP_DESCRIPTION,
|
|
98
|
+
})}
|
|
99
|
+
/>
|
|
100
|
+
<JsonLd
|
|
101
|
+
data={generateOrganizationJsonLd({
|
|
102
|
+
name: APP_NAME,
|
|
103
|
+
logo: "/opengraph-image.jpg",
|
|
104
|
+
})}
|
|
105
|
+
/>
|
|
106
|
+
|
|
89
107
|
{/* Skip link for keyboard navigation accessibility */}
|
|
90
108
|
<Suspense fallback={null}>
|
|
91
109
|
<Link
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
export function useMedia(mediaQuery: string, initialValue?: boolean) {
|
|
4
|
+
const [isVerified, setIsVerified] = useState<boolean | undefined>(initialValue)
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (typeof window === "undefined" || !("matchMedia" in window)) {
|
|
8
|
+
console.warn("matchMedia is not supported by your current browser")
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
const mediaQueryList = window.matchMedia(mediaQuery)
|
|
12
|
+
const changeHandler = () => setIsVerified(!!mediaQueryList.matches)
|
|
13
|
+
|
|
14
|
+
changeHandler()
|
|
15
|
+
if (typeof mediaQueryList.addEventListener === "function") {
|
|
16
|
+
mediaQueryList.addEventListener("change", changeHandler)
|
|
17
|
+
return () => {
|
|
18
|
+
mediaQueryList.removeEventListener("change", changeHandler)
|
|
19
|
+
}
|
|
20
|
+
} else if (typeof mediaQueryList.addListener === "function") {
|
|
21
|
+
mediaQueryList.addListener(changeHandler)
|
|
22
|
+
return () => {
|
|
23
|
+
mediaQueryList.removeListener(changeHandler)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}, [mediaQuery])
|
|
27
|
+
|
|
28
|
+
return isVerified
|
|
29
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Article,
|
|
3
|
+
BreadcrumbList,
|
|
4
|
+
Organization,
|
|
5
|
+
SearchAction,
|
|
6
|
+
Thing,
|
|
7
|
+
WebPage,
|
|
8
|
+
WebSite,
|
|
9
|
+
WithContext,
|
|
10
|
+
} from "schema-dts";
|
|
11
|
+
|
|
12
|
+
const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
|
|
13
|
+
|
|
14
|
+
function isAbsoluteUrl(value: string) {
|
|
15
|
+
return /^https?:\/\//.test(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveUrl(value?: string) {
|
|
19
|
+
if (!value) return APP_BASE_URL;
|
|
20
|
+
if (isAbsoluteUrl(value)) return value;
|
|
21
|
+
if (!APP_BASE_URL) return undefined;
|
|
22
|
+
|
|
23
|
+
return new URL(value, APP_BASE_URL).toString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* -------------------------------- Component ------------------------------- */
|
|
27
|
+
|
|
28
|
+
export function JsonLd<T extends Thing>({
|
|
29
|
+
data,
|
|
30
|
+
}: {
|
|
31
|
+
data: WithContext<T>;
|
|
32
|
+
}) {
|
|
33
|
+
return (
|
|
34
|
+
<script
|
|
35
|
+
type="application/ld+json"
|
|
36
|
+
dangerouslySetInnerHTML={{
|
|
37
|
+
__html: JSON.stringify(data).replace(/</g, "\\u003c"),
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* -------------------------------- Generators ------------------------------ */
|
|
44
|
+
|
|
45
|
+
interface WebSiteJsonLdOptions {
|
|
46
|
+
name: string;
|
|
47
|
+
url?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
/** URL to site-wide search (e.g. "/search?q={search_term_string}") */
|
|
50
|
+
searchUrl?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function generateWebSiteJsonLd(
|
|
54
|
+
options: WebSiteJsonLdOptions
|
|
55
|
+
): WithContext<WebSite> {
|
|
56
|
+
const { name, url, description, searchUrl } = options;
|
|
57
|
+
const resolvedUrl = resolveUrl(url);
|
|
58
|
+
const resolvedSearchUrl = resolveUrl(searchUrl);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"@context": "https://schema.org",
|
|
62
|
+
"@type": "WebSite",
|
|
63
|
+
name,
|
|
64
|
+
...(resolvedUrl && { url: resolvedUrl }),
|
|
65
|
+
...(description && { description }),
|
|
66
|
+
...(resolvedSearchUrl && {
|
|
67
|
+
potentialAction: {
|
|
68
|
+
"@type": "SearchAction",
|
|
69
|
+
target: resolvedSearchUrl,
|
|
70
|
+
"query-input": "required name=search_term_string",
|
|
71
|
+
} as SearchAction & { "query-input": string },
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface OrganizationJsonLdOptions {
|
|
77
|
+
name: string;
|
|
78
|
+
url?: string;
|
|
79
|
+
logo?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
sameAs?: string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function generateOrganizationJsonLd(
|
|
85
|
+
options: OrganizationJsonLdOptions
|
|
86
|
+
): WithContext<Organization> {
|
|
87
|
+
const { name, url, logo, description, sameAs } = options;
|
|
88
|
+
const resolvedUrl = resolveUrl(url);
|
|
89
|
+
const resolvedLogo = resolveUrl(logo);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"@context": "https://schema.org",
|
|
93
|
+
"@type": "Organization",
|
|
94
|
+
name,
|
|
95
|
+
...(resolvedUrl && { url: resolvedUrl }),
|
|
96
|
+
...(resolvedLogo && { logo: resolvedLogo }),
|
|
97
|
+
...(description && { description }),
|
|
98
|
+
...(sameAs && { sameAs }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface WebPageJsonLdOptions {
|
|
103
|
+
title: string;
|
|
104
|
+
url?: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
image?: string;
|
|
107
|
+
datePublished?: string;
|
|
108
|
+
dateModified?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function generateWebPageJsonLd(
|
|
112
|
+
options: WebPageJsonLdOptions
|
|
113
|
+
): WithContext<WebPage> {
|
|
114
|
+
const { title, url, description, image, datePublished, dateModified } =
|
|
115
|
+
options;
|
|
116
|
+
const resolvedUrl = resolveUrl(url);
|
|
117
|
+
const resolvedImage = resolveUrl(image);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"@context": "https://schema.org",
|
|
121
|
+
"@type": "WebPage",
|
|
122
|
+
name: title,
|
|
123
|
+
...(resolvedUrl && { url: resolvedUrl }),
|
|
124
|
+
...(description && { description }),
|
|
125
|
+
...(resolvedImage && { image: resolvedImage }),
|
|
126
|
+
...(datePublished && { datePublished }),
|
|
127
|
+
...(dateModified && { dateModified }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ArticleJsonLdOptions {
|
|
132
|
+
title: string;
|
|
133
|
+
url?: string;
|
|
134
|
+
description?: string;
|
|
135
|
+
image?: string;
|
|
136
|
+
datePublished?: string;
|
|
137
|
+
dateModified?: string;
|
|
138
|
+
authorName?: string;
|
|
139
|
+
authorUrl?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function generateArticleJsonLd(
|
|
143
|
+
options: ArticleJsonLdOptions
|
|
144
|
+
): WithContext<Article> {
|
|
145
|
+
const {
|
|
146
|
+
title,
|
|
147
|
+
url,
|
|
148
|
+
description,
|
|
149
|
+
image,
|
|
150
|
+
datePublished,
|
|
151
|
+
dateModified,
|
|
152
|
+
authorName,
|
|
153
|
+
authorUrl,
|
|
154
|
+
} = options;
|
|
155
|
+
const resolvedUrl = resolveUrl(url);
|
|
156
|
+
const resolvedImage = resolveUrl(image);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"@context": "https://schema.org",
|
|
160
|
+
"@type": "Article",
|
|
161
|
+
headline: title,
|
|
162
|
+
...(resolvedUrl && { url: resolvedUrl }),
|
|
163
|
+
...(description && { description }),
|
|
164
|
+
...(resolvedImage && { image: resolvedImage }),
|
|
165
|
+
...(datePublished && { datePublished }),
|
|
166
|
+
...(dateModified && { dateModified }),
|
|
167
|
+
...(authorName && {
|
|
168
|
+
author: {
|
|
169
|
+
"@type": "Person",
|
|
170
|
+
name: authorName,
|
|
171
|
+
...(authorUrl && { url: authorUrl }),
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface BreadcrumbItem {
|
|
178
|
+
name: string;
|
|
179
|
+
url: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function generateBreadcrumbJsonLd(
|
|
183
|
+
items: BreadcrumbItem[]
|
|
184
|
+
): WithContext<BreadcrumbList> {
|
|
185
|
+
return {
|
|
186
|
+
"@context": "https://schema.org",
|
|
187
|
+
"@type": "BreadcrumbList",
|
|
188
|
+
itemListElement: items.map((item, index) => {
|
|
189
|
+
const resolvedItemUrl = resolveUrl(item.url);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"@type": "ListItem",
|
|
193
|
+
position: index + 1,
|
|
194
|
+
name: item.name,
|
|
195
|
+
...(resolvedItemUrl && { item: resolvedItemUrl }),
|
|
196
|
+
};
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"next": "^16",
|
|
27
27
|
"react": "^19",
|
|
28
28
|
"react-dom": "^19",
|
|
29
|
-
"react-use": "^17.6.0",
|
|
30
29
|
"tailwind-merge": "^3.4.0",
|
|
31
30
|
"zod": "^4.3.6"
|
|
32
31
|
},
|
|
@@ -43,6 +42,7 @@
|
|
|
43
42
|
"babel-plugin-react-compiler": "1.0.0",
|
|
44
43
|
"cross-env": "^10.1.0",
|
|
45
44
|
"postcss-preset-env": "^10.6.1",
|
|
45
|
+
"schema-dts": "^2.0.0",
|
|
46
46
|
"tailwindcss": "^4",
|
|
47
47
|
"typescript": "^5"
|
|
48
48
|
},
|
|
@@ -6,6 +6,11 @@ import { fontsVariable } from "@/lib/styles/fonts";
|
|
|
6
6
|
import AppData from "@/package.json";
|
|
7
7
|
import "@/lib/styles/index.css";
|
|
8
8
|
import { cn } from "@/lib/styles/cn";
|
|
9
|
+
import {
|
|
10
|
+
JsonLd,
|
|
11
|
+
generateWebSiteJsonLd,
|
|
12
|
+
generateOrganizationJsonLd,
|
|
13
|
+
} from "@/lib/utils/json-ld";
|
|
9
14
|
|
|
10
15
|
const APP_NAME = AppData.name;
|
|
11
16
|
const APP_DEFAULT_TITLE = "Basement Starter";
|
|
@@ -86,6 +91,19 @@ export default async function Layout({ children }: PropsWithChildren) {
|
|
|
86
91
|
suppressHydrationWarning
|
|
87
92
|
>
|
|
88
93
|
<body>
|
|
94
|
+
<JsonLd
|
|
95
|
+
data={generateWebSiteJsonLd({
|
|
96
|
+
name: APP_DEFAULT_TITLE,
|
|
97
|
+
description: APP_DESCRIPTION,
|
|
98
|
+
})}
|
|
99
|
+
/>
|
|
100
|
+
<JsonLd
|
|
101
|
+
data={generateOrganizationJsonLd({
|
|
102
|
+
name: APP_NAME,
|
|
103
|
+
logo: "/opengraph-image.jpg",
|
|
104
|
+
})}
|
|
105
|
+
/>
|
|
106
|
+
|
|
89
107
|
{/* Skip link for keyboard navigation accessibility */}
|
|
90
108
|
<Suspense fallback={null}>
|
|
91
109
|
<Link
|