bsmnt 0.3.3 → 0.4.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 (45) 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-webgl/.env.example +3 -3
  37. package/src/templates/next-webgl/app/layout.tsx +2 -2
  38. package/src/templates/next-webgl/app/robots.ts +2 -2
  39. package/src/templates/next-webgl/app/sitemap.xml/route.ts +51 -0
  40. package/src/templates/next-webgl/lib/utils/metadata.ts +2 -2
  41. package/src/templates/next-webgl/lib/utils/url.ts +16 -0
  42. package/src/helpers/integrate/sanity/files/app/sitemap.ts +0 -61
  43. package/src/templates/next-default/app/sitemap.ts +0 -16
  44. package/src/templates/next-experiments/app/sitemap.ts +0 -16
  45. package/src/templates/next-webgl/app/sitemap.ts +0 -16
@@ -6,7 +6,7 @@ const CMS_CONFIG = {
6
6
  sanity: {
7
7
  mergeFiles: [
8
8
  "app/layout.tsx",
9
- "app/sitemap.ts",
9
+ "app/sitemap.xml/route.ts",
10
10
  "lib/integrations/check-integration.ts",
11
11
  ],
12
12
  additivePaths: [
@@ -1 +1 @@
1
- {"version":3,"file":"merge-config.js","sourceRoot":"","sources":["../../../src/helpers/integrate/merge-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,UAAU,GAA+B;IAC9C,MAAM,EAAE;QACP,UAAU,EAAE;YACX,gBAAgB;YAChB,gBAAgB;YAChB,uCAAuC;SACvC;QAED,aAAa,EAAE;YACd,yBAAyB;YACzB,4BAA4B;YAC5B,oBAAoB;YACpB,oBAAoB;YACpB,YAAY;YACZ,cAAc;YACd,UAAU;YACV,gBAAgB;YAChB,uBAAuB;YACvB,uBAAuB;YACvB,wCAAwC;YACxC,4BAA4B;YAC5B,UAAU;SACV;KACD;CACD,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACvC,OAAQ,UAAwC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AAC/D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACzC,OAAO,GAAG,IAAI,UAAU,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"merge-config.js","sourceRoot":"","sources":["../../../src/helpers/integrate/merge-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,UAAU,GAA+B;IAC9C,MAAM,EAAE;QACP,UAAU,EAAE;YACX,gBAAgB;YAChB,0BAA0B;YAC1B,uCAAuC;SACvC;QAED,aAAa,EAAE;YACd,yBAAyB;YACzB,4BAA4B;YAC5B,oBAAoB;YACpB,oBAAoB;YACpB,YAAY;YACZ,cAAc;YACd,UAAU;YACV,gBAAgB;YAChB,uBAAuB;YACvB,uBAAuB;YACvB,wCAAwC;YACxC,4BAA4B;YAC5B,UAAU;SACV;KACD;CACD,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACvC,OAAQ,UAAwC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AAC/D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACzC,OAAO,GAAG,IAAI,UAAU,CAAC;AAC1B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"merge-orchestrator.d.ts","sourceRoot":"","sources":["../../../src/helpers/integrate/merge-orchestrator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA2FlE;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACtC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,GAAG,GACV,OAAO,CAAC,YAAY,CAAC,CA6HvB"}
1
+ {"version":3,"file":"merge-orchestrator.d.ts","sourceRoot":"","sources":["../../../src/helpers/integrate/merge-orchestrator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA4FlE;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACtC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,GAAG,GACV,OAAO,CAAC,YAAY,CAAC,CA6HvB"}
@@ -11,7 +11,7 @@ import { mergeSitemap } from "./sanity/mergers/sitemap-merger.js";
11
11
  const CMS_MERGERS = {
12
12
  sanity: {
13
13
  "app/layout.tsx": mergeLayout,
14
- "app/sitemap.ts": (templatePath) => mergeSitemap(templatePath),
14
+ "app/sitemap.xml/route.ts": (templatePath) => mergeSitemap(templatePath),
15
15
  "lib/integrations/check-integration.ts": (templatePath) => mergeCheckIntegration(templatePath),
16
16
  },
17
17
  };
@@ -142,16 +142,16 @@ export async function injectIntegration(targetDir, cms, spinner) {
142
142
  }
143
143
  continue;
144
144
  }
145
- // Check if integration file exists
146
- if (!(await fs.pathExists(integrationFile))) {
145
+ // Get the CMS-specific merger function (use base path for lookup)
146
+ const merger = getMerger(cms, mergeFile);
147
+ // Check if integration file exists (skip check if a merger handles it inline)
148
+ if (!merger && !(await fs.pathExists(integrationFile))) {
147
149
  results.skipped.push({
148
150
  path: transformedPath,
149
151
  reason: "Not in integration",
150
152
  });
151
153
  continue;
152
154
  }
153
- // Get the CMS-specific merger function (use base path for lookup)
154
- const merger = getMerger(cms, mergeFile);
155
155
  if (merger) {
156
156
  const result = await merger(templateFile, {
157
157
  targetDir,
@@ -1 +1 @@
1
- {"version":3,"file":"merge-orchestrator.js","sourceRoot":"","sources":["../../../src/helpers/integrate/merge-orchestrator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"merge-orchestrator.js","sourceRoot":"","sources":["../../../src/helpers/integrate/merge-orchestrator.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/helpers/integrate/sanity/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,eAAO,MAAM,YAAY,EAAE,uBAqD1B,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/helpers/integrate/sanity/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,eAAO,MAAM,YAAY,EAAE,uBAsD1B,CAAC"}
@@ -37,6 +37,7 @@ export const sanityConfig = {
37
37
  "app/api/blog",
38
38
  "app/blog",
39
39
  "app/sitemap.md",
40
+ "lib/utils/url.ts",
40
41
  "lib/utils/json-ld.tsx",
41
42
  "lib/utils/metadata.ts",
42
43
  "lib/utils/portable-text-to-markdown.ts",
@@ -45,7 +46,7 @@ export const sanityConfig = {
45
46
  ],
46
47
  mergeFiles: [
47
48
  "app/layout.tsx",
48
- "app/sitemap.ts",
49
+ "app/sitemap.xml/route.ts",
49
50
  "lib/integrations/check-integration.ts",
50
51
  ],
51
52
  };
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/helpers/integrate/sanity/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,CAAC,MAAM,YAAY,GAA4B;IACpD,IAAI,EAAE,QAAQ;IAEd,YAAY,EAAE;QACb,gBAAgB,EAAE,QAAQ;QAC1B,QAAQ,EAAE,QAAQ;QAClB,wBAAwB,EAAE,QAAQ;QAClC,qBAAqB,EAAE,SAAS;QAChC,qBAAqB,EAAE,QAAQ;QAC/B,mBAAmB,EAAE,QAAQ;QAC7B,wBAAwB,EAAE,QAAQ;QAClC,gBAAgB,EAAE,QAAQ;QAC1B,aAAa,EAAE,UAAU;QACzB,MAAM,EAAE,QAAQ;QAChB,qBAAqB,EAAE,QAAQ;KAC/B;IAED,eAAe,EAAE;QAChB,YAAY,EAAE,QAAQ;KACtB;IAED,OAAO,EAAE;QACR,gBAAgB,EACf,wFAAwF;QACzF,gBAAgB,EACf,0LAA0L;QAC3L,MAAM,EAAE,wBAAwB;QAChC,GAAG,EAAE,UAAU;QACf,QAAQ,EAAE,wBAAwB;QAClC,KAAK,EAAE,YAAY;KACnB;IAED,aAAa,EAAE;QACd,yBAAyB;QACzB,4BAA4B;QAC5B,oBAAoB;QACpB,oBAAoB;QACpB,YAAY;QACZ,cAAc;QACd,UAAU;QACV,gBAAgB;QAChB,uBAAuB;QACvB,uBAAuB;QACvB,wCAAwC;QACxC,4BAA4B;QAC5B,UAAU;KACV;IAED,UAAU,EAAE;QACX,gBAAgB;QAChB,gBAAgB;QAChB,uCAAuC;KACvC;CACD,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/helpers/integrate/sanity/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,CAAC,MAAM,YAAY,GAA4B;IACpD,IAAI,EAAE,QAAQ;IAEd,YAAY,EAAE;QACb,gBAAgB,EAAE,QAAQ;QAC1B,QAAQ,EAAE,QAAQ;QAClB,wBAAwB,EAAE,QAAQ;QAClC,qBAAqB,EAAE,SAAS;QAChC,qBAAqB,EAAE,QAAQ;QAC/B,mBAAmB,EAAE,QAAQ;QAC7B,wBAAwB,EAAE,QAAQ;QAClC,gBAAgB,EAAE,QAAQ;QAC1B,aAAa,EAAE,UAAU;QACzB,MAAM,EAAE,QAAQ;QAChB,qBAAqB,EAAE,QAAQ;KAC/B;IAED,eAAe,EAAE;QAChB,YAAY,EAAE,QAAQ;KACtB;IAED,OAAO,EAAE;QACR,gBAAgB,EACf,wFAAwF;QACzF,gBAAgB,EACf,0LAA0L;QAC3L,MAAM,EAAE,wBAAwB;QAChC,GAAG,EAAE,UAAU;QACf,QAAQ,EAAE,wBAAwB;QAClC,KAAK,EAAE,YAAY;KACnB;IAED,aAAa,EAAE;QACd,yBAAyB;QACzB,4BAA4B;QAC5B,oBAAoB;QACpB,oBAAoB;QACpB,YAAY;QACZ,cAAc;QACd,UAAU;QACV,gBAAgB;QAChB,kBAAkB;QAClB,uBAAuB;QACvB,uBAAuB;QACvB,wCAAwC;QACxC,4BAA4B;QAC5B,UAAU;KACV;IAED,UAAU,EAAE;QACX,gBAAgB;QAChB,0BAA0B;QAC1B,uCAAuC;KACvC;CACD,CAAC"}
@@ -4,9 +4,7 @@ interface MergeResult {
4
4
  success?: boolean;
5
5
  }
6
6
  /**
7
- * Merge Sanity integration into template sitemap.
8
- * Preserves all existing agnostic code (routes, baseRoutes structure)
9
- * and injects Sanity-specific fetching logic.
7
+ * Replace the static sitemap.xml route with a Sanity-aware version.
10
8
  */
11
9
  export declare function mergeSitemap(templatePath: string): Promise<MergeResult>;
12
10
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap-merger.d.ts","sourceRoot":"","sources":["../../../../../src/helpers/integrate/sanity/mergers/sitemap-merger.ts"],"names":[],"mappings":"AAyDA,UAAU,WAAW;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAqC7E"}
1
+ {"version":3,"file":"sitemap-merger.d.ts","sourceRoot":"","sources":["../../../../../src/helpers/integrate/sanity/mergers/sitemap-merger.ts"],"names":[],"mappings":"AAyHA,UAAU,WAAW;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,YAAY,CACjC,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC,CAatB"}
@@ -1,92 +1,133 @@
1
1
  import fs from "fs-extra";
2
2
  /**
3
- * Sanity-specific import to add
3
+ * Sanity-aware sitemap.xml route.
4
+ * Replaces the static-only sitemap with one that dynamically imports Sanity
5
+ * client/queries to fetch CMS pages and articles when Sanity is configured,
6
+ * falling back to the homepage entry otherwise.
4
7
  */
5
- const SANITY_IMPORT = `import { isSanityConfigured } from "@/lib/integrations/check-integration"`;
6
- /**
7
- * Sanity fetch logic to inject before return statement.
8
- * Uses early return pattern and spread operator for cleaner code.
9
- */
10
- const SANITY_FETCH_LOGIC = `
11
- // Only fetch Sanity articles if Sanity is configured
12
- if (isSanityConfigured()) {
13
- try {
14
- const sanityModule = await import("@/lib/integrations/sanity/client")
15
- const sanityGroq = await import("next-sanity")
16
-
17
- const client = sanityModule?.client
18
- const groq = sanityGroq?.groq
19
-
20
- // Skip if client is null (shouldn't happen since we check isSanityConfigured)
21
- if (!(client && groq)) return baseRoutes
22
-
23
- type SanityDocument = {
24
- slug: { current: string }
25
- _updatedAt: string
26
- metadata?: { noIndex?: boolean }
8
+ const SANITY_SITEMAP_ROUTE = `import { NextResponse } from "next/server";
9
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
10
+ import { getBaseUrl } from "@/lib/utils/url";
11
+
12
+ export const dynamic = "force-dynamic";
13
+
14
+ interface SitemapEntry {
15
+ url: string;
16
+ lastModified?: string | null;
17
+ }
18
+
19
+ const createPageEntry = (
20
+ baseUrl: string,
21
+ slug: string | null,
22
+ updatedAt?: string | null
23
+ ): SitemapEntry => {
24
+ const path = slug ? \`/\${slug}\` : "";
25
+ return {
26
+ url: \`\${baseUrl}\${path}\`,
27
+ lastModified: updatedAt ?? null,
28
+ };
29
+ };
30
+
31
+ const escapeXml = (value: string) =>
32
+ value
33
+ .replaceAll("&", "&amp;")
34
+ .replaceAll("<", "&lt;")
35
+ .replaceAll(">", "&gt;")
36
+ .replaceAll('"', "&quot;")
37
+ .replaceAll("'", "&apos;");
38
+
39
+ const renderUrl = (entry: SitemapEntry) => {
40
+ const parts = [" <url>", \` <loc>\${escapeXml(entry.url)}</loc>\`];
41
+
42
+ if (entry.lastModified) {
43
+ parts.push(
44
+ \` <lastmod>\${new Date(entry.lastModified).toISOString()}</lastmod>\`
45
+ );
46
+ }
47
+
48
+ parts.push(" </url>");
49
+
50
+ return parts.join("\\n");
51
+ };
52
+
53
+ const renderSitemap = (entries: SitemapEntry[]) =>
54
+ [
55
+ '<?xml version="1.0" encoding="UTF-8"?>',
56
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
57
+ ...entries.map(renderUrl),
58
+ "</urlset>",
59
+ ].join("\\n");
60
+
61
+ const getSitemapResponse = (entries: SitemapEntry[]) =>
62
+ new NextResponse(renderSitemap(entries), {
63
+ headers: {
64
+ "Content-Type": "application/xml",
65
+ },
66
+ });
67
+
68
+ export const GET = async () => {
69
+ const baseUrl = getBaseUrl();
70
+ const homepageEntry = createPageEntry(baseUrl, null);
71
+
72
+ if (!isSanityConfigured()) return getSitemapResponse([homepageEntry]);
73
+
74
+ try {
75
+ const { client } = await import("@/lib/integrations/sanity/client");
76
+ const {
77
+ SITEMAP_ARTICLES_QUERY,
78
+ SITEMAP_PAGES_QUERY,
79
+ } = await import("@/lib/integrations/sanity/queries");
80
+
81
+ if (!client) return getSitemapResponse([homepageEntry]);
82
+
83
+ type SanityDoc = {
84
+ title?: string;
85
+ slug: { current: string };
86
+ _updatedAt: string;
87
+ noIndex?: boolean;
88
+ };
89
+
90
+ const [articles, pages] = await Promise.all([
91
+ client.fetch<SanityDoc[]>(SITEMAP_ARTICLES_QUERY),
92
+ client.fetch<SanityDoc[]>(SITEMAP_PAGES_QUERY),
93
+ ]);
94
+
95
+ const sitemapEntries: SitemapEntry[] = [homepageEntry];
96
+
97
+ for (const article of articles) {
98
+ if (!article.noIndex) {
99
+ sitemapEntries.push(
100
+ createPageEntry(baseUrl, \`blog/\${article.slug.current}\`, article._updatedAt)
101
+ );
27
102
  }
103
+ }
28
104
 
29
- const articles = (await client.fetch(
30
- groq\`*[_type == "article" && defined(slug.current)] {
31
- slug,
32
- _updatedAt,
33
- metadata
34
- }\`
35
- )) as SanityDocument[]
36
-
37
- // Add articles to sitemap (exclude noIndex articles)
38
- const articleEntries: MetadataRoute.Sitemap = articles
39
- .filter((article: SanityDocument) => !article.metadata?.noIndex)
40
- .map((article: SanityDocument) => ({
41
- url: \`\${APP_BASE_URL}/blog/\${article.slug.current}\`,
42
- lastModified: new Date(article._updatedAt),
43
- changeFrequency: "weekly" as const,
44
- priority: 0.7,
45
- }))
46
-
47
- return [...baseRoutes, ...articleEntries]
48
- } catch (error) {
49
- console.error("Error generating sitemap from Sanity:", error)
50
- return baseRoutes
105
+ for (const page of pages) {
106
+ if (!page.noIndex) {
107
+ sitemapEntries.push(
108
+ createPageEntry(baseUrl, page.slug.current, page._updatedAt)
109
+ );
110
+ }
51
111
  }
52
- }
53
112
 
113
+ return getSitemapResponse(sitemapEntries);
114
+ } catch (error) {
115
+ console.error("Error generating sitemap.xml:", error);
116
+ return getSitemapResponse([homepageEntry]);
117
+ }
118
+ };
54
119
  `;
55
120
  /**
56
- * Merge Sanity integration into template sitemap.
57
- * Preserves all existing agnostic code (routes, baseRoutes structure)
58
- * and injects Sanity-specific fetching logic.
121
+ * Replace the static sitemap.xml route with a Sanity-aware version.
59
122
  */
60
123
  export async function mergeSitemap(templatePath) {
61
- let content = await fs.readFile(templatePath, "utf-8");
124
+ const content = await fs.readFile(templatePath, "utf-8");
62
125
  // Skip if already has Sanity integration
63
- if (content.includes("isSanityConfigured")) {
64
- return { skipped: true, reason: "Already has Sanity integration" };
65
- }
66
- // 1. Add Sanity import after existing imports
67
- const importMatches = [...content.matchAll(/^import .+$/gm)];
68
- if (importMatches.length > 0) {
69
- const lastImport = importMatches.at(-1);
70
- if (lastImport && lastImport.index !== undefined) {
71
- const insertPos = lastImport.index + lastImport[0].length;
72
- content =
73
- content.slice(0, insertPos) +
74
- "\n" +
75
- SANITY_IMPORT +
76
- content.slice(insertPos);
77
- }
78
- }
79
- // 2. Add Sanity fetch logic before the final return statement
80
- // Find the last "return baseRoutes" or "return [...baseRoutes" pattern
81
- const returnMatch = content.match(/(\n)([ \t]*)(return\s+(?:baseRoutes|\[\.\.\.baseRoutes))/m);
82
- if (returnMatch) {
83
- const returnIndex = content.indexOf(returnMatch[0]);
84
- content =
85
- content.slice(0, returnIndex) +
86
- SANITY_FETCH_LOGIC +
87
- content.slice(returnIndex);
126
+ if (content.includes("isSanityConfigured") ||
127
+ content.includes("SITEMAP_ARTICLES_QUERY")) {
128
+ return { skipped: true, reason: "Already has Sanity sitemap integration" };
88
129
  }
89
- await fs.writeFile(templatePath, content);
130
+ await fs.writeFile(templatePath, SANITY_SITEMAP_ROUTE);
90
131
  return { success: true };
91
132
  }
92
133
  //# sourceMappingURL=sitemap-merger.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap-merger.js","sourceRoot":"","sources":["../../../../../src/helpers/integrate/sanity/mergers/sitemap-merger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAE1B;;GAEG;AACH,MAAM,aAAa,GAAG,2EAA2E,CAAC;AAElG;;;GAGG;AACH,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4C1B,CAAC;AAQF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,YAAoB;IACtD,IAAI,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAEvD,yCAAyC;IACzC,IAAI,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC;QAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,gCAAgC,EAAE,CAAC;IACpE,CAAC;IAED,8CAA8C;IAC9C,MAAM,aAAa,GAAG,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC;IAC7D,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,MAAM,UAAU,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,IAAI,UAAU,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAClD,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAC1D,OAAO;gBACN,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC;oBAC3B,IAAI;oBACJ,aAAa;oBACb,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC3B,CAAC;IACF,CAAC;IAED,8DAA8D;IAC9D,uEAAuE;IACvE,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAChC,2DAA2D,CAC3D,CAAC;IACF,IAAI,WAAW,EAAE,CAAC;QACjB,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,OAAO;YACN,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC;gBAC7B,kBAAkB;gBAClB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"sitemap-merger.js","sourceRoot":"","sources":["../../../../../src/helpers/integrate/sanity/mergers/sitemap-merger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+G5B,CAAC;AAQF;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,YAAoB;IAEpB,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAEzD,yCAAyC;IACzC,IACC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;QACtC,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAC,EACzC,CAAC;QACF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,wCAAwC,EAAE,CAAC;IAC5E,CAAC;IAED,MAAM,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,oBAAoB,CAAC,CAAC;IACvD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bsmnt",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "packageManager": "bun@1.2.20",
5
5
  "description": "CLI to scaffold basement projects and add integrations",
6
6
  "type": "module",
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { portableTextToMarkdown } from "@/lib/utils/portable-text-to-markdown";
3
3
  import { isSanityConfigured } from "@/lib/integrations/check-integration";
4
+ import { getBaseUrl } from "@/lib/utils/url";
4
5
  import { ARTICLE_QUERY } from "@/lib/integrations/sanity/queries";
5
6
  import { SanityFetch } from "@/lib/integrations/sanity/live";
6
7
  import type { Article } from "@/lib/integrations/sanity/sanity.types";
@@ -16,7 +17,7 @@ export async function GET(
16
17
  }
17
18
 
18
19
  const slug = rawSlug.slice(0, -3);
19
- const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
20
+ const baseUrl = getBaseUrl();
20
21
 
21
22
  if (!isSanityConfigured()) {
22
23
  return new NextResponse("# 404 Not Found\n\nCMS not configured.", {
@@ -1,4 +1,4 @@
1
- import { revalidateTag } from "next/cache";
1
+ import { revalidatePath, revalidateTag } from "next/cache";
2
2
  import { type NextRequest, NextResponse } from "next/server";
3
3
  import { parseBody } from "next-sanity/webhook";
4
4
 
@@ -25,6 +25,9 @@ export async function POST(request: NextRequest) {
25
25
  revalidateTag(`${body._type}:${body.slug.current}`, "max");
26
26
  }
27
27
 
28
+ revalidatePath("/sitemap.xml");
29
+ revalidatePath("/sitemap.md");
30
+
28
31
  return NextResponse.json({
29
32
  status: 200,
30
33
  revalidated: true,
@@ -9,7 +9,9 @@ import { RichText } from "@/lib/integrations/sanity/components/rich-text";
9
9
 
10
10
  export async function generateStaticParams() {
11
11
  if (!client) return [];
12
- const articles = await client.fetch(ALL_ARTICLES_QUERY);
12
+ const articles = await client.fetch<
13
+ Array<{ slug?: { current?: string } }>
14
+ >(ALL_ARTICLES_QUERY);
13
15
 
14
16
  return (articles ?? [])
15
17
  .filter((article) => article.slug?.current)
@@ -9,13 +9,13 @@ import { fontsVariable } from "@/lib/styles/fonts";
9
9
  import AppData from "@/package.json";
10
10
  import "@/lib/styles/css/index.css";
11
11
  import { cn } from "@/lib/styles/cn";
12
+ import { getBaseUrl } from "@/lib/utils/url";
12
13
 
13
14
  const APP_NAME = AppData.name;
14
15
  const APP_DEFAULT_TITLE = "Basement Starter";
15
16
  const APP_TITLE_TEMPLATE = "%s - Basement Starter";
16
17
  const APP_DESCRIPTION = AppData.description;
17
- const APP_BASE_URL =
18
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
18
+ const APP_BASE_URL = getBaseUrl();
19
19
 
20
20
  const geist = Geist({
21
21
  subsets: ["latin"],
@@ -1,8 +1,11 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { isSanityConfigured } from "@/lib/integrations/check-integration";
3
+ import { getBaseUrl } from "@/lib/utils/url";
4
+
5
+ export const dynamic = "force-dynamic";
3
6
 
4
7
  export async function GET() {
5
- const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
8
+ const baseUrl = getBaseUrl();
6
9
 
7
10
  if (!isSanityConfigured()) {
8
11
  return new NextResponse(
@@ -14,13 +17,13 @@ export async function GET() {
14
17
  }
15
18
 
16
19
  try {
17
- const sanityModule = await import("@/lib/integrations/sanity/client");
18
- const sanityGroq = await import("next-sanity");
19
-
20
- const client = sanityModule?.client;
21
- const groq = sanityGroq?.groq;
20
+ const { client } = await import("@/lib/integrations/sanity/client");
21
+ const {
22
+ SITEMAP_ARTICLES_QUERY,
23
+ SITEMAP_PAGES_QUERY,
24
+ } = await import("@/lib/integrations/sanity/queries");
22
25
 
23
- if (!(client && groq)) {
26
+ if (!client) {
24
27
  return new NextResponse("# Sitemap\n\nUnable to connect to CMS.", {
25
28
  status: 503,
26
29
  headers: { "Content-Type": "text/markdown; charset=utf-8" },
@@ -28,18 +31,15 @@ export async function GET() {
28
31
  }
29
32
 
30
33
  type SanityDoc = {
31
- title: string;
34
+ title?: string;
32
35
  slug: { current: string };
33
36
  _updatedAt: string;
34
37
  };
35
38
 
36
- const articles = await client.fetch<SanityDoc[]>(
37
- groq`*[_type == "article" && defined(slug.current)] | order(publishedAt desc) {
38
- title,
39
- slug,
40
- _updatedAt
41
- }`,
42
- );
39
+ const [articles, pages] = await Promise.all([
40
+ client.fetch<SanityDoc[]>(SITEMAP_ARTICLES_QUERY),
41
+ client.fetch<SanityDoc[]>(SITEMAP_PAGES_QUERY),
42
+ ]);
43
43
 
44
44
  const parts = [
45
45
  `# ${new URL(baseUrl).hostname} - Content Sitemap`,
@@ -52,14 +52,26 @@ export async function GET() {
52
52
  if (articles.length > 0) {
53
53
  parts.push("## Articles", "");
54
54
  for (const article of articles) {
55
+ const title = article.title || article.slug.current;
56
+ parts.push(
57
+ `- [${title}](${baseUrl}/blog/${article.slug.current}.md)`,
58
+ );
59
+ }
60
+ parts.push("");
61
+ }
62
+
63
+ if (pages.length > 0) {
64
+ parts.push("## Pages", "");
65
+ for (const page of pages) {
66
+ const title = page.title || page.slug.current;
55
67
  parts.push(
56
- `- [${article.title}](${baseUrl}/blog/${article.slug.current}.md)`,
68
+ `- [${title}](${baseUrl}/${page.slug.current}.md)`,
57
69
  );
58
70
  }
59
71
  parts.push("");
60
72
  }
61
73
 
62
- if (articles.length === 0) {
74
+ if (articles.length === 0 && pages.length === 0) {
63
75
  parts.push("No content published yet.", "");
64
76
  }
65
77
 
@@ -69,7 +81,6 @@ export async function GET() {
69
81
  headers: {
70
82
  "Content-Type": "text/markdown; charset=utf-8",
71
83
  "X-Content-Type-Options": "nosniff",
72
- "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=600",
73
84
  },
74
85
  });
75
86
  } catch (error) {
@@ -0,0 +1,74 @@
1
+ # Sanity Webhook Setup
2
+
3
+ Configure a Sanity webhook to trigger Next.js on-demand revalidation whenever content changes. This keeps pages, sitemap.xml, and sitemap.md in sync with the CMS without redeploying.
4
+
5
+ ## 1. Generate a webhook secret
6
+
7
+ Generate a strong random string locally:
8
+
9
+ ```bash
10
+ openssl rand -base64 32
11
+ ```
12
+
13
+ Keep this value — you'll paste it into both the webhook config and your environment variables.
14
+
15
+ ## 2. Add the secret to your environment
16
+
17
+ Add to `.env.local` (development) and your production env (Vercel, etc.):
18
+
19
+ ```bash
20
+ SANITY_REVALIDATE_SECRET=<paste-generated-secret-here>
21
+ ```
22
+
23
+ ## 3. Create the webhook in Sanity
24
+
25
+ 1. Go to [sanity.io/manage](https://sanity.io/manage)
26
+ 2. Select your project → **API** → **Webhooks** → **Create webhook**
27
+ 3. Fill in the fields below:
28
+
29
+ | Field | Value |
30
+ | --- | --- |
31
+ | **Name** | `Next.js Revalidate` |
32
+ | **Description** | Revalidates pages and sitemap on content changes |
33
+ | **URL** | `https://your-domain.com/api/revalidate` |
34
+ | **Dataset** | `production` (or the dataset you want to revalidate) |
35
+ | **Trigger on** | ✅ Create &nbsp; ✅ Update &nbsp; ✅ Delete |
36
+ | **Projection** | `{ _type, slug }` |
37
+ | **HTTP method** | `POST` |
38
+ | **HTTP Headers** | *(none required)* |
39
+ | **API version** | `2023-05-03` or later |
40
+ | **Include drafts** | ❌ Off (unless you want drafts to trigger revalidation) |
41
+ | **Secret** | Paste the same secret from step 1 |
42
+
43
+ > Update the **Filter** if you add more indexed document types (e.g. `_type in ["page", "article", "project"]`).
44
+
45
+ ## 4. Verify the route
46
+
47
+ The webhook handler lives at `app/api/revalidate/route.ts`. It:
48
+
49
+ - Verifies the request signature against `SANITY_REVALIDATE_SECRET`
50
+ - Calls `revalidateTag(_type)` for every page using that document type
51
+ - Calls `revalidateTag(_type:slug)` for the specific document
52
+ - Calls `revalidatePath("/sitemap.xml")` and `revalidatePath("/sitemap.md")`
53
+
54
+ ## 5. Test it
55
+
56
+ 1. In Sanity Studio, publish a change to any `page` or `article`
57
+ 2. Open the webhook in sanity.io/manage → **Recent deliveries**
58
+ 3. You should see a `200 OK` response
59
+
60
+ If you see `401 Invalid signature` — the secret in your env doesn't match the webhook secret.
61
+ If you see `400 Bad Request` — the projection is missing `_type`.
62
+
63
+ ## Troubleshooting
64
+
65
+ **Changes aren't showing up on the live site**
66
+ - Confirm the webhook delivery returned `200`
67
+ - Confirm `SANITY_REVALIDATE_SECRET` is set in **production** (not just local)
68
+ - Confirm your fetch functions use tags matching `_type` or `_type:slug`
69
+
70
+ **Slug-specific revalidation doesn't fire**
71
+ - Projection must include `slug` — `{ _type }` alone isn't enough
72
+
73
+ **Signature always invalid**
74
+ - Copy the secret fresh from Sanity and re-paste into env — trailing whitespace breaks HMAC verification
@@ -1,3 +1,5 @@
1
+ import { getBaseUrl } from "@/lib/utils/url";
2
+
1
3
  /**
2
4
  * Sanity Environment Configuration
3
5
  *
@@ -23,7 +25,7 @@ export const projectId =
23
25
  export const studioUrl =
24
26
  process.env.NODE_ENV === "development"
25
27
  ? "http://localhost:3000/studio"
26
- : `${process.env.NEXT_PUBLIC_BASE_URL || ""}/studio`;
28
+ : `${getBaseUrl()}/studio`;
27
29
 
28
30
  /**
29
31
  * Single Sanity API read token used for both browser and server live preview.
@@ -39,4 +41,4 @@ export const sanityToken =
39
41
  export const previewURL =
40
42
  process.env.NODE_ENV === "development"
41
43
  ? "http://localhost:3000"
42
- : process.env.NEXT_PUBLIC_BASE_URL || "";
44
+ : getBaseUrl();
@@ -64,3 +64,45 @@ export const ARTICLE_BY_ID_QUERY = defineQuery(`
64
64
  _updatedAt
65
65
  }
66
66
  `);
67
+
68
+ export const PAGE_QUERY = defineQuery(`
69
+ *[_type == "example" && slug.current == $slug][0] {
70
+ _id,
71
+ title,
72
+ slug,
73
+ hero,
74
+ ${richTextWithLinks},
75
+ features,
76
+ tags,
77
+ metadata,
78
+ publishedAt,
79
+ _updatedAt
80
+ }
81
+ `);
82
+
83
+ export const SITEMAP_ARTICLES_QUERY = defineQuery(`
84
+ *[_type == "article" && defined(slug.current)] {
85
+ title,
86
+ slug,
87
+ _updatedAt,
88
+ "noIndex": metadata.noIndex
89
+ }
90
+ `);
91
+
92
+ export const SITEMAP_PAGES_QUERY = defineQuery(`
93
+ *[_type == "example" && defined(slug.current)] {
94
+ title,
95
+ slug,
96
+ _updatedAt,
97
+ "noIndex": metadata.noIndex
98
+ }
99
+ `);
100
+
101
+ export const ALL_PAGES_QUERY = defineQuery(`
102
+ *[_type == "example" && defined(slug.current)] | order(publishedAt desc) {
103
+ slug,
104
+ _updatedAt,
105
+ "noIndex": metadata.noIndex
106
+ }
107
+ `);
108
+
@@ -0,0 +1,90 @@
1
+ import { client } from "./client";
2
+ import { SITEMAP_ARTICLES_QUERY, SITEMAP_PAGES_QUERY } from "./queries";
3
+
4
+ export interface SitemapEntry {
5
+ loc: string;
6
+ lastmod: string;
7
+ changefreq: string;
8
+ priority: number;
9
+ }
10
+
11
+ interface SitemapDocument {
12
+ slug?: {
13
+ current?: string | null;
14
+ } | null;
15
+ _updatedAt: string;
16
+ noIndex?: boolean | null;
17
+ }
18
+
19
+ /**
20
+ * Fetch all published articles as sitemap entries.
21
+ * Excludes articles with metadata.noIndex set to true.
22
+ */
23
+ async function getArticleEntries(baseUrl: string): Promise<SitemapEntry[]> {
24
+ if (!client) return [];
25
+
26
+ try {
27
+ const articles =
28
+ (await client.fetch(SITEMAP_ARTICLES_QUERY)) as SitemapDocument[] | null;
29
+
30
+ if (!articles) return [];
31
+
32
+ return articles
33
+ .filter((article) => article.slug?.current && !article.noIndex)
34
+ .map((article) => ({
35
+ loc: `${baseUrl}/blog/${article.slug!.current!}`,
36
+ lastmod: new Date(article._updatedAt).toISOString(),
37
+ changefreq: "weekly",
38
+ priority: 0.7,
39
+ }));
40
+ } catch (error) {
41
+ console.error("Error fetching article sitemap entries:", error);
42
+ return [];
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Fetch all published CMS pages as sitemap entries.
48
+ * Supports nested slugs and excludes metadata.noIndex pages.
49
+ */
50
+ async function getPageEntries(baseUrl: string): Promise<SitemapEntry[]> {
51
+ if (!client) return [];
52
+
53
+ try {
54
+ const pages =
55
+ (await client.fetch(SITEMAP_PAGES_QUERY)) as SitemapDocument[] | null;
56
+
57
+ if (!pages) return [];
58
+
59
+ return pages
60
+ .filter((page) => page.slug?.current && !page.noIndex)
61
+ .map((page) => ({
62
+ loc: `${baseUrl}/${page.slug!.current!}`,
63
+ lastmod: new Date(page._updatedAt).toISOString(),
64
+ changefreq: "monthly",
65
+ priority: 0.8,
66
+ }));
67
+ } catch (error) {
68
+ console.error("Error fetching page sitemap entries:", error);
69
+ return [];
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Fetch all CMS-managed sitemap entries across all collections.
75
+ *
76
+ * To add a new collection, create a getter function (e.g. getPageEntries)
77
+ * and include it in the Promise.all below.
78
+ */
79
+ export async function getCMSSitemapEntries(
80
+ baseUrl: string,
81
+ ): Promise<SitemapEntry[]> {
82
+ const entries = await Promise.all([
83
+ getArticleEntries(baseUrl),
84
+ getPageEntries(baseUrl),
85
+ // Add more collections here as needed:
86
+ // getProjectEntries(baseUrl),
87
+ ]);
88
+
89
+ return entries.flat();
90
+ }
@@ -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,23 @@
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)
12
+ return process.env.NEXT_PUBLIC_BASE_URL;
13
+ if (process.env.VERCEL_PROJECT_PRODUCTION_URL)
14
+ return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
15
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
16
+ return "http://localhost:3000";
17
+ }
18
+
19
+ export function getPageUrl(baseUrl: string, slug: string | null): string {
20
+ if (!slug) return baseUrl;
21
+ const path = slug.startsWith("/") ? slug : `/${slug}`;
22
+ return `${baseUrl.replace(/\/$/, "")}${path}`;
23
+ }
@@ -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
+ }
@@ -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,8 +1,5 @@
1
1
  import type { MetadataRoute } from "next";
2
2
 
3
- const APP_BASE_URL =
4
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
5
-
6
3
  export default function robots(): MetadataRoute.Robots {
7
4
  return {
8
5
  rules: {
@@ -10,6 +7,5 @@ export default function robots(): MetadataRoute.Robots {
10
7
  allow: "/",
11
8
  disallow: [],
12
9
  },
13
- sitemap: `${APP_BASE_URL}/sitemap.xml`,
14
10
  };
15
11
  }
@@ -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
+ }
@@ -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
- }
@@ -1,16 +0,0 @@
1
- import type { MetadataRoute } from "next";
2
-
3
- const APP_BASE_URL =
4
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
5
-
6
- export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
7
- const baseRoutes: MetadataRoute.Sitemap = [
8
- {
9
- url: APP_BASE_URL,
10
- lastModified: new Date(),
11
- changeFrequency: "daily",
12
- priority: 1,
13
- },
14
- ];
15
- return baseRoutes;
16
- }
@@ -1,16 +0,0 @@
1
- import type { MetadataRoute } from "next";
2
-
3
- const APP_BASE_URL =
4
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
5
-
6
- export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
7
- const baseRoutes: MetadataRoute.Sitemap = [
8
- {
9
- url: APP_BASE_URL,
10
- lastModified: new Date(),
11
- changeFrequency: "daily",
12
- priority: 1,
13
- },
14
- ];
15
- return baseRoutes;
16
- }
@@ -1,16 +0,0 @@
1
- import type { MetadataRoute } from "next";
2
-
3
- const APP_BASE_URL =
4
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
5
-
6
- export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
7
- const baseRoutes: MetadataRoute.Sitemap = [
8
- {
9
- url: APP_BASE_URL,
10
- lastModified: new Date(),
11
- changeFrequency: "daily",
12
- priority: 1,
13
- },
14
- ];
15
- return baseRoutes;
16
- }