bsmnt 0.2.11 → 0.3.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/dist/helpers/create/copy-template.d.ts +1 -1
- package/dist/helpers/create/copy-template.d.ts.map +1 -1
- package/dist/helpers/create/index.d.ts.map +1 -1
- package/dist/helpers/create/index.js +2 -1
- package/dist/helpers/create/index.js.map +1 -1
- package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
- package/dist/helpers/integrate/merge-config.js +0 -2
- 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 +2 -4
- package/dist/helpers/integrate/sanity/config.js.map +1 -1
- package/dist/index.js +84 -35
- package/dist/index.js.map +1 -1
- package/index.js +2 -2
- package/package.json +1 -1
- package/src/templates/next-default/.vscode/settings.json +1 -1
- package/src/templates/next-default/README.md +6 -7
- package/src/templates/next-default/app/layout.tsx +17 -4
- package/src/templates/next-default/biome.json +1 -1
- package/src/templates/next-default/css.d.ts +1 -0
- package/src/templates/next-default/lib/README.md +4 -8
- package/src/templates/next-default/lib/hooks/use-media.ts +3 -1
- package/src/templates/next-default/lib/styles/global.css +182 -0
- package/src/templates/next-default/lib/utils/json-ld.tsx +13 -18
- package/src/templates/next-default/lib/utils/portable-text-to-markdown.ts +83 -0
- package/src/templates/next-default/package.json +3 -3
- package/src/templates/next-experiments/.vscode/settings.json +1 -1
- package/src/templates/next-experiments/README.md +6 -7
- package/src/templates/next-experiments/app/layout.tsx +17 -4
- package/src/templates/next-experiments/biome.json +1 -1
- package/src/templates/next-experiments/css.d.ts +1 -0
- package/src/templates/next-experiments/lib/README.md +4 -8
- package/src/templates/next-experiments/lib/hooks/use-media.ts +3 -1
- package/src/templates/next-experiments/lib/styles/global.css +182 -0
- package/src/templates/next-experiments/lib/utils/json-ld.tsx +13 -18
- package/src/templates/next-experiments/lib/utils/portable-text-to-markdown.ts +83 -0
- package/src/templates/next-experiments/package.json +3 -3
- package/src/templates/next-pagebuilder/.env.example +11 -0
- package/src/templates/next-pagebuilder/README.md +23 -0
- package/src/templates/next-pagebuilder/_gitignore +67 -0
- package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +68 -0
- package/src/templates/next-pagebuilder/app/(content)/layout.tsx +13 -0
- package/src/templates/next-pagebuilder/app/api/[[...slug]]/route.ts +100 -0
- package/src/templates/next-pagebuilder/app/api/draft-mode/disable/route.ts +7 -0
- package/src/templates/next-pagebuilder/app/api/draft-mode/enable/route.ts +20 -0
- package/src/templates/next-pagebuilder/app/api/revalidate/route.ts +121 -0
- package/src/templates/next-pagebuilder/app/favicon.ico +0 -0
- package/src/templates/next-pagebuilder/app/layout.tsx +80 -0
- package/src/templates/next-pagebuilder/app/robots.ts +15 -0
- package/src/templates/next-pagebuilder/app/sitemap.md/route.ts +124 -0
- package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +80 -0
- package/src/templates/next-pagebuilder/app/studio/[[...tool]]/page.tsx +8 -0
- package/src/templates/next-pagebuilder/biome.json +239 -0
- package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +95 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/cta-button.tsx +28 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/mega-menu-panel.tsx +90 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/nav-item-renderer.tsx +98 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/nav-leaf-item.tsx +33 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/types.ts +7 -0
- package/src/templates/next-pagebuilder/components/layout/header/header-client.tsx +110 -0
- package/src/templates/next-pagebuilder/components/layout/header/index.tsx +8 -0
- package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +45 -0
- package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +30 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +83 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +27 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +17 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +17 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-card.tsx +66 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-grid.tsx +42 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +28 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts +16 -0
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +36 -0
- package/src/templates/next-pagebuilder/components/page-builder/types.ts +23 -0
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +91 -0
- package/src/templates/next-pagebuilder/components/sanity/draft-mode-toggle.tsx +27 -0
- package/src/templates/next-pagebuilder/components/sanity/rich-text.tsx +87 -0
- package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +27 -0
- package/src/templates/next-pagebuilder/components/ui/image/index.tsx +216 -0
- package/src/templates/next-pagebuilder/components/ui/link/index.tsx +152 -0
- package/src/templates/next-pagebuilder/components/ui/sanity-image/index.tsx +41 -0
- package/src/templates/next-pagebuilder/lib/integrations/check-integration.ts +5 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/client.ts +27 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-builder-input.tsx +36 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-category-input.tsx +50 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/components/rich-text.tsx +84 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/confirm-publish-action.ts +40 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/env.ts +34 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +35 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/icons.ts +58 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +61 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/page-builder-config.ts +132 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/page-category.ts +28 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +281 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.cli.ts +29 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +211 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/index.ts +4 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/author.ts +70 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/index.ts +96 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/company-data.ts +62 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/navbar.ts +74 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/link.ts +125 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/metadata.ts +68 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page.ts +232 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/singletons.ts +44 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/structure.ts +453 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/image.ts +8 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/link.ts +137 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
- package/src/templates/next-pagebuilder/lib/scripts/sanity-typegen.ts +45 -0
- package/src/templates/next-pagebuilder/lib/styles/cn.ts +5 -0
- package/src/templates/next-pagebuilder/lib/styles/global.css +70 -0
- package/src/templates/next-pagebuilder/lib/utils/base-url.ts +17 -0
- package/src/templates/next-pagebuilder/lib/utils/format-date.ts +8 -0
- package/src/templates/next-pagebuilder/lib/utils/json-ld.tsx +213 -0
- package/src/templates/next-pagebuilder/lib/utils/metadata.ts +167 -0
- package/src/templates/next-pagebuilder/lib/utils/sitemap.ts +37 -0
- package/src/templates/next-pagebuilder/lib/utils/slug-tag.ts +6 -0
- package/src/templates/next-pagebuilder/next.config.ts +134 -0
- package/src/templates/next-pagebuilder/package.json +71 -0
- package/src/templates/next-pagebuilder/postcss.config.mjs +39 -0
- package/src/templates/next-pagebuilder/proxy.ts +81 -0
- package/src/templates/next-pagebuilder/svg.d.ts +5 -0
- package/src/templates/next-pagebuilder/tsconfig.json +38 -0
- package/src/templates/next-webgl/.vscode/settings.json +1 -1
- package/src/templates/next-webgl/README.md +6 -7
- package/src/templates/next-webgl/app/layout.tsx +17 -4
- package/src/templates/next-webgl/biome.json +1 -1
- package/src/templates/next-webgl/css.d.ts +1 -0
- package/src/templates/next-webgl/lib/README.md +4 -8
- package/src/templates/next-webgl/lib/hooks/use-media.ts +3 -1
- package/src/templates/next-webgl/lib/styles/global.css +182 -0
- package/src/templates/next-webgl/lib/utils/json-ld.tsx +13 -18
- package/src/templates/next-webgl/lib/utils/portable-text-to-markdown.ts +83 -0
- package/src/templates/next-webgl/package.json +3 -3
- package/src/helpers/integrate/sanity/files/lib/scripts/copy-sanity-mcp.ts +0 -23
- package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +0 -297
- package/src/templates/next-default/lib/scripts/dev.ts +0 -32
- package/src/templates/next-default/lib/styles/README.md +0 -13
- package/src/templates/next-default/lib/styles/fonts.ts +0 -20
- package/src/templates/next-default/lib/styles/index.css +0 -3
- package/src/templates/next-default/lib/styles/tokens.css +0 -179
- package/src/templates/next-default/lib/utils/README.md +0 -40
- package/src/templates/next-default/lib/utils/easings.ts +0 -240
- package/src/templates/next-default/lib/utils/fetch.ts +0 -84
- package/src/templates/next-default/lib/utils/global-css.d.ts +0 -1
- package/src/templates/next-default/lib/utils/math.ts +0 -236
- package/src/templates/next-default/lib/utils/strings.ts +0 -246
- package/src/templates/next-default/lib/utils/types.d.ts +0 -15
- package/src/templates/next-default/lib/utils/viewport.ts +0 -199
- package/src/templates/next-experiments/lib/scripts/dev.ts +0 -32
- package/src/templates/next-experiments/lib/styles/README.md +0 -13
- package/src/templates/next-experiments/lib/styles/fonts.ts +0 -20
- package/src/templates/next-experiments/lib/styles/index.css +0 -3
- package/src/templates/next-experiments/lib/styles/tokens.css +0 -179
- package/src/templates/next-experiments/lib/utils/README.md +0 -40
- package/src/templates/next-experiments/lib/utils/easings.ts +0 -240
- package/src/templates/next-experiments/lib/utils/fetch.ts +0 -84
- package/src/templates/next-experiments/lib/utils/global-css.d.ts +0 -1
- package/src/templates/next-experiments/lib/utils/math.ts +0 -236
- package/src/templates/next-experiments/lib/utils/strings.ts +0 -246
- package/src/templates/next-experiments/lib/utils/types.d.ts +0 -15
- package/src/templates/next-experiments/lib/utils/viewport.ts +0 -199
- package/src/templates/next-webgl/lib/scripts/dev.ts +0 -32
- package/src/templates/next-webgl/lib/styles/README.md +0 -13
- package/src/templates/next-webgl/lib/styles/fonts.ts +0 -20
- package/src/templates/next-webgl/lib/styles/index.css +0 -3
- package/src/templates/next-webgl/lib/styles/tokens.css +0 -179
- package/src/templates/next-webgl/lib/utils/README.md +0 -40
- package/src/templates/next-webgl/lib/utils/easings.ts +0 -240
- package/src/templates/next-webgl/lib/utils/fetch.ts +0 -84
- package/src/templates/next-webgl/lib/utils/global-css.d.ts +0 -1
- package/src/templates/next-webgl/lib/utils/math.ts +0 -236
- package/src/templates/next-webgl/lib/utils/strings.ts +0 -246
- package/src/templates/next-webgl/lib/utils/types.d.ts +0 -15
- package/src/templates/next-webgl/lib/utils/viewport.ts +0 -199
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Thing,
|
|
7
|
-
WebPage,
|
|
8
|
-
WebSite,
|
|
9
|
-
WithContext,
|
|
10
|
-
} from "schema-dts";
|
|
1
|
+
type JsonLdValue = {
|
|
2
|
+
"@context": "https://schema.org";
|
|
3
|
+
"@type": string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
11
6
|
|
|
12
7
|
const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
|
|
13
8
|
|
|
@@ -25,10 +20,10 @@ function resolveUrl(value?: string) {
|
|
|
25
20
|
|
|
26
21
|
/* -------------------------------- Component ------------------------------- */
|
|
27
22
|
|
|
28
|
-
export function JsonLd<T extends
|
|
23
|
+
export function JsonLd<T extends JsonLdValue>({
|
|
29
24
|
data,
|
|
30
25
|
}: {
|
|
31
|
-
data:
|
|
26
|
+
data: T;
|
|
32
27
|
}) {
|
|
33
28
|
return (
|
|
34
29
|
<script
|
|
@@ -52,7 +47,7 @@ interface WebSiteJsonLdOptions {
|
|
|
52
47
|
|
|
53
48
|
export function generateWebSiteJsonLd(
|
|
54
49
|
options: WebSiteJsonLdOptions
|
|
55
|
-
):
|
|
50
|
+
): JsonLdValue {
|
|
56
51
|
const { name, url, description, searchUrl } = options;
|
|
57
52
|
const resolvedUrl = resolveUrl(url);
|
|
58
53
|
const resolvedSearchUrl = resolveUrl(searchUrl);
|
|
@@ -68,7 +63,7 @@ export function generateWebSiteJsonLd(
|
|
|
68
63
|
"@type": "SearchAction",
|
|
69
64
|
target: resolvedSearchUrl,
|
|
70
65
|
"query-input": "required name=search_term_string",
|
|
71
|
-
}
|
|
66
|
+
},
|
|
72
67
|
}),
|
|
73
68
|
};
|
|
74
69
|
}
|
|
@@ -83,7 +78,7 @@ interface OrganizationJsonLdOptions {
|
|
|
83
78
|
|
|
84
79
|
export function generateOrganizationJsonLd(
|
|
85
80
|
options: OrganizationJsonLdOptions
|
|
86
|
-
):
|
|
81
|
+
): JsonLdValue {
|
|
87
82
|
const { name, url, logo, description, sameAs } = options;
|
|
88
83
|
const resolvedUrl = resolveUrl(url);
|
|
89
84
|
const resolvedLogo = resolveUrl(logo);
|
|
@@ -110,7 +105,7 @@ interface WebPageJsonLdOptions {
|
|
|
110
105
|
|
|
111
106
|
export function generateWebPageJsonLd(
|
|
112
107
|
options: WebPageJsonLdOptions
|
|
113
|
-
):
|
|
108
|
+
): JsonLdValue {
|
|
114
109
|
const { title, url, description, image, datePublished, dateModified } =
|
|
115
110
|
options;
|
|
116
111
|
const resolvedUrl = resolveUrl(url);
|
|
@@ -141,7 +136,7 @@ interface ArticleJsonLdOptions {
|
|
|
141
136
|
|
|
142
137
|
export function generateArticleJsonLd(
|
|
143
138
|
options: ArticleJsonLdOptions
|
|
144
|
-
):
|
|
139
|
+
): JsonLdValue {
|
|
145
140
|
const {
|
|
146
141
|
title,
|
|
147
142
|
url,
|
|
@@ -181,7 +176,7 @@ interface BreadcrumbItem {
|
|
|
181
176
|
|
|
182
177
|
export function generateBreadcrumbJsonLd(
|
|
183
178
|
items: BreadcrumbItem[]
|
|
184
|
-
):
|
|
179
|
+
): JsonLdValue {
|
|
185
180
|
return {
|
|
186
181
|
"@context": "https://schema.org",
|
|
187
182
|
"@type": "BreadcrumbList",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface PortableTextMarkDefinition {
|
|
2
|
+
_key?: string;
|
|
3
|
+
_type?: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PortableTextSpan {
|
|
8
|
+
_type?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
marks?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PortableTextBlock {
|
|
14
|
+
_type?: string;
|
|
15
|
+
style?: string;
|
|
16
|
+
listItem?: "bullet" | "number";
|
|
17
|
+
level?: number;
|
|
18
|
+
children?: PortableTextSpan[];
|
|
19
|
+
markDefs?: PortableTextMarkDefinition[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const headingStyles: Record<string, string> = {
|
|
23
|
+
h1: "#",
|
|
24
|
+
h2: "##",
|
|
25
|
+
h3: "###",
|
|
26
|
+
h4: "####",
|
|
27
|
+
h5: "#####",
|
|
28
|
+
h6: "######",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function applyMarks(
|
|
32
|
+
text: string,
|
|
33
|
+
marks: string[] | undefined,
|
|
34
|
+
markDefs: PortableTextMarkDefinition[]
|
|
35
|
+
) {
|
|
36
|
+
return (marks ?? []).reduce((result, mark) => {
|
|
37
|
+
if (mark === "strong") return `**${result}**`
|
|
38
|
+
if (mark === "em") return `*${result}*`
|
|
39
|
+
if (mark === "code") return `\`${result}\``
|
|
40
|
+
|
|
41
|
+
const link = markDefs.find((definition) => definition._key === mark)
|
|
42
|
+
if (link?._type === "link" && link.href) {
|
|
43
|
+
return `[${result}](${link.href})`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
}, text)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function serializeBlock(block: PortableTextBlock) {
|
|
51
|
+
const text = (block.children ?? [])
|
|
52
|
+
.map((child) =>
|
|
53
|
+
applyMarks(child.text ?? "", child.marks, block.markDefs ?? [])
|
|
54
|
+
)
|
|
55
|
+
.join("")
|
|
56
|
+
.trim()
|
|
57
|
+
|
|
58
|
+
if (!text) return ""
|
|
59
|
+
|
|
60
|
+
if (block.listItem) {
|
|
61
|
+
const level = Math.max((block.level ?? 1) - 1, 0)
|
|
62
|
+
const indent = " ".repeat(level)
|
|
63
|
+
const marker = block.listItem === "number" ? "1." : "-"
|
|
64
|
+
return `${indent}${marker} ${text}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (block.style === "blockquote") {
|
|
68
|
+
return `> ${text}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const headingPrefix = headingStyles[block.style ?? ""]
|
|
72
|
+
if (headingPrefix) {
|
|
73
|
+
return `${headingPrefix} ${text}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return text
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function portableTextToMarkdown(
|
|
80
|
+
value: PortableTextBlock[] | null | undefined
|
|
81
|
+
) {
|
|
82
|
+
return (value ?? []).map(serializeBlock).filter(Boolean).join("\n\n")
|
|
83
|
+
}
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"analyze": "cross-env ANALYZE=true bun run build",
|
|
9
9
|
"analyze:experimental": "next experimental-analyze",
|
|
10
10
|
"build": "next build",
|
|
11
|
-
"dev": "
|
|
12
|
-
"dev:https": "
|
|
13
|
-
"dev:inspect": "
|
|
11
|
+
"dev": "next dev",
|
|
12
|
+
"dev:https": "next dev --experimental-https",
|
|
13
|
+
"dev:inspect": "next dev --inspect",
|
|
14
14
|
"format": "biome format --write .",
|
|
15
15
|
"lighthouse": "bunx @unlighthouse/cli --site http://localhost:3000",
|
|
16
16
|
"lint": "biome lint --max-diagnostics=200",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Page Builder Template
|
|
2
|
+
|
|
3
|
+
Next.js + Sanity CMS page builder architecture. All URL structure is managed in Sanity, not in code.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
- Single catch-all route `[[...slug]]` handles all pages
|
|
14
|
+
- Sanity page builder with composable content blocks
|
|
15
|
+
- Blog with categories, filtering, and pagination
|
|
16
|
+
- Visual editing and live preview support
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
- `bun dev` - Start dev server
|
|
21
|
+
- `bun build` - Production build
|
|
22
|
+
- `bun lint` - Run linter
|
|
23
|
+
- `bun sanity:typegen` - Generate Sanity types
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.turbo
|
|
8
|
+
.npmrc
|
|
9
|
+
.yarn/*
|
|
10
|
+
!.yarn/patches
|
|
11
|
+
!.yarn/plugins
|
|
12
|
+
!.yarn/releases
|
|
13
|
+
!.yarn/versions
|
|
14
|
+
|
|
15
|
+
# testing
|
|
16
|
+
/coverage
|
|
17
|
+
|
|
18
|
+
# next.js
|
|
19
|
+
/.next/
|
|
20
|
+
/out/
|
|
21
|
+
|
|
22
|
+
# TypeScript
|
|
23
|
+
*.tsbuildinfo
|
|
24
|
+
|
|
25
|
+
# production
|
|
26
|
+
/build
|
|
27
|
+
/public/sw.js
|
|
28
|
+
|
|
29
|
+
# misc
|
|
30
|
+
.DS_Store
|
|
31
|
+
*.pem
|
|
32
|
+
.cursor/scratchpad.md
|
|
33
|
+
|
|
34
|
+
# debug
|
|
35
|
+
npm-debug.log*
|
|
36
|
+
yarn-debug.log*
|
|
37
|
+
yarn-error.log*
|
|
38
|
+
|
|
39
|
+
# local env files
|
|
40
|
+
.env
|
|
41
|
+
.env.local
|
|
42
|
+
.env*.local
|
|
43
|
+
.env.development.local
|
|
44
|
+
.env.test.local
|
|
45
|
+
.env.production.local
|
|
46
|
+
|
|
47
|
+
# vercel
|
|
48
|
+
.vercel
|
|
49
|
+
|
|
50
|
+
# tldr
|
|
51
|
+
.tldr
|
|
52
|
+
|
|
53
|
+
# eslint
|
|
54
|
+
.eslintcache
|
|
55
|
+
|
|
56
|
+
# certificates
|
|
57
|
+
certificates
|
|
58
|
+
|
|
59
|
+
next-env.d.ts
|
|
60
|
+
# claude
|
|
61
|
+
.claude
|
|
62
|
+
|
|
63
|
+
# unlighthouse
|
|
64
|
+
.unlighthouse/
|
|
65
|
+
|
|
66
|
+
# storybook
|
|
67
|
+
*storybook.log
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { notFound } from "next/navigation"
|
|
2
|
+
import { client } from "@/lib/integrations/sanity/client"
|
|
3
|
+
import { sanityFetch } from "@/lib/integrations/sanity/live"
|
|
4
|
+
import {
|
|
5
|
+
ALL_PAGE_SLUGS_QUERY,
|
|
6
|
+
PAGE_QUERY,
|
|
7
|
+
} from "@/lib/integrations/sanity/queries"
|
|
8
|
+
import type {
|
|
9
|
+
ALL_PAGE_SLUGS_QUERY_RESULT,
|
|
10
|
+
PAGE_QUERY_RESULT,
|
|
11
|
+
} from "@/lib/integrations/sanity/sanity.types"
|
|
12
|
+
import { generateSanityMetadata } from "@/lib/utils/metadata"
|
|
13
|
+
import { getSlugTag } from "@/lib/utils/slug-tag"
|
|
14
|
+
import { PageDocument } from "@/components/page-document"
|
|
15
|
+
|
|
16
|
+
const getPageDocument = async (slug: string | null, stega = true) =>
|
|
17
|
+
sanityFetch({
|
|
18
|
+
query: PAGE_QUERY,
|
|
19
|
+
params: { slug },
|
|
20
|
+
tags: [getSlugTag(slug)],
|
|
21
|
+
stega,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const toPath = (segments?: string[]) =>
|
|
25
|
+
segments?.filter(Boolean).join("/") || null
|
|
26
|
+
|
|
27
|
+
const getResolvedPage = async (
|
|
28
|
+
path: string | null,
|
|
29
|
+
stega = true
|
|
30
|
+
): Promise<NonNullable<PAGE_QUERY_RESULT> | null> => {
|
|
31
|
+
const { data: page } = await getPageDocument(path, stega)
|
|
32
|
+
return (page as NonNullable<PAGE_QUERY_RESULT> | null) ?? null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const generateStaticParams = async () => {
|
|
36
|
+
if (!client) return []
|
|
37
|
+
|
|
38
|
+
const slugs =
|
|
39
|
+
await client.fetch<ALL_PAGE_SLUGS_QUERY_RESULT>(ALL_PAGE_SLUGS_QUERY)
|
|
40
|
+
|
|
41
|
+
return (slugs ?? []).flatMap((page) => {
|
|
42
|
+
if (!page.slug) return []
|
|
43
|
+
return { slug: page.slug.split("/") }
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Props = { params: Promise<{ slug?: string[] }> }
|
|
48
|
+
|
|
49
|
+
export const generateMetadata = async ({ params }: Props) => {
|
|
50
|
+
const { slug } = await params
|
|
51
|
+
const path = toPath(slug)
|
|
52
|
+
const page = await getResolvedPage(path, false)
|
|
53
|
+
|
|
54
|
+
return page
|
|
55
|
+
? generateSanityMetadata({ document: page, url: path ? `/${path}` : "/" })
|
|
56
|
+
: {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default async function CatchAllPage({ params }: Props) {
|
|
60
|
+
const { slug } = await params
|
|
61
|
+
|
|
62
|
+
const path = toPath(slug)
|
|
63
|
+
const page = await getResolvedPage(path)
|
|
64
|
+
|
|
65
|
+
if (!page) return notFound()
|
|
66
|
+
|
|
67
|
+
return <PageDocument page={page} path={path} />
|
|
68
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { JsonLd } from "@/components/layout/json-ld"
|
|
2
|
+
import { Wrapper } from "@/components/layout/wrapper"
|
|
3
|
+
import { SanityVisualEditing } from "@/components/sanity/visual-editing"
|
|
4
|
+
|
|
5
|
+
const Layout = async ({ children }: { children: React.ReactNode }) => (
|
|
6
|
+
<>
|
|
7
|
+
<JsonLd />
|
|
8
|
+
<Wrapper>{children}</Wrapper>
|
|
9
|
+
<SanityVisualEditing />
|
|
10
|
+
</>
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export default Layout
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration"
|
|
3
|
+
import { sanityFetch } from "@/lib/integrations/sanity/live"
|
|
4
|
+
import { PAGE_QUERY } from "@/lib/integrations/sanity/queries"
|
|
5
|
+
import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
6
|
+
import { renderPageBuilderMarkdown } from "@/lib/integrations/sanity/utils/page-builder-markdown"
|
|
7
|
+
import { getBaseUrl } from "@/lib/utils/base-url"
|
|
8
|
+
|
|
9
|
+
type PageResult = NonNullable<PAGE_QUERY_RESULT>
|
|
10
|
+
|
|
11
|
+
const getSlugPath = (slugParts?: string[]) => {
|
|
12
|
+
if (!slugParts?.length) return null
|
|
13
|
+
|
|
14
|
+
const lastSegment = slugParts.at(-1)
|
|
15
|
+
|
|
16
|
+
if (!lastSegment?.endsWith(".md")) return null
|
|
17
|
+
|
|
18
|
+
const pathSegments = [...slugParts]
|
|
19
|
+
pathSegments[pathSegments.length - 1] = lastSegment.slice(0, -3)
|
|
20
|
+
|
|
21
|
+
const slug = pathSegments.filter(Boolean).join("/")
|
|
22
|
+
if (!slug) return null
|
|
23
|
+
|
|
24
|
+
return slug === "index" ? null : slug
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function GET(
|
|
28
|
+
_request: Request,
|
|
29
|
+
props: { params: Promise<{ slug?: string[] }> }
|
|
30
|
+
) {
|
|
31
|
+
const { slug: rawSlugParts } = await props.params
|
|
32
|
+
const slug = getSlugPath(rawSlugParts)
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
rawSlugParts?.length &&
|
|
36
|
+
slug === null &&
|
|
37
|
+
rawSlugParts.join("/") !== "index.md"
|
|
38
|
+
) {
|
|
39
|
+
return new NextResponse(null, { status: 404 })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!rawSlugParts?.length) {
|
|
43
|
+
return new NextResponse(null, { status: 404 })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!isSanityConfigured()) {
|
|
47
|
+
return new NextResponse("# 404 Not Found\n\nCMS not configured.", {
|
|
48
|
+
status: 404,
|
|
49
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { data } = await sanityFetch({
|
|
54
|
+
query: PAGE_QUERY,
|
|
55
|
+
params: { slug },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const page = data as PageResult | null
|
|
59
|
+
|
|
60
|
+
const baseUrl = getBaseUrl()
|
|
61
|
+
|
|
62
|
+
if (!page) {
|
|
63
|
+
return new NextResponse("# 404 Not Found\n\nPage not found.", {
|
|
64
|
+
status: 404,
|
|
65
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const body = renderPageBuilderMarkdown(page.pageBuilder)
|
|
70
|
+
const canonicalPath = slug ? `/${slug}` : "/"
|
|
71
|
+
|
|
72
|
+
return new NextResponse(
|
|
73
|
+
[
|
|
74
|
+
`# ${page.title}`,
|
|
75
|
+
"",
|
|
76
|
+
page._updatedAt
|
|
77
|
+
? `**Updated:** ${new Date(page._updatedAt).toLocaleDateString()}`
|
|
78
|
+
: null,
|
|
79
|
+
"",
|
|
80
|
+
`Canonical URL: ${baseUrl}${canonicalPath}`,
|
|
81
|
+
"",
|
|
82
|
+
"---",
|
|
83
|
+
"",
|
|
84
|
+
body || "No page builder content yet.",
|
|
85
|
+
"",
|
|
86
|
+
"---",
|
|
87
|
+
"",
|
|
88
|
+
`[View all content](${baseUrl}/sitemap.md)`,
|
|
89
|
+
]
|
|
90
|
+
.filter((part): part is string => part !== null)
|
|
91
|
+
.join("\n"),
|
|
92
|
+
{
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
95
|
+
Vary: "Accept",
|
|
96
|
+
"X-Content-Type-Options": "nosniff",
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import { defineEnableDraftMode } from "next-sanity/draft-mode"
|
|
3
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration"
|
|
4
|
+
import { client } from "@/lib/integrations/sanity/client"
|
|
5
|
+
import { sanityToken } from "@/lib/integrations/sanity/env"
|
|
6
|
+
|
|
7
|
+
const draftModeHandler =
|
|
8
|
+
isSanityConfigured() && client
|
|
9
|
+
? defineEnableDraftMode({
|
|
10
|
+
client: client.withConfig({ token: sanityToken }),
|
|
11
|
+
})
|
|
12
|
+
: {
|
|
13
|
+
GET: () =>
|
|
14
|
+
NextResponse.json(
|
|
15
|
+
{ error: "Sanity is not configured" },
|
|
16
|
+
{ status: 503 }
|
|
17
|
+
),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const { GET } = draftModeHandler
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { revalidatePath, revalidateTag } from "next/cache"
|
|
2
|
+
import { type NextRequest, NextResponse } from "next/server"
|
|
3
|
+
import { parseBody } from "next-sanity/webhook"
|
|
4
|
+
import { client } from "@/lib/integrations/sanity/client"
|
|
5
|
+
import { SANITY_LAYOUT_TAGS } from "@/lib/integrations/sanity/fetchers/layout"
|
|
6
|
+
import { pageBuilderReferenceMembers as pbrm } from "@/lib/integrations/sanity/page-builder-config"
|
|
7
|
+
import {
|
|
8
|
+
P_REF_PB_COMP_QUERY,
|
|
9
|
+
P_REV_SLUGS_QUERY,
|
|
10
|
+
} from "@/lib/integrations/sanity/queries"
|
|
11
|
+
import type { P_REF_PB_COMP_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
12
|
+
import { getPagePath } from "@/lib/utils/sitemap"
|
|
13
|
+
|
|
14
|
+
const ERRORS = {
|
|
15
|
+
BAD_REQ: ["Bad Request", { status: 400 }],
|
|
16
|
+
INVALID_SIG: ["Invalid signature", { status: 401 }],
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
type WebhookSlug = {
|
|
20
|
+
current?: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type WebhookDocument = {
|
|
24
|
+
_id: string
|
|
25
|
+
_type: string
|
|
26
|
+
resolvedSlug?: string | null
|
|
27
|
+
slug?: WebhookSlug | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type MutationOperation = "create" | "update" | "delete"
|
|
31
|
+
|
|
32
|
+
type RevalidateWebhookBody = WebhookDocument & {
|
|
33
|
+
operation?: MutationOperation
|
|
34
|
+
before?: Partial<WebhookDocument> | null
|
|
35
|
+
after?: Partial<WebhookDocument> | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const pageBuilderComponent = new Set<string>(pbrm.map((m) => m.documentType))
|
|
39
|
+
|
|
40
|
+
const normalizeRevalidationSlug = (slug: string | null | undefined) => {
|
|
41
|
+
if (slug === undefined) return null
|
|
42
|
+
if (slug === null) return "/"
|
|
43
|
+
return `/${slug}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const getCanonicalPageSlug = async (documentId: string) => {
|
|
47
|
+
if (!client) return null
|
|
48
|
+
|
|
49
|
+
const pageSlugs = await client.fetch<Array<{ slug: string | null }>>(
|
|
50
|
+
P_REV_SLUGS_QUERY,
|
|
51
|
+
{ documentIds: [documentId] }
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return normalizeRevalidationSlug(
|
|
55
|
+
pageSlugs.find((page) => page.slug !== undefined)?.slug
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const getPagesThatReferenceComp = async (documentId: string) => {
|
|
60
|
+
if (!client) return []
|
|
61
|
+
|
|
62
|
+
const pages = await client.fetch<P_REF_PB_COMP_QUERY_RESULT>(
|
|
63
|
+
P_REF_PB_COMP_QUERY,
|
|
64
|
+
{
|
|
65
|
+
documentId,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return (pages ?? []).map((page) => normalizeRevalidationSlug(page.slug))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const getAffectedPageSlugsForWebhook = async (body: RevalidateWebhookBody) => {
|
|
73
|
+
const pages: Array<string | null> = []
|
|
74
|
+
|
|
75
|
+
if (body.operation === "delete" && body?.before?.slug?.current) {
|
|
76
|
+
revalidateTag(body.before.slug.current, "max")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If the document is a page, get its slug
|
|
80
|
+
if (body._type === "page") {
|
|
81
|
+
const data = await getCanonicalPageSlug(body._id)
|
|
82
|
+
if (data !== null) pages.push(data)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If the document is a page builder component, get the page slugs that reference it
|
|
86
|
+
if (pageBuilderComponent.has(body._type)) {
|
|
87
|
+
const data = await getPagesThatReferenceComp(body._id)
|
|
88
|
+
pages.push(...data)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return pages.filter((page): page is string => page !== null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function POST(request: NextRequest) {
|
|
95
|
+
try {
|
|
96
|
+
const { body, isValidSignature } = await parseBody<RevalidateWebhookBody>(
|
|
97
|
+
request,
|
|
98
|
+
process.env.SANITY_REVALIDATE_SECRET
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (!isValidSignature) return new Response(...ERRORS.INVALID_SIG)
|
|
102
|
+
if (!(body?._type && body._id)) return new Response(...ERRORS.BAD_REQ)
|
|
103
|
+
if (body._id.startsWith("drafts."))
|
|
104
|
+
return NextResponse.json({ status: 200, now: Date.now(), skipped: true })
|
|
105
|
+
|
|
106
|
+
const affectedPageSlugs = await getAffectedPageSlugsForWebhook(body)
|
|
107
|
+
|
|
108
|
+
for (const slug of affectedPageSlugs) {
|
|
109
|
+
revalidatePath(getPagePath(slug))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (SANITY_LAYOUT_TAGS.includes(body?._type)) {
|
|
113
|
+
revalidateTag(body._type, "max")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return NextResponse.json({ status: 200, now: Date.now() })
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error("Revalidation error:", error)
|
|
119
|
+
return new Response("Internal Server Error", { status: 500 })
|
|
120
|
+
}
|
|
121
|
+
}
|
|
Binary file
|