bsmnt 0.3.2 → 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.
- package/dist/helpers/integrate/merge-config.js +1 -1
- package/dist/helpers/integrate/merge-config.js.map +1 -1
- package/dist/helpers/integrate/merge-orchestrator.d.ts.map +1 -1
- package/dist/helpers/integrate/merge-orchestrator.js +5 -5
- package/dist/helpers/integrate/merge-orchestrator.js.map +1 -1
- package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
- package/dist/helpers/integrate/sanity/config.js +2 -1
- package/dist/helpers/integrate/sanity/config.js.map +1 -1
- package/dist/helpers/integrate/sanity/mergers/sitemap-merger.d.ts +1 -3
- package/dist/helpers/integrate/sanity/mergers/sitemap-merger.d.ts.map +1 -1
- package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js +117 -76
- package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js.map +1 -1
- package/package.json +1 -1
- package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +2 -1
- package/src/helpers/integrate/sanity/files/app/api/revalidate/route.ts +4 -1
- package/src/helpers/integrate/sanity/files/app/blog/[slug]/page.tsx +3 -1
- package/src/helpers/integrate/sanity/files/app/layout.tsx +2 -2
- package/src/helpers/integrate/sanity/files/app/sitemap.md/route.ts +29 -18
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/WEBHOOK-SETUP.md +74 -0
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/env.ts +4 -2
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/queries.ts +42 -0
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sitemap.ts +90 -0
- package/src/helpers/integrate/sanity/files/lib/utils/metadata.ts +2 -2
- package/src/helpers/integrate/sanity/files/lib/utils/url.ts +23 -0
- package/src/templates/next-default/.env.example +3 -3
- package/src/templates/next-default/app/layout.tsx +2 -2
- package/src/templates/next-default/app/robots.ts +2 -2
- package/src/templates/next-default/app/sitemap.xml/route.ts +51 -0
- package/src/templates/next-default/lib/utils/metadata.ts +2 -2
- package/src/templates/next-default/lib/utils/url.ts +16 -0
- package/src/templates/next-experiments/.env.example +3 -3
- package/src/templates/next-experiments/app/layout.tsx +2 -2
- package/src/templates/next-experiments/app/robots.ts +0 -4
- package/src/templates/next-experiments/lib/utils/metadata.ts +2 -2
- package/src/templates/next-experiments/lib/utils/url.ts +16 -0
- package/src/templates/next-pagebuilder/.biome/plugins/README.md +21 -0
- package/src/templates/next-pagebuilder/.biome/plugins/no-anchor-element.grit +12 -0
- package/src/templates/next-pagebuilder/.biome/plugins/no-relative-parent-imports.grit +10 -0
- package/src/templates/next-pagebuilder/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
- package/src/templates/next-webgl/.env.example +3 -3
- package/src/templates/next-webgl/app/layout.tsx +2 -2
- package/src/templates/next-webgl/app/robots.ts +2 -2
- package/src/templates/next-webgl/app/sitemap.xml/route.ts +51 -0
- package/src/templates/next-webgl/lib/utils/metadata.ts +2 -2
- package/src/templates/next-webgl/lib/utils/url.ts +16 -0
- package/src/helpers/integrate/sanity/files/app/sitemap.ts +0 -61
- package/src/templates/next-default/app/sitemap.ts +0 -16
- package/src/templates/next-experiments/app/sitemap.ts +0 -16
- package/src/templates/next-webgl/app/sitemap.ts +0 -16
|
@@ -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
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## Plugins
|
|
2
|
+
|
|
3
|
+
### 1. `no-anchor-element.grit`
|
|
4
|
+
Enforces using Next.js `<Link>` component instead of HTML `<a>` elements.
|
|
5
|
+
|
|
6
|
+
### 2. `no-unnecessary-forwardref.grit`
|
|
7
|
+
Checks for unnecessary `forwardRef` usage in React 19 with the compiler.
|
|
8
|
+
|
|
9
|
+
### 3. `no-relative-parent-imports.grit`
|
|
10
|
+
Forbids relative parent imports (`../`) and encourages alias imports (`@/`).
|
|
11
|
+
|
|
12
|
+
## Plugin Configuration
|
|
13
|
+
|
|
14
|
+
The plugins are configured in `biome.json`:
|
|
15
|
+
```json
|
|
16
|
+
"plugins": [
|
|
17
|
+
"./biome-plugins/no-anchor-element.grit",
|
|
18
|
+
"./biome-plugins/no-unnecessary-forwardref.grit",
|
|
19
|
+
"./biome-plugins/no-relative-parent-imports.grit"
|
|
20
|
+
]
|
|
21
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
language js;
|
|
2
|
+
|
|
3
|
+
`<a $attrs>$content</a>` as $anchor where {
|
|
4
|
+
!$anchor <: within `if ($condition) { return ($jsx) }` where {
|
|
5
|
+
$condition <: contains or { `isExternal`, `isExternalSSR` }
|
|
6
|
+
},
|
|
7
|
+
register_diagnostic(
|
|
8
|
+
span = $anchor,
|
|
9
|
+
message = "Use custom Link component instead of <a> element. The Link component handles both internal and external links automatically.",
|
|
10
|
+
severity = "error"
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
language js;
|
|
2
|
+
|
|
3
|
+
`import $imports from $source` as $import where {
|
|
4
|
+
$source <: r"['\"]\.\.\/\.\.\/.*['\"]",
|
|
5
|
+
register_diagnostic(
|
|
6
|
+
span = $import,
|
|
7
|
+
message = "Use alias imports (~/dir/) instead of deep relative imports (../../). Single level imports (../) are allowed for colocated files.",
|
|
8
|
+
severity = "error"
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
# CORE (Recommended)
|
|
14
14
|
# ============================================
|
|
15
15
|
|
|
16
|
-
# Base URL
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
|
-
}
|