bsmnt 0.3.3 → 0.4.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.
Files changed (70) hide show
  1. package/dist/helpers/integrate/merge-config.js +1 -1
  2. package/dist/helpers/integrate/merge-config.js.map +1 -1
  3. package/dist/helpers/integrate/merge-orchestrator.d.ts.map +1 -1
  4. package/dist/helpers/integrate/merge-orchestrator.js +5 -5
  5. package/dist/helpers/integrate/merge-orchestrator.js.map +1 -1
  6. package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
  7. package/dist/helpers/integrate/sanity/config.js +2 -1
  8. package/dist/helpers/integrate/sanity/config.js.map +1 -1
  9. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.d.ts +1 -3
  10. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.d.ts.map +1 -1
  11. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js +117 -76
  12. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +2 -1
  15. package/src/helpers/integrate/sanity/files/app/api/revalidate/route.ts +4 -1
  16. package/src/helpers/integrate/sanity/files/app/blog/[slug]/page.tsx +3 -1
  17. package/src/helpers/integrate/sanity/files/app/layout.tsx +2 -2
  18. package/src/helpers/integrate/sanity/files/app/sitemap.md/route.ts +29 -18
  19. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/WEBHOOK-SETUP.md +74 -0
  20. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/env.ts +4 -2
  21. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/queries.ts +42 -0
  22. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sitemap.ts +90 -0
  23. package/src/helpers/integrate/sanity/files/lib/utils/metadata.ts +2 -2
  24. package/src/helpers/integrate/sanity/files/lib/utils/url.ts +23 -0
  25. package/src/templates/next-default/.env.example +3 -3
  26. package/src/templates/next-default/app/layout.tsx +2 -2
  27. package/src/templates/next-default/app/robots.ts +2 -2
  28. package/src/templates/next-default/app/sitemap.xml/route.ts +51 -0
  29. package/src/templates/next-default/lib/utils/metadata.ts +2 -2
  30. package/src/templates/next-default/lib/utils/url.ts +16 -0
  31. package/src/templates/next-experiments/.env.example +3 -3
  32. package/src/templates/next-experiments/app/layout.tsx +2 -2
  33. package/src/templates/next-experiments/app/robots.ts +0 -4
  34. package/src/templates/next-experiments/lib/utils/metadata.ts +2 -2
  35. package/src/templates/next-experiments/lib/utils/url.ts +16 -0
  36. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +17 -8
  37. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +19 -7
  38. package/src/templates/next-pagebuilder/app/actions/refresh.ts +5 -0
  39. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +15 -19
  40. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -5
  41. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +11 -10
  42. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +14 -4
  43. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-card.tsx +3 -5
  44. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +93 -0
  45. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-grid.tsx +7 -9
  46. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  47. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +212 -0
  48. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/types.ts +5 -4
  49. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +13 -5
  50. package/src/templates/next-pagebuilder/components/page-document/index.tsx +9 -4
  51. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +2 -1
  52. package/src/templates/next-pagebuilder/lib/integrations/sanity/constants.ts +1 -0
  53. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +17 -18
  54. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +29 -2
  55. package/src/templates/next-pagebuilder/lib/integrations/sanity/presentation.ts +118 -0
  56. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +144 -31
  57. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +5 -100
  58. package/src/templates/next-pagebuilder/next.config.ts +3 -0
  59. package/src/templates/next-pagebuilder/package.json +1 -2
  60. package/src/templates/next-webgl/.env.example +3 -3
  61. package/src/templates/next-webgl/app/layout.tsx +2 -2
  62. package/src/templates/next-webgl/app/robots.ts +2 -2
  63. package/src/templates/next-webgl/app/sitemap.xml/route.ts +51 -0
  64. package/src/templates/next-webgl/lib/utils/metadata.ts +2 -2
  65. package/src/templates/next-webgl/lib/utils/url.ts +16 -0
  66. package/src/helpers/integrate/sanity/files/app/sitemap.ts +0 -61
  67. package/src/templates/next-default/app/sitemap.ts +0 -16
  68. package/src/templates/next-experiments/app/sitemap.ts +0 -16
  69. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +0 -28
  70. package/src/templates/next-webgl/app/sitemap.ts +0 -16
