bsmnt 0.2.11 → 0.3.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.
Files changed (115) hide show
  1. package/dist/helpers/create/copy-template.d.ts +1 -1
  2. package/dist/helpers/create/copy-template.d.ts.map +1 -1
  3. package/dist/helpers/create/index.d.ts.map +1 -1
  4. package/dist/helpers/create/index.js +2 -1
  5. package/dist/helpers/create/index.js.map +1 -1
  6. package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
  7. package/dist/helpers/integrate/merge-config.js +0 -2
  8. package/dist/helpers/integrate/merge-config.js.map +1 -1
  9. package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
  10. package/dist/helpers/integrate/sanity/config.js +3 -14
  11. package/dist/helpers/integrate/sanity/config.js.map +1 -1
  12. package/dist/index.js +84 -35
  13. package/dist/index.js.map +1 -1
  14. package/package.json +1 -1
  15. package/src/templates/next-pagebuilder/.env.example +11 -0
  16. package/src/templates/next-pagebuilder/README.md +23 -0
  17. package/src/templates/next-pagebuilder/_gitignore +67 -0
  18. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +68 -0
  19. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +13 -0
  20. package/src/templates/next-pagebuilder/app/api/[[...slug]]/route.ts +100 -0
  21. package/src/templates/next-pagebuilder/app/api/draft-mode/disable/route.ts +7 -0
  22. package/src/templates/next-pagebuilder/app/api/draft-mode/enable/route.ts +20 -0
  23. package/src/templates/next-pagebuilder/app/api/revalidate/route.ts +121 -0
  24. package/src/templates/next-pagebuilder/app/favicon.ico +0 -0
  25. package/src/templates/next-pagebuilder/app/layout.tsx +80 -0
  26. package/src/templates/next-pagebuilder/app/robots.ts +15 -0
  27. package/src/templates/next-pagebuilder/app/sitemap.md/route.ts +124 -0
  28. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +80 -0
  29. package/src/templates/next-pagebuilder/app/studio/[[...tool]]/page.tsx +8 -0
  30. package/src/templates/next-pagebuilder/biome.json +239 -0
  31. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +95 -0
  32. package/src/templates/next-pagebuilder/components/layout/header/components/cta-button.tsx +28 -0
  33. package/src/templates/next-pagebuilder/components/layout/header/components/mega-menu-panel.tsx +90 -0
  34. package/src/templates/next-pagebuilder/components/layout/header/components/nav-item-renderer.tsx +98 -0
  35. package/src/templates/next-pagebuilder/components/layout/header/components/nav-leaf-item.tsx +33 -0
  36. package/src/templates/next-pagebuilder/components/layout/header/components/types.ts +7 -0
  37. package/src/templates/next-pagebuilder/components/layout/header/header-client.tsx +110 -0
  38. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +8 -0
  39. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +45 -0
  40. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +30 -0
  41. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +83 -0
  42. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +27 -0
  43. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +17 -0
  44. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +17 -0
  45. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-card.tsx +66 -0
  46. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-grid.tsx +42 -0
  47. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +28 -0
  48. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts +16 -0
  49. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +36 -0
  50. package/src/templates/next-pagebuilder/components/page-builder/types.ts +23 -0
  51. package/src/templates/next-pagebuilder/components/page-document/index.tsx +91 -0
  52. package/src/templates/next-pagebuilder/components/sanity/draft-mode-toggle.tsx +27 -0
  53. package/src/templates/next-pagebuilder/components/sanity/rich-text.tsx +87 -0
  54. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +27 -0
  55. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +216 -0
  56. package/src/templates/next-pagebuilder/components/ui/link/index.tsx +152 -0
  57. package/src/templates/next-pagebuilder/components/ui/sanity-image/index.tsx +41 -0
  58. package/src/templates/next-pagebuilder/lib/integrations/check-integration.ts +5 -0
  59. package/src/templates/next-pagebuilder/lib/integrations/sanity/client.ts +27 -0
  60. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  61. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-builder-input.tsx +36 -0
  62. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  63. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/rich-text.tsx +84 -0
  64. package/src/templates/next-pagebuilder/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  65. package/src/templates/next-pagebuilder/lib/integrations/sanity/env.ts +34 -0
  66. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +35 -0
  67. package/src/templates/next-pagebuilder/lib/integrations/sanity/icons.ts +58 -0
  68. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +61 -0
  69. package/src/templates/next-pagebuilder/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  70. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-builder-config.ts +132 -0
  71. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-category.ts +28 -0
  72. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +281 -0
  73. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.cli.ts +29 -0
  74. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +211 -0
  75. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/index.ts +4 -0
  76. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  77. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  78. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  79. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  80. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/author.ts +70 -0
  81. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  82. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/index.ts +96 -0
  83. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/company-data.ts +62 -0
  84. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  85. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/navbar.ts +74 -0
  86. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  87. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  88. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/metadata.ts +68 -0
  89. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  90. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
  91. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  92. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page.ts +232 -0
  93. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  94. package/src/templates/next-pagebuilder/lib/integrations/sanity/singletons.ts +44 -0
  95. package/src/templates/next-pagebuilder/lib/integrations/sanity/structure.ts +453 -0
  96. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/image.ts +8 -0
  97. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/link.ts +137 -0
  98. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
  99. package/src/templates/next-pagebuilder/lib/scripts/sanity-typegen.ts +45 -0
  100. package/src/templates/next-pagebuilder/lib/styles/cn.ts +5 -0
  101. package/src/templates/next-pagebuilder/lib/styles/global.css +70 -0
  102. package/src/templates/next-pagebuilder/lib/utils/base-url.ts +17 -0
  103. package/src/templates/next-pagebuilder/lib/utils/format-date.ts +8 -0
  104. package/src/templates/next-pagebuilder/lib/utils/json-ld.tsx +213 -0
  105. package/src/templates/next-pagebuilder/lib/utils/metadata.ts +167 -0
  106. package/src/templates/next-pagebuilder/lib/utils/sitemap.ts +37 -0
  107. package/src/templates/next-pagebuilder/lib/utils/slug-tag.ts +6 -0
  108. package/src/templates/next-pagebuilder/next.config.ts +134 -0
  109. package/src/templates/next-pagebuilder/package.json +71 -0
  110. package/src/templates/next-pagebuilder/postcss.config.mjs +39 -0
  111. package/src/templates/next-pagebuilder/proxy.ts +81 -0
  112. package/src/templates/next-pagebuilder/svg.d.ts +5 -0
  113. package/src/templates/next-pagebuilder/tsconfig.json +38 -0
  114. package/src/helpers/integrate/sanity/files/lib/scripts/copy-sanity-mcp.ts +0 -23
  115. package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +0 -297
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Wrapper around `sanity schema extract && sanity typegen generate`
3
+ * that gracefully skips when Sanity is not configured.
4
+ */
5
+ import { execSync } from "node:child_process"
6
+ import { existsSync } from "node:fs"
7
+ import { resolve } from "node:path"
8
+
9
+ const sanityDir = resolve(import.meta.dir, "../integrations/sanity")
10
+ const envFile = resolve(import.meta.dir, "../../.env.local")
11
+
12
+ // Check if Sanity project ID is configured
13
+ const hasEnvFile = existsSync(envFile)
14
+
15
+ if (!hasEnvFile) {
16
+ console.log(" Skipping sanity:typegen — .env.local not found")
17
+ process.exit(0)
18
+ }
19
+
20
+ // Read .env.local to check for project ID
21
+ const envContent = Bun.file(envFile)
22
+ const text = await envContent.text()
23
+ const hasProjectId = text
24
+ .split("\n")
25
+ .some(
26
+ (line) =>
27
+ line.startsWith("NEXT_PUBLIC_SANITY_PROJECT_ID=") &&
28
+ line.split("=")[1]?.trim() !== "",
29
+ )
30
+
31
+ if (!hasProjectId) {
32
+ console.log(
33
+ " Skipping sanity:typegen — NEXT_PUBLIC_SANITY_PROJECT_ID not set",
34
+ )
35
+ process.exit(0)
36
+ }
37
+
38
+ try {
39
+ execSync(
40
+ `cd ${sanityDir} && bun --env-file ../../../.env.local sanity schema extract && bun --env-file ../../../.env.local sanity typegen generate && bun biome check --write --unsafe`,
41
+ { stdio: "inherit" },
42
+ )
43
+ } catch {
44
+ console.warn(" sanity:typegen failed — continuing anyway")
45
+ }
@@ -0,0 +1,5 @@
1
+ import { cx } from "class-variance-authority"
2
+ import type { ClassValue } from "class-variance-authority/types"
3
+ import { twMerge } from "tailwind-merge"
4
+
5
+ export const cn = (...inputs: ClassValue[]) => twMerge(cx(inputs))
@@ -0,0 +1,70 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ --breakpoint-*: initial;
5
+ --breakpoint-desktop-large: 1920px;
6
+ --breakpoint-desktop: 1440px;
7
+ --breakpoint-tablet-lg: 1024px;
8
+ --breakpoint-tablet: 768px;
9
+ --breakpoint-mobile: 640px;
10
+
11
+ --color-*: initial;
12
+ --color-primary: #ffffff;
13
+ --color-secondary: #000000;
14
+ --color-contrast: #ff4d00;
15
+ --color-black: #000000;
16
+ --color-white: #ffffff;
17
+ --color-orange: #ff4d00;
18
+ --color-blue: #487cff;
19
+ --color-green: #00ff9b;
20
+ --color-violet: #f101a5;
21
+ --color-pink: #ff73a6;
22
+ --color-gray: #666666;
23
+
24
+ /* gray */
25
+ --color-gray-50: #f5f5f5;
26
+ --color-gray-100: #e0e0e0;
27
+ --color-gray-200: #c2c2c2;
28
+ --color-gray-300: #a3a3a3;
29
+ --color-gray-400: #858585;
30
+ --color-gray-500: #666666;
31
+ --color-gray-600: #4d4d4d;
32
+ --color-gray-700: #333333;
33
+ --color-gray-800: #1a1a1a;
34
+ }
35
+
36
+ @theme inline {
37
+ --font-geist: var(--font-geist-variable);
38
+ }
39
+
40
+ html {
41
+ --scrollbar-gutter: 0px;
42
+ scrollbar-gutter: stable;
43
+ }
44
+
45
+ body {
46
+ -webkit-font-smoothing: antialiased;
47
+ -moz-osx-font-smoothing: grayscale;
48
+
49
+ @apply flex flex-col bg-primary text-secondary overscroll-none min-h-svh;
50
+ }
51
+
52
+ *:focus-visible {
53
+ outline: 2px solid var(--color-contrast);
54
+ @apply outline-2 outline-contrast;
55
+ }
56
+
57
+ img {
58
+ -webkit-touch-callout: none;
59
+ -webkit-user-select: none;
60
+ -moz-user-select: none;
61
+ -ms-user-select: none;
62
+ -o-user-select: none;
63
+ user-select: none;
64
+ }
65
+
66
+ button {
67
+ cursor: pointer;
68
+ user-select: none;
69
+ }
70
+
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Centralized base URL resolution.
3
+ *
4
+ * Priority:
5
+ * 1. NEXT_PUBLIC_BASE_URL (explicit production URL)
6
+ * 2. VERCEL_PROJECT_PRODUCTION_URL (Vercel production domain)
7
+ * 3. VERCEL_URL (Vercel preview/deployment URL)
8
+ * 4. http://localhost:3000 (local development)
9
+ */
10
+ export const getBaseUrl = () =>
11
+ process.env.NEXT_PUBLIC_BASE_URL ||
12
+ (process.env.VERCEL_PROJECT_PRODUCTION_URL
13
+ ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
14
+ : undefined) ||
15
+ (process.env.VERCEL_URL
16
+ ? `https://${process.env.VERCEL_URL}`
17
+ : "http://localhost:3000")
@@ -0,0 +1,8 @@
1
+ const options: Intl.DateTimeFormatOptions = {
2
+ year: "numeric",
3
+ month: "long",
4
+ day: "numeric",
5
+ }
6
+
7
+ export const formatDate = (date: Date | string | number) =>
8
+ new Date(date).toLocaleDateString("en-US", options)
@@ -0,0 +1,213 @@
1
+ import type {
2
+ Article,
3
+ BreadcrumbList,
4
+ Organization,
5
+ Thing,
6
+ WebPage,
7
+ WebSite,
8
+ WithContext,
9
+ } from "schema-dts"
10
+ import { getBaseUrl } from "./base-url"
11
+
12
+ const BASE_URL = getBaseUrl()
13
+
14
+ interface JsonLdProps<T extends Thing> {
15
+ data: WithContext<T>
16
+ }
17
+
18
+ const serializeJsonLd = <T extends Thing>(data: WithContext<T>) =>
19
+ JSON.stringify(data).replace(/</g, "\\u003c")
20
+
21
+ export const JsonLd = <T extends Thing>({ data }: JsonLdProps<T>) => (
22
+ <script
23
+ type="application/ld+json"
24
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD must be emitted as raw script text, and the payload is serialized and `<`-escaped first.
25
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
26
+ />
27
+ )
28
+
29
+ interface WebSiteJsonLdOptions {
30
+ name: string
31
+ url?: string
32
+ description?: string
33
+ }
34
+
35
+ export const generateWebSiteJsonLd = (
36
+ options: WebSiteJsonLdOptions
37
+ ): WithContext<WebSite> => {
38
+ const { name, url = BASE_URL, description } = options
39
+ return {
40
+ "@context": "https://schema.org",
41
+ "@type": "WebSite",
42
+ name,
43
+ ...(url ? { url } : {}),
44
+ ...(description && { description }),
45
+ }
46
+ }
47
+
48
+ interface OrganizationJsonLdOptions {
49
+ name: string
50
+ url?: string
51
+ logo?: string
52
+ description?: string
53
+ sameAs?: string[]
54
+ }
55
+
56
+ export const generateOrganizationJsonLd = (
57
+ options: OrganizationJsonLdOptions
58
+ ): WithContext<Organization> => {
59
+ const { name, url = BASE_URL, logo, description, sameAs } = options
60
+
61
+ return {
62
+ "@context": "https://schema.org",
63
+ "@type": "Organization",
64
+ name,
65
+ ...(url ? { url } : {}),
66
+ ...(logo && { logo: `${BASE_URL}${logo}` }),
67
+ ...(description && { description }),
68
+ ...(sameAs && { sameAs }),
69
+ }
70
+ }
71
+
72
+ interface WebPageJsonLdOptions {
73
+ title: string
74
+ url?: string
75
+ description?: string
76
+ image?: string
77
+ datePublished?: string
78
+ dateModified?: string
79
+ }
80
+
81
+ export const generateWebPageJsonLd = (
82
+ options: WebPageJsonLdOptions
83
+ ): WithContext<WebPage> => {
84
+ const { title, url, description, image, datePublished, dateModified } =
85
+ options
86
+ const fullUrl = url ? `${BASE_URL}${url}` : BASE_URL
87
+
88
+ return {
89
+ "@context": "https://schema.org",
90
+ "@type": "WebPage",
91
+ name: title,
92
+ ...(fullUrl ? { url: fullUrl } : {}),
93
+ ...(description && { description }),
94
+ ...(image && { image }),
95
+ ...(datePublished && { datePublished }),
96
+ ...(dateModified && { dateModified }),
97
+ }
98
+ }
99
+
100
+ interface ArticleJsonLdOptions {
101
+ title: string
102
+ url?: string
103
+ description?: string
104
+ image?: string
105
+ datePublished?: string
106
+ dateModified?: string
107
+ authorName?: string
108
+ authorUrl?: string
109
+ }
110
+
111
+ export function generateArticleJsonLd(
112
+ options: ArticleJsonLdOptions
113
+ ): WithContext<Article> {
114
+ const {
115
+ title,
116
+ url,
117
+ description,
118
+ image,
119
+ datePublished,
120
+ dateModified,
121
+ authorName,
122
+ authorUrl,
123
+ } = options
124
+ const fullUrl = url ? `${BASE_URL}${url}` : BASE_URL
125
+ return {
126
+ "@context": "https://schema.org",
127
+ "@type": "Article",
128
+ headline: title,
129
+ ...(fullUrl ? { url: fullUrl } : {}),
130
+ ...(description && { description }),
131
+ ...(image && { image }),
132
+ ...(datePublished && { datePublished }),
133
+ ...(dateModified && { dateModified }),
134
+ ...(authorName && {
135
+ author: {
136
+ "@type": "Person",
137
+ name: authorName,
138
+ ...(authorUrl && { url: authorUrl }),
139
+ },
140
+ }),
141
+ }
142
+ }
143
+
144
+ interface BreadcrumbItem {
145
+ name: string
146
+ url: string
147
+ }
148
+
149
+ export function generateBreadcrumbJsonLd(
150
+ items: BreadcrumbItem[]
151
+ ): WithContext<BreadcrumbList> {
152
+ return {
153
+ "@context": "https://schema.org",
154
+ "@type": "BreadcrumbList",
155
+ itemListElement: items.map((item, index) => ({
156
+ "@type": "ListItem" as const,
157
+ position: index + 1,
158
+ name: item.name,
159
+ item: `${BASE_URL}${item.url}`,
160
+ })),
161
+ }
162
+ }
163
+
164
+ /* ——————————————————————————————————————————————————————————
165
+ * Sanity helpers
166
+ *
167
+ * Accept flat options instead of a document shape so they
168
+ * work with any Sanity type without interface mismatches.
169
+ * —————————————————————————————————————————————————————————— */
170
+
171
+ interface SanityArticleJsonLdOptions {
172
+ title: string
173
+ url: string
174
+ description?: string | null | undefined
175
+ image?: string | null | undefined
176
+ datePublished?: string | null | undefined
177
+ dateModified?: string
178
+ authorName?: string | null | undefined
179
+ authorUrl?: string | null | undefined
180
+ }
181
+
182
+ export function generateSanityArticleJsonLd(
183
+ options: SanityArticleJsonLdOptions
184
+ ): WithContext<Article> {
185
+ const opts: ArticleJsonLdOptions = { title: options.title, url: options.url }
186
+ if (options.description) opts.description = options.description
187
+ if (options.image) opts.image = options.image
188
+ if (options.datePublished) opts.datePublished = options.datePublished
189
+ if (options.dateModified) opts.dateModified = options.dateModified
190
+ if (options.authorName) opts.authorName = options.authorName
191
+ if (options.authorUrl) opts.authorUrl = options.authorUrl
192
+ return generateArticleJsonLd(opts)
193
+ }
194
+
195
+ interface SanityWebPageJsonLdOptions {
196
+ title: string
197
+ url: string
198
+ description?: string | null | undefined
199
+ image?: string | null | undefined
200
+ datePublished?: string | null | undefined
201
+ dateModified?: string
202
+ }
203
+
204
+ export function generateSanityWebPageJsonLd(
205
+ options: SanityWebPageJsonLdOptions
206
+ ): WithContext<WebPage> {
207
+ const opts: WebPageJsonLdOptions = { title: options.title, url: options.url }
208
+ if (options.description) opts.description = options.description
209
+ if (options.image) opts.image = options.image
210
+ if (options.datePublished) opts.datePublished = options.datePublished
211
+ if (options.dateModified) opts.dateModified = options.dateModified
212
+ return generateWebPageJsonLd(opts)
213
+ }
@@ -0,0 +1,167 @@
1
+ import type { Metadata } from "next"
2
+ import { urlForImage } from "@/lib/integrations/sanity/utils/image"
3
+ import { getBaseUrl } from "./base-url"
4
+
5
+ interface GenerateMetadataOptions {
6
+ title?: string | undefined
7
+ description?: string | undefined
8
+ keywords?: string[] | undefined
9
+ image?:
10
+ | {
11
+ url?: string | undefined
12
+ width?: number | undefined
13
+ height?: number | undefined
14
+ }
15
+ | undefined
16
+ url?: string | undefined
17
+ siteName?: string | undefined
18
+ index?: boolean | undefined
19
+ noIndex?: boolean | undefined
20
+ type?: "website" | "article"
21
+ publishedTime?: string | undefined
22
+ modifiedTime?: string | undefined
23
+ authors?: string[] | undefined
24
+ }
25
+
26
+ interface SanityMetadataImage {
27
+ asset?: {
28
+ _ref?: string
29
+ _type?: "reference"
30
+ }
31
+ }
32
+
33
+ interface SanityPageMetadata {
34
+ metaTitle?: string | null
35
+ metaDescription?: string | null
36
+ og?: {
37
+ image?: SanityMetadataImage | null
38
+ } | null
39
+ index?: boolean | null
40
+ title?: string | null
41
+ description?: string | null
42
+ image?: SanityMetadataImage | null
43
+ noIndex?: boolean | null
44
+ }
45
+
46
+ interface SanityMetadataDocument {
47
+ title?: string | null
48
+ subtitle?: string | null
49
+ metadata?: SanityPageMetadata | null
50
+ }
51
+
52
+ interface GenerateSanityMetadataOptions {
53
+ document: SanityMetadataDocument
54
+ url?: string
55
+ siteName?: string
56
+ type?: "website" | "article"
57
+ }
58
+
59
+ const APP_BASE_URL = getBaseUrl()
60
+
61
+ /**
62
+ * Generate complete metadata object for pages
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * export async function generateMetadata({ params }) {
67
+ * const page = await fetchPage(params.slug)
68
+ *
69
+ * return generatePageMetadata({
70
+ * title: page.metadata?.title || page.title,
71
+ * description: page.metadata?.description,
72
+ * image: { url: page.metadata?.image?.asset?.url },
73
+ * url: `/page/${params.slug}`,
74
+ * noIndex: page.metadata?.noIndex,
75
+ * })
76
+ * }
77
+ * ```
78
+ */
79
+ const generatePageMetadata = (options: GenerateMetadataOptions): Metadata => {
80
+ const {
81
+ title,
82
+ description,
83
+ keywords,
84
+ image,
85
+ url,
86
+ siteName = "Basement",
87
+ index,
88
+ noIndex = false,
89
+ type = "website",
90
+ publishedTime,
91
+ modifiedTime,
92
+ authors,
93
+ } = options
94
+
95
+ const shouldIndex = index ?? !noIndex
96
+ const fullUrl = url ? `${APP_BASE_URL}${url}` : APP_BASE_URL
97
+ const imageUrl = image?.url || "/opengraph-image.jpg"
98
+ const width = image?.width || 1200
99
+ const height = image?.height || 630
100
+
101
+ const metadata: Metadata = {
102
+ metadataBase: new URL(APP_BASE_URL),
103
+ title,
104
+ description,
105
+ keywords,
106
+ alternates: { canonical: url || "/" },
107
+ openGraph: {
108
+ title,
109
+ description,
110
+ url: fullUrl,
111
+ siteName,
112
+ locale: "en_US",
113
+ type,
114
+ images: [{ url: imageUrl, width, height }],
115
+ ...(publishedTime && { publishedTime }),
116
+ ...(modifiedTime && { modifiedTime }),
117
+ ...(authors && { authors }),
118
+ },
119
+ twitter: {
120
+ card: "summary_large_image",
121
+ title,
122
+ description,
123
+ images: [{ url: imageUrl, width, height }],
124
+ },
125
+ other: { "fb:app_id": process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "" },
126
+ }
127
+
128
+ if (!shouldIndex) metadata.robots = { index: false, follow: false }
129
+
130
+ return metadata
131
+ }
132
+
133
+ export const generateSanityMetadata = ({
134
+ document,
135
+ url,
136
+ siteName,
137
+ type = "website",
138
+ }: GenerateSanityMetadataOptions): Metadata => {
139
+ const pageMetadata = document.metadata
140
+ const title =
141
+ pageMetadata?.metaTitle ||
142
+ pageMetadata?.title ||
143
+ document.title ||
144
+ undefined
145
+ const description =
146
+ pageMetadata?.metaDescription ||
147
+ pageMetadata?.description ||
148
+ document.subtitle ||
149
+ undefined
150
+ const socialImageSource = pageMetadata?.image || pageMetadata?.og?.image
151
+ const imageUrl = socialImageSource
152
+ ? urlForImage(socialImageSource).width(1200).height(630).url()
153
+ : undefined
154
+ const index =
155
+ pageMetadata?.index ??
156
+ (pageMetadata?.noIndex != null ? !pageMetadata.noIndex : true)
157
+
158
+ return generatePageMetadata({
159
+ title,
160
+ description,
161
+ image: imageUrl ? { url: imageUrl, width: 1200, height: 630 } : undefined,
162
+ url,
163
+ siteName,
164
+ index,
165
+ type,
166
+ })
167
+ }
@@ -0,0 +1,37 @@
1
+ import { PAGE_SITEMAP_QUERY } from "@/lib/integrations/sanity/queries"
2
+ import type { PAGE_SITEMAP_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
3
+ import { getBaseUrl } from "./base-url"
4
+
5
+ export const getSitemapBaseUrl = getBaseUrl
6
+
7
+ const getNormalizedPageSlug = (slug: string | null) => {
8
+ if (!slug || slug === "/") return ""
9
+ return slug.replace(/^\/+|\/+$/g, "")
10
+ }
11
+
12
+ export const getPagePath = (slug: string | null) => {
13
+ const normalizedSlug = getNormalizedPageSlug(slug)
14
+ return normalizedSlug ? `/${normalizedSlug}` : "/"
15
+ }
16
+
17
+ export const getPageUrl = (baseUrl: string, slug: string | null) =>
18
+ new URL(getPagePath(slug), baseUrl).toString()
19
+
20
+ export const getMarkdownPath = (baseUrl: string, slug: string | null) => {
21
+ const normalizedSlug = getNormalizedPageSlug(slug)
22
+
23
+ return normalizedSlug
24
+ ? `${baseUrl}/${normalizedSlug}.md`
25
+ : `${baseUrl}/index.md`
26
+ }
27
+
28
+ export const getSitemap = async () => {
29
+ const sanityModule = await import("@/lib/integrations/sanity/client")
30
+ const client = sanityModule?.client
31
+
32
+ if (!client) return null
33
+
34
+ const p = await client.fetch<PAGE_SITEMAP_QUERY_RESULT>(PAGE_SITEMAP_QUERY)
35
+
36
+ return p
37
+ }
@@ -0,0 +1,6 @@
1
+ export const getSlugTag = (slug: string | null | undefined) => {
2
+ const normalizedSlug = slug?.replace(/^\/+|\/+$/g, "") ?? ""
3
+ const lastSlugSegment = normalizedSlug.split("/").filter(Boolean).at(-1)
4
+
5
+ return lastSlugSegment ?? "homepage"
6
+ }
@@ -0,0 +1,134 @@
1
+ import withBundleAnalyzer from "@next/bundle-analyzer"
2
+ import type { NextConfig } from "next"
3
+
4
+ const nextConfig: NextConfig = {
5
+ reactStrictMode: true,
6
+ reactCompiler: true,
7
+ typedRoutes: true,
8
+ turbopack: {
9
+ rules: {
10
+ "*.svg": {
11
+ loaders: [
12
+ {
13
+ loader: "@svgr/webpack",
14
+ options: {
15
+ memo: true,
16
+ dimensions: false,
17
+ svgoConfig: {
18
+ multipass: true,
19
+ plugins: [
20
+ "removeDimensions",
21
+ "removeOffCanvasPaths",
22
+ "reusePaths",
23
+ "removeElementsByAttr",
24
+ "removeStyleElement",
25
+ "removeScriptElement",
26
+ "prefixIds",
27
+ "cleanupIds",
28
+ {
29
+ name: "cleanupNumericValues",
30
+ params: {
31
+ floatPrecision: 1,
32
+ },
33
+ },
34
+ {
35
+ name: "convertPathData",
36
+ params: {
37
+ floatPrecision: 1,
38
+ },
39
+ },
40
+ {
41
+ name: "convertTransform",
42
+ params: {
43
+ floatPrecision: 1,
44
+ },
45
+ },
46
+ {
47
+ name: "cleanupListOfValues",
48
+ params: {
49
+ floatPrecision: 1,
50
+ },
51
+ },
52
+ ],
53
+ },
54
+ },
55
+ },
56
+ ],
57
+ as: "*.js",
58
+ },
59
+ },
60
+ },
61
+ logging: {
62
+ browserToTerminal: true,
63
+ },
64
+ experimental: {
65
+ optimizePackageImports: [
66
+ "@react-three/drei",
67
+ "@react-three/fiber",
68
+ "gsap",
69
+ "three",
70
+ "postprocessing",
71
+ "lenis",
72
+ ],
73
+ },
74
+ images: {
75
+ dangerouslyAllowSVG: true,
76
+ remotePatterns: [
77
+ {
78
+ protocol: "https",
79
+ hostname: "cdn.sanity.io",
80
+ },
81
+ ],
82
+ minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
83
+ qualities: [90],
84
+ formats: ["image/avif", "image/webp"],
85
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
86
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
87
+ },
88
+ headers: async () => [
89
+ {
90
+ source: "/(.*)",
91
+ headers: [
92
+ {
93
+ key: "X-Content-Type-Options",
94
+ value: "nosniff",
95
+ },
96
+ {
97
+ key: "Content-Security-Policy",
98
+ value: "frame-ancestors 'self';",
99
+ },
100
+ {
101
+ key: "X-Frame-Options",
102
+ value: "SAMEORIGIN",
103
+ },
104
+ {
105
+ key: "X-XSS-Protection",
106
+ value: "1; mode=block",
107
+ },
108
+ {
109
+ key: "X-DNS-Prefetch-Control",
110
+ value: "on",
111
+ },
112
+ {
113
+ key: "Strict-Transport-Security",
114
+ value: "max-age=63072000; includeSubDomains; preload",
115
+ },
116
+ {
117
+ key: "Permissions-Policy",
118
+ value: "camera=(), microphone=(), geolocation=()",
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ }
124
+
125
+ const bundleAnalyzerPlugin = withBundleAnalyzer({
126
+ enabled: process.env.ANALYZE === "true",
127
+ })
128
+
129
+ const NextApp = () => {
130
+ const plugins = [bundleAnalyzerPlugin]
131
+ return plugins.reduce((config, plugin) => plugin(config), nextConfig)
132
+ }
133
+
134
+ export default NextApp