@@ -0,0 +1,118 @@
1
+ import type { Observable } from "rxjs"
2
+ import { map } from "rxjs/operators"
3
+ import type { DocumentStore } from "sanity"
4
+ import {
5
+ type DocumentLocationsState,
6
+ defineDocuments,
7
+ presentationTool,
8
+ } from "sanity/presentation"
9
+ import { HOMEPAGE_DOCUMENT_ID } from "./constants"
10
+ import { previewURL } from "./env"
11
+
12
+ const MAX_FOLDER_DEPTH = 6
13
+
14
+ const buildNestedSlugExpression = (
15
+ parentReferenceField: string,
16
+ leafSlugField: string
17
+ ) => {
18
+ const cases = Array.from({ length: MAX_FOLDER_DEPTH }, (_, index) => {
19
+ const depth = MAX_FOLDER_DEPTH - index
20
+ const parentSlugRefs = Array.from({ length: depth }, (_, parentIndex) => {
21
+ const remainingDepth = depth - parentIndex - 1
22
+ return `${parentReferenceField}${"->parentFolder".repeat(remainingDepth)}->slug.current`
23
+ })
24
+
25
+ const pathDefinedChecks = parentSlugRefs
26
+ .map((field) => `defined(${field})`)
27
+ .join(" && ")
28
+
29
+ const childPath = [...parentSlugRefs, leafSlugField].join(' + "/" + ')
30
+ const folderRootPath = parentSlugRefs.join(' + "/" + ')
31
+
32
+ return [
33
+ `${pathDefinedChecks} && ${leafSlugField} == "/" => ${folderRootPath}`,
34
+ `${pathDefinedChecks} && defined(${leafSlugField}) => ${childPath}`,
35
+ ]
36
+ }).flat()
37
+
38
+ return `select(${cases.join(",\n ")},${leafSlugField})`
39
+ }
40
+
41
+ const pageResolvedSlugExpression = buildNestedSlugExpression(
42
+ "pageFolder",
43
+ "slug.current"
44
+ )
45
+
46
+ const CONTENT_TYPE_PREFIXES: Record<string, string> = {
47
+ blogContent: "blog",
48
+ }
49
+
50
+ const resolveSlugLocation = <T extends { title?: string }>(
51
+ documentStore: DocumentStore,
52
+ id: string,
53
+ projection: string,
54
+ toHref: (doc: T) => string | null,
55
+ fallbackTitle: string
56
+ ): Observable<DocumentLocationsState> =>
57
+ documentStore
58
+ .listenQuery(`*[_id == $id][0]{ ${projection} }`, { id }, {})
59
+ .pipe(
60
+ map((doc: T | null) => {
61
+ const href = doc ? toHref(doc) : null
62
+ if (!href) return { locations: [] }
63
+ return { locations: [{ title: doc?.title || fallbackTitle, href }] }
64
+ })
65
+ )
66
+
67
+ export const presentation = presentationTool({
68
+ name: "preview",
69
+ title: "Preview",
70
+ resolve: {
71
+ mainDocuments: defineDocuments([
72
+ {
73
+ route: "/",
74
+ filter: `_type == "page" && _id == "${HOMEPAGE_DOCUMENT_ID}"`,
75
+ },
76
+ ...Object.entries(CONTENT_TYPE_PREFIXES).map(([type, prefix]) => ({
77
+ route: `/${prefix}/:slug`,
78
+ filter: `_type == "${type}" && slug.current == $slug`,
79
+ })),
80
+ {
81
+ route: "/:slug+",
82
+ filter: `_type == "page" && ${pageResolvedSlugExpression} == $slug`,
83
+ },
84
+ ]),
85
+ locations: (params, { documentStore }) => {
86
+ if (params.type === "page") {
87
+ if (params.id === HOMEPAGE_DOCUMENT_ID) {
88
+ return { locations: [{ title: "Homepage", href: "/" }] }
89
+ }
90
+ return resolveSlugLocation<{ title?: string; resolvedSlug?: string }>(
91
+ documentStore,
92
+ params.id,
93
+ `title, "resolvedSlug": ${pageResolvedSlugExpression}`,
94
+ (doc) => (doc.resolvedSlug ? `/${doc.resolvedSlug}` : null),
95
+ "Page"
96
+ )
97
+ }
98
+
99
+ const prefix = CONTENT_TYPE_PREFIXES[params.type]
100
+ if (!prefix) return null
101
+
102
+ return resolveSlugLocation<{ title?: string; slug?: string }>(
103
+ documentStore,
104
+ params.id,
105
+ `title, "slug": slug.current`,
106
+ (doc) => (doc.slug ? `/${prefix}/${doc.slug}` : null),
107
+ "Untitled"
108
+ )
109
+ },
110
+ },
111
+ previewUrl: {
112
+ origin: previewURL,
113
+ draftMode: {
114
+ enable: "/api/draft-mode/enable",
115
+ disable: "/api/draft-mode/disable",
116
+ },
117
+ },
118
+ })
@@ -97,6 +97,13 @@ const pageProjection = `
97
97
  _updatedAt
98
98
  `
99
99
 
100
+ const pageMetadataProjection = `
101
+ _id,
102
+ title,
103
+ metadata,
104
+ _updatedAt
105
+ `
106
+
100
107
  export const PAGE_QUERY = defineQuery(`
101
108
  *[
102
109
  _type == "page" &&
@@ -109,6 +116,18 @@ export const PAGE_QUERY = defineQuery(`
109
116
  }
110
117
  `)
111
118
 
119
+ export const PAGE_METADATA_QUERY = defineQuery(`
120
+ *[
121
+ _type == "page" &&
122
+ (
123
+ ($slug == null && !defined(pageFolder._ref) && (!defined(slug.current) || slug.current == "" || slug.current == "/")) ||
124
+ ${pageResolvedSlugExpression} == $slug
125
+ )
126
+ ][0] {
127
+ ${pageMetadataProjection}
128
+ }
129
+ `)
130
+
112
131
  export const ALL_PAGE_SLUGS_QUERY = defineQuery(`
113
132
  *[_type == "page" && (defined(pageFolder->slug.current) || defined(slug.current))] | order(title asc) {
114
133
  title,
@@ -193,6 +212,68 @@ export const ALL_BLOG_ARTICLES_QUERY = defineQuery(`
193
212
  }
194
213
  `)
195
214
 
215
+ export const BLOG_ARTICLES_PAGINATED_QUERY = defineQuery(`
216
+ *[
217
+ _type == "page" &&
218
+ pageFolder->slug.current == "blog" &&
219
+ defined(slug.current) &&
220
+ slug.current != "" &&
221
+ slug.current != "/" &&
222
+ (
223
+ !defined($category) ||
224
+ $category == "" ||
225
+ $category in pageBuilder[_ref in *[_type == "blogContent"]._id][0]->categories[]->slug.current
226
+ )
227
+ ] | order(_updatedAt desc)[$offset...$offset + $limit] {
228
+ _id,
229
+ title,
230
+ "slug": slug,
231
+ "resolvedSlug": ${pageResolvedSlugExpression},
232
+ "blogContent": pageBuilder[_ref in *[_type == "blogContent"]._id][0]->{
233
+ thumbnail{
234
+ ...,
235
+ alt
236
+ },
237
+ categories[]->{
238
+ _id,
239
+ title,
240
+ slug
241
+ },
242
+ date,
243
+ author->{
244
+ _id,
245
+ name,
246
+ avatar
247
+ }
248
+ },
249
+ metadata,
250
+ _updatedAt
251
+ }
252
+ `)
253
+
254
+ export const BLOG_ARTICLES_COUNT_QUERY = defineQuery(`
255
+ count(*[
256
+ _type == "page" &&
257
+ pageFolder->slug.current == "blog" &&
258
+ defined(slug.current) &&
259
+ slug.current != "" &&
260
+ slug.current != "/" &&
261
+ (
262
+ !defined($category) ||
263
+ $category == "" ||
264
+ $category in pageBuilder[_ref in *[_type == "blogContent"]._id][0]->categories[]->slug.current
265
+ )
266
+ ])
267
+ `)
268
+
269
+ export const ALL_BLOG_CATEGORIES_QUERY = defineQuery(`
270
+ *[_type == "blogCategory"] | order(title asc) {
271
+ _id,
272
+ title,
273
+ slug
274
+ }
275
+ `)
276
+
196
277
  // Company Data
197
278
  export const COMPANY_DATA_QUERY = defineQuery(`
198
279
  *[_type == "companyData"][0]{
@@ -235,47 +316,79 @@ const navMegaMenuProjection = `
235
316
  }
236
317
  `
237
318
 
238
- export const NAVBAR_QUERY = defineQuery(`
239
- *[_type == "navbar"][0]{
240
- logo{
241
- ...,
242
- asset->{
243
- _id,
244
- url,
245
- metadata { dimensions }
246
- }
247
- },
248
- navigationItems[]{
249
- _key,
250
- title,
251
- itemType,
252
- ${navLinkProjection},
253
- megaMenu{
254
- ${navMegaMenuProjection}
255
- }
319
+ const navbarProjection = `
320
+ logo{
321
+ ...,
322
+ asset->{
323
+ _id,
324
+ url,
325
+ metadata { dimensions }
326
+ }
327
+ },
328
+ navigationItems[]{
329
+ _key,
330
+ title,
331
+ itemType,
332
+ ${navLinkProjection},
333
+ megaMenu{
334
+ ${navMegaMenuProjection}
335
+ }
336
+ },
337
+ ctaButtons[]{
338
+ _key,
339
+ label,
340
+ "link": link[0]{
341
+ ${linkItemProjection}
256
342
  },
257
- ctaButtons[]{
343
+ variant
344
+ }
345
+ `
346
+
347
+ const footerProjection = `
348
+ links[]{
349
+ _key,
350
+ title,
351
+ items[]{
258
352
  _key,
259
353
  label,
260
- "link": link[0]{
261
- ${linkItemProjection}
262
- },
263
- variant
354
+ href
264
355
  }
265
356
  }
357
+ `
358
+
359
+ const companyDataProjection = `
360
+ socialLinks[]{
361
+ _key,
362
+ name,
363
+ icon,
364
+ url,
365
+ label
366
+ }
367
+ `
368
+
369
+ export const NAVBAR_QUERY = defineQuery(`
370
+ *[_type == "navbar"][0]{
371
+ ${navbarProjection}
372
+ }
266
373
  `)
267
374
 
268
375
  // Footer
269
376
  export const FOOTER_QUERY = defineQuery(`
270
377
  *[_type == "footer"][0]{
271
- links[]{
272
- _key,
273
- title,
274
- items[]{
275
- _key,
276
- label,
277
- href
278
- }
378
+ ${footerProjection}
379
+ }
380
+ `)
381
+
382
+ export const SITE_LAYOUT_QUERY = defineQuery(`
383
+ {
384
+ "navbar": *[_type == "navbar"][0]{
385
+ ${navbarProjection}
386
+ },
387
+ "footer": *[_type == "footer"][0]{
388
+ ${footerProjection}
389
+ },
390
+ "companyData": *[_type == "companyData"][0]{
391
+ ${companyDataProjection}
279
392
  }
280
393
  }
281
394
  `)
@@ -1,14 +1,11 @@
1
1
  import { table } from "@sanity/table"
2
2
  import { defineConfig } from "sanity"
3
- import {
4
- defineDocuments,
5
- defineLocations,
6
- presentationTool,
7
- } from "sanity/presentation"
8
3
  import { structureTool } from "sanity/structure"
9
4
  import { media } from "sanity-plugin-media"
10
5
  import { createConfirmPublishAction } from "./confirm-publish-action"
11
- import { dataset, previewURL, projectId } from "./env"
6
+ import { HOMEPAGE_DOCUMENT_ID } from "./constants"
7
+ import { dataset, projectId } from "./env"
8
+ import { presentation } from "./presentation"
12
9
  import { schema } from "./schemas"
13
10
  import { singletonComponentTypes } from "./singletons"
14
11
  import { structure } from "./structure"
@@ -23,46 +20,7 @@ const singletonDocumentActions = new Set([
23
20
 
24
21
  const isSingletonDocument = (schemaType: string, documentId?: string) =>
25
22
  singletonComponentTypes.has(schemaType) ||
26
- (schemaType === "page" && documentId === "page-homepage")
27
-
28
- const MAX_FOLDER_DEPTH = 6
29
-
30
- const buildNestedSlugExpression = (
31
- parentReferenceField: string,
32
- leafSlugField: string
33
- ) => {
34
- const cases = Array.from({ length: MAX_FOLDER_DEPTH }, (_, index) => {
35
- const depth = MAX_FOLDER_DEPTH - index
36
- const parentSlugRefs = Array.from({ length: depth }, (_, parentIndex) => {
37
- const remainingDepth = depth - parentIndex - 1
38
- return `${parentReferenceField}${"->parentFolder".repeat(remainingDepth)}->slug.current`
39
- })
40
-
41
- const pathDefinedChecks = parentSlugRefs
42
- .map((field) => `defined(${field})`)
43
- .join(" && ")
44
-
45
- const childPath = [...parentSlugRefs, leafSlugField].join(' + "/" + ')
46
- const folderRootPath = parentSlugRefs.join(' + "/" + ')
47
-
48
- return [
49
- `${pathDefinedChecks} && ${leafSlugField} == "/" => ${folderRootPath}`,
50
- `${pathDefinedChecks} && defined(${leafSlugField}) => ${childPath}`,
51
- ]
52
- }).flat()
53
-
54
- return `
55
- select(
56
- ${cases.join(",\n ")},
57
- ${leafSlugField}
58
- )
59
- `
60
- }
61
-
62
- const pageResolvedSlugExpression = buildNestedSlugExpression(
63
- "pageFolder",
64
- "slug.current"
65
- )
23
+ (schemaType === "page" && documentId === HOMEPAGE_DOCUMENT_ID)
66
24
 
67
25
  export default defineConfig({
68
26
  basePath: "/studio",
@@ -151,60 +109,7 @@ export default defineConfig({
151
109
  },
152
110
  plugins: [
153
111
  structureTool({ structure }),
154
-
155
- presentationTool({
156
- name: "preview",
157
- title: "Preview",
158
- resolve: {
159
- // Map routes to documents and GROQ filters
160
- mainDocuments: defineDocuments([
161
- {
162
- route: "/",
163
- filter: '_type == "page" && _id == "page-homepage"',
164
- },
165
- // Page builder pages - all other slugs (supports nested paths)
166
- {
167
- route: "/:slug+",
168
- filter: `_type == "page" && ${pageResolvedSlugExpression} == $slug`,
169
- },
170
- ]),
171
- locations: {
172
- page: defineLocations({
173
- select: {
174
- _id: "_id",
175
- title: "title",
176
- slug: "slug.current",
177
- resolvedSlug: pageResolvedSlugExpression,
178
- },
179
- resolve: (doc) => {
180
- if (doc?._id === "page-homepage") {
181
- return {
182
- locations: [{ title: doc?.title || "Homepage", href: "/" }],
183
- }
184
- }
185
- if (doc?.resolvedSlug) {
186
- return {
187
- locations: [
188
- {
189
- title: doc?.title || "Page",
190
- href: `/${doc.resolvedSlug}`,
191
- },
192
- ],
193
- }
194
- }
195
- return { locations: [] }
196
- },
197
- }),
198
- },
199
- },
200
- previewUrl: {
201
- origin: previewURL,
202
- draftMode: {
203
- enable: "/api/draft-mode/enable",
204
- disable: "/api/draft-mode/disable",
205
- },
206
- },
207
- }),
112
+ presentation,
208
113
  media(),
209
114
  table(),
210
115
  ],
@@ -1,6 +1,8 @@
1
1
  import withBundleAnalyzer from "@next/bundle-analyzer"
2
2
  import type { NextConfig } from "next"
3
3
 
4
+
5
+
4
6
  const nextConfig: NextConfig = {
5
7
  reactStrictMode: true,
6
8
  reactCompiler: true,
@@ -62,6 +64,7 @@ const nextConfig: NextConfig = {
62
64
  browserToTerminal: true,
63
65
  },
64
66
  experimental: {
67
+ prefetchInlining: true,
65
68
  optimizePackageImports: [
66
69
  "@react-three/drei",
67
70
  "@react-three/fiber",
@@ -39,13 +39,11 @@
39
39
  "react-use": "^17.6.0",
40
40
  "sanity": "^5.17.1",
41
41
  "sanity-plugin-media": "^4.1.1",
42
- "schema-dts": "^2.0.0",
43
42
  "tailwind-merge": "^3.5.0",
44
43
  "zod": "^4.3.6"
45
44
  },
46
45
  "devDependencies": {
47
46
  "@biomejs/biome": "2.4.8",
48
- "@clack/prompts": "^1.1.0",
49
47
  "@csstools/postcss-global-data": "^4.0.0",
50
48
  "@next/bundle-analyzer": "16.2.1",
51
49
  "@svgr/webpack": "^8.1.0",
@@ -59,6 +57,7 @@
59
57
  "cross-env": "^10.1.0",
60
58
  "postcss-functions": "^4.0.2",
61
59
  "postcss-preset-env": "^11.2.0",
60
+ "schema-dts": "^2.0.0",
62
61
  "tailwindcss": "^4.2.2",
63
62
  "typescript": "^5.9.3"
64
63
  },
@@ -13,9 +13,9 @@
13
13
  # CORE (Recommended)
14
14
  # ============================================
15
15
 
16
- # Base URL for your site (used for SEO, canonical URLs, etc.)
17
- # Development: http://localhost:3000
18
- # Production: https://your-domain.com
16
+ # Base URL (sitemaps, OG tags, canonical URLs)
17
+ # On Vercel: auto-detected if unset. Set explicitly for custom domains.
18
+ # Local: http://localhost:3000
19
19
  NEXT_PUBLIC_BASE_URL="http://localhost:3000"
20
20
 
21
21
  # Draft mode secret for preview functionality
@@ -5,6 +5,7 @@ import { Link } from "@/components/ui/link";
5
5
  import AppData from "@/package.json";
6
6
  import "@/lib/styles/global.css";
7
7
  import { cn } from "@/lib/styles/cn";
8
+ import { getBaseUrl } from "@/lib/utils/url";
8
9
  import {
9
10
  JsonLd,
10
11
  generateWebSiteJsonLd,
@@ -15,8 +16,7 @@ const APP_NAME = AppData.name;
15
16
  const APP_DEFAULT_TITLE = "Basement Starter";
16
17
  const APP_TITLE_TEMPLATE = "%s - Basement Starter";
17
18
  const APP_DESCRIPTION = AppData.description;
18
- const APP_BASE_URL =
19
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
19
+ const APP_BASE_URL = getBaseUrl();
20
20
 
21
21
  const geist = Geist({
22
22
  subsets: ["latin"],
@@ -1,7 +1,7 @@
1
1
  import type { MetadataRoute } from "next";
2
+ import { getBaseUrl } from "@/lib/utils/url";
2
3
 
3
- const APP_BASE_URL =
4
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
4
+ const APP_BASE_URL = getBaseUrl();
5
5
 
6
6
  export default function robots(): MetadataRoute.Robots {
7
7
  return {
@@ -0,0 +1,51 @@
1
+ import { getBaseUrl } from "@/lib/utils/url";
2
+
3
+ const BASE_URL = getBaseUrl();
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ interface SitemapEntry {
8
+ loc: string;
9
+ lastmod: string;
10
+ changefreq: string;
11
+ priority: number;
12
+ }
13
+
14
+ function toXml(entries: SitemapEntry[]): string {
15
+ const urls = entries
16
+ .map(
17
+ (entry) => `
18
+ <url>
19
+ <loc>${entry.loc}</loc>
20
+ <lastmod>${entry.lastmod}</lastmod>
21
+ <changefreq>${entry.changefreq}</changefreq>
22
+ <priority>${entry.priority}</priority>
23
+ </url>`,
24
+ )
25
+ .join("");
26
+
27
+ return `<?xml version="1.0" encoding="UTF-8"?>
28
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}
29
+ </urlset>`;
30
+ }
31
+
32
+ function getStaticEntries(): SitemapEntry[] {
33
+ return [
34
+ {
35
+ loc: BASE_URL,
36
+ lastmod: new Date().toISOString(),
37
+ changefreq: "daily",
38
+ priority: 1,
39
+ },
40
+ ];
41
+ }
42
+
43
+ export async function GET() {
44
+ const entries: SitemapEntry[] = getStaticEntries();
45
+
46
+ return new Response(toXml(entries), {
47
+ headers: {
48
+ "Content-Type": "application/xml",
49
+ },
50
+ });
51
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Metadata } from "next";
2
+ import { getBaseUrl } from "@/lib/utils/url";
2
3
 
3
4
  /**
4
5
  * Metadata Generation Utilities
@@ -26,8 +27,7 @@ interface GenerateMetadataOptions {
26
27
  authors?: string[];
27
28
  }
28
29
 
29
- const APP_BASE_URL =
30
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
30
+ const APP_BASE_URL = getBaseUrl();
31
31
 
32
32
  /**
33
33
  * Generate complete metadata object for pages
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Resolves the application base URL from environment variables.
3
+ *
4
+ * Priority:
5
+ * 1. NEXT_PUBLIC_BASE_URL - explicit override (custom domains)
6
+ * 2. VERCEL_PROJECT_PRODUCTION_URL - auto-set by Vercel (production domain)
7
+ * 3. VERCEL_URL - auto-set by Vercel (preview/branch deploys)
8
+ * 4. localhost fallback
9
+ */
10
+ export function getBaseUrl(): string {
11
+ if (process.env.NEXT_PUBLIC_BASE_URL) return process.env.NEXT_PUBLIC_BASE_URL;
12
+ if (process.env.VERCEL_PROJECT_PRODUCTION_URL)
13
+ return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
14
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
15
+ return "http://localhost:3000";
16
+ }
@@ -1,61 +0,0 @@
1
- import type { MetadataRoute } from "next";
2
- import { isSanityConfigured } from "@/lib/integrations/check-integration";
3
-
4
- const APP_BASE_URL =
5
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
6
-
7
- export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
8
- const baseRoutes: MetadataRoute.Sitemap = [
9
- {
10
- url: APP_BASE_URL,
11
- lastModified: new Date(),
12
- changeFrequency: "daily",
13
- priority: 1,
14
- },
15
- ];
16
-
17
- // Only fetch Sanity articles if Sanity is configured
18
- if (isSanityConfigured()) {
19
- try {
20
- const sanityModule = await import("@/lib/integrations/sanity/client");
21
- const sanityGroq = await import("next-sanity");
22
-
23
- const client = sanityModule?.client;
24
- const groq = sanityGroq?.groq;
25
-
26
- // Skip if client is null (shouldn't happen since we check isSanityConfigured)
27
- if (!(client && groq)) return baseRoutes;
28
-
29
- type SanityDocument = {
30
- slug: { current: string };
31
- _updatedAt: string;
32
- metadata?: { noIndex?: boolean };
33
- };
34
-
35
- const articles = (await client.fetch(
36
- groq`*[_type == "article" && defined(slug.current)] {
37
- slug,
38
- _updatedAt,
39
- metadata
40
- }`,
41
- )) as SanityDocument[];
42
-
43
- // Add articles to sitemap (exclude noIndex articles)
44
- const articleEntries: MetadataRoute.Sitemap = articles
45
- .filter((article: SanityDocument) => !article.metadata?.noIndex)
46
- .map((article: SanityDocument) => ({
47
- url: `${APP_BASE_URL}/blog/${article.slug.current}`,
48
- lastModified: new Date(article._updatedAt),
49
- changeFrequency: "weekly" as const,
50
- priority: 0.7,
51
- }));
52
-
53
- return [...baseRoutes, ...articleEntries];
54
- } catch (error) {
55
- console.error("Error generating sitemap from Sanity:", error);
56
- return baseRoutes;
57
- }
58
- }
59
-
60
- return baseRoutes;
61
- }