bsmnt 0.2.0 → 0.2.4
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/package.json +21 -4
- package/src/templates/next-default/.biome/plugins/no-anchor-element.grit +12 -0
- package/src/templates/next-default/.biome/plugins/no-relative-parent-imports.grit +10 -0
- package/src/templates/next-default/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
- package/src/templates/next-default/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/src/templates/next-default/.vscode/extensions.json +20 -0
- package/src/templates/next-default/.vscode/settings.json +105 -0
- package/src/templates/next-default/app/favicon.ico +0 -0
- package/src/templates/next-default/app/layout.tsx +104 -0
- package/src/templates/next-default/app/page.tsx +11 -0
- package/src/templates/next-default/app/robots.ts +15 -0
- package/src/templates/next-default/app/sitemap.ts +16 -0
- package/src/templates/next-default/biome.json +2 -2
- package/src/templates/next-default/components/layout/footer/index.tsx +31 -0
- package/src/templates/next-default/components/layout/header/index.tsx +9 -0
- package/src/templates/next-default/components/layout/theme/index.tsx +66 -0
- package/src/templates/next-default/components/layout/wrapper/index.tsx +63 -0
- package/src/templates/next-default/components/ui/README.md +77 -0
- package/src/templates/next-default/components/ui/image/README.md +37 -0
- package/src/templates/next-default/components/ui/image/index.tsx +219 -0
- package/src/templates/next-default/components/ui/link/index.tsx +152 -0
- package/src/templates/next-default/lib/utils/metadata.ts +26 -26
- package/src/templates/next-default/package.json +4 -4
- package/src/templates/next-default/public/fonts/geist/Geist-Mono.woff2 +0 -0
- package/src/templates/next-experiments/app/layout.tsx +18 -18
- package/src/templates/next-experiments/app/robots.ts +3 -3
- package/src/templates/next-experiments/app/sitemap.ts +4 -4
- package/src/templates/next-experiments/biome.json +2 -2
- package/src/templates/next-experiments/components/layout/theme/index.tsx +1 -1
- package/src/templates/next-experiments/components/layout/wrapper/index.tsx +7 -9
- package/src/templates/next-experiments/components/ui/image/index.tsx +39 -46
- package/src/templates/next-experiments/components/ui/link/index.tsx +6 -0
- package/src/templates/next-experiments/lib/utils/metadata.ts +26 -26
- package/src/templates/next-experiments/package.json +4 -4
- package/src/templates/next-webgl/app/layout.tsx +18 -18
- package/src/templates/next-webgl/app/robots.ts +3 -3
- package/src/templates/next-webgl/app/sitemap.ts +4 -4
- package/src/templates/next-webgl/biome.json +2 -2
- package/src/templates/next-webgl/components/layout/theme/index.tsx +1 -1
- package/src/templates/next-webgl/components/layout/wrapper/index.tsx +0 -2
- package/src/templates/next-webgl/components/ui/image/index.tsx +6 -11
- package/src/templates/next-webgl/components/ui/link/index.tsx +9 -2
- package/src/templates/next-webgl/components/webgl/components/scene/index.tsx +1 -0
- package/src/templates/next-webgl/lib/utils/metadata.ts +26 -26
- package/src/templates/next-webgl/package.json +4 -4
- package/src/templates/next-experiments/.cursor/rules/README.md +0 -184
- package/src/templates/next-experiments/.cursor/rules/architecture.mdc +0 -437
- package/src/templates/next-experiments/.cursor/rules/components.mdc +0 -436
- package/src/templates/next-experiments/.cursor/rules/integrations.mdc +0 -447
- package/src/templates/next-experiments/.cursor/rules/main.mdc +0 -278
- package/src/templates/next-experiments/.cursor/rules/styling.mdc +0 -433
- package/src/templates/next-experiments/.github/workflows/lighthouse-to-slack.yml +0 -136
- package/src/templates/next-webgl/.cursor/rules/README.md +0 -184
- package/src/templates/next-webgl/.cursor/rules/architecture.mdc +0 -437
- package/src/templates/next-webgl/.cursor/rules/components.mdc +0 -436
- package/src/templates/next-webgl/.cursor/rules/integrations.mdc +0 -447
- package/src/templates/next-webgl/.cursor/rules/main.mdc +0 -278
- package/src/templates/next-webgl/.cursor/rules/styling.mdc +0 -433
- package/src/templates/next-webgl/.github/workflows/lighthouse-to-slack.yml +0 -136
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* Next.js Image wrapper with optimized defaults and error handling.
|
|
5
5
|
* Always use this component instead of next/image directly.
|
|
6
6
|
*/
|
|
7
|
-
"use client"
|
|
7
|
+
"use client";
|
|
8
8
|
|
|
9
|
-
import cn from "clsx"
|
|
10
|
-
import NextImage, { type ImageProps as NextImageProps } from "next/image"
|
|
11
|
-
import type { CSSProperties, Ref } from "react"
|
|
12
|
-
import { breakpoints } from "@/lib/styles/config"
|
|
13
|
-
import s from "./image.module.css"
|
|
9
|
+
import cn from "clsx";
|
|
10
|
+
import NextImage, { type ImageProps as NextImageProps } from "next/image";
|
|
11
|
+
import type { CSSProperties, Ref } from "react";
|
|
12
|
+
import { breakpoints } from "@/lib/styles/config";
|
|
13
|
+
import s from "./image.module.css";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Enhanced Image component props extending Next.js Image.
|
|
@@ -20,26 +20,24 @@ import s from "./image.module.css"
|
|
|
20
20
|
*/
|
|
21
21
|
export type ImageProps = Omit<NextImageProps, "objectFit" | "alt"> & {
|
|
22
22
|
/** CSS object-fit property for image positioning */
|
|
23
|
-
objectFit?: CSSProperties["objectFit"]
|
|
23
|
+
objectFit?: CSSProperties["objectFit"];
|
|
24
24
|
/** Display as block element (adds display: block) */
|
|
25
|
-
block?: boolean
|
|
25
|
+
block?: boolean;
|
|
26
26
|
/** Size on mobile devices (e.g., "100vw", "50vw") */
|
|
27
|
-
mobileSize?: `${number}vw
|
|
27
|
+
mobileSize?: `${number}vw`;
|
|
28
28
|
/** Size on desktop devices (e.g., "33vw", "25vw") */
|
|
29
|
-
desktopSize?: `${number}vw
|
|
29
|
+
desktopSize?: `${number}vw`;
|
|
30
30
|
/** Ref for accessing the underlying img element */
|
|
31
|
-
ref?: Ref<HTMLImageElement
|
|
32
|
-
/** Alt text for accessibility (required for meaningful images) */
|
|
33
|
-
alt?: string
|
|
31
|
+
ref?: Ref<HTMLImageElement>;
|
|
34
32
|
/** Aspect ratio for automatic placeholder and layout stability */
|
|
35
|
-
aspectRatio?: number
|
|
36
|
-
}
|
|
33
|
+
aspectRatio?: number;
|
|
34
|
+
};
|
|
37
35
|
|
|
38
36
|
// Memoize helper functions to avoid recreation
|
|
39
37
|
const toBase64 = (str: string) =>
|
|
40
38
|
typeof window === "undefined"
|
|
41
39
|
? Buffer.from(str).toString("base64")
|
|
42
|
-
: window.btoa(str)
|
|
40
|
+
: window.btoa(str);
|
|
43
41
|
|
|
44
42
|
// Helper to generate blur placeholder with transparent background by default
|
|
45
43
|
const generateShimmer = (w: number, h: number) => `
|
|
@@ -54,7 +52,7 @@ const generateShimmer = (w: number, h: number) => `
|
|
|
54
52
|
<rect width="${w}" height="${h}" fill="rgba(0,0,0,0)" />
|
|
55
53
|
<rect id="r" width="${w}" height="${h}" fill="url(#g)" />
|
|
56
54
|
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
|
|
57
|
-
</svg
|
|
55
|
+
</svg>`;
|
|
58
56
|
|
|
59
57
|
// Helper to determine if blur placeholder should be used
|
|
60
58
|
const shouldUseBlurPlaceholder = (
|
|
@@ -62,10 +60,10 @@ const shouldUseBlurPlaceholder = (
|
|
|
62
60
|
placeholder: string,
|
|
63
61
|
blurDataURL: string | undefined
|
|
64
62
|
): boolean => {
|
|
65
|
-
if (!src) return false
|
|
66
|
-
const isSvg = typeof src === "string" && src.includes(".svg")
|
|
67
|
-
return !isSvg && placeholder === "blur" && !blurDataURL
|
|
68
|
-
}
|
|
63
|
+
if (!src) return false;
|
|
64
|
+
const isSvg = typeof src === "string" && src.includes(".svg");
|
|
65
|
+
return !isSvg && placeholder === "blur" && !blurDataURL;
|
|
66
|
+
};
|
|
69
67
|
|
|
70
68
|
// Helper to generate blur data URL
|
|
71
69
|
const generateBlurDataURL = (
|
|
@@ -73,11 +71,11 @@ const generateBlurDataURL = (
|
|
|
73
71
|
aspectRatio: number | undefined,
|
|
74
72
|
existingBlurDataURL: string | undefined
|
|
75
73
|
): string | undefined => {
|
|
76
|
-
if (!(shouldUse && aspectRatio)) return existingBlurDataURL
|
|
74
|
+
if (!(shouldUse && aspectRatio)) return existingBlurDataURL;
|
|
77
75
|
|
|
78
|
-
const shimmerSvg = generateShimmer(700, Math.round(700 / aspectRatio))
|
|
79
|
-
return `data:image/svg+xml;base64,${toBase64(shimmerSvg)}
|
|
80
|
-
}
|
|
76
|
+
const shimmerSvg = generateShimmer(700, Math.round(700 / aspectRatio));
|
|
77
|
+
return `data:image/svg+xml;base64,${toBase64(shimmerSvg)}`;
|
|
78
|
+
};
|
|
81
79
|
|
|
82
80
|
// Helper to determine final placeholder value
|
|
83
81
|
const getFinalPlaceholder = (
|
|
@@ -89,11 +87,11 @@ const getFinalPlaceholder = (
|
|
|
89
87
|
if (!shouldUse) {
|
|
90
88
|
return originalPlaceholder === "blur" && !blurDataURL
|
|
91
89
|
? "empty"
|
|
92
|
-
: originalPlaceholder
|
|
90
|
+
: originalPlaceholder;
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
return aspectRatio || blurDataURL ? "blur" : "empty"
|
|
96
|
-
}
|
|
93
|
+
return aspectRatio || blurDataURL ? "blur" : "empty";
|
|
94
|
+
};
|
|
97
95
|
|
|
98
96
|
/**
|
|
99
97
|
* Enhanced Image component with responsive sizing and automatic optimizations.
|
|
@@ -102,14 +100,14 @@ const getFinalPlaceholder = (
|
|
|
102
100
|
* - Automatic responsive sizes generation
|
|
103
101
|
* - Smart blur placeholders with aspect ratio support
|
|
104
102
|
* - Performance optimizations (lazy loading by default)
|
|
105
|
-
* -
|
|
103
|
+
* - Priority flag for LCP images
|
|
106
104
|
*
|
|
107
105
|
* @param props - Image props extending Next.js Image
|
|
108
106
|
* @param props.aspectRatio - Aspect ratio for layout stability and blur placeholder
|
|
109
107
|
* @param props.mobileSize - Size on mobile (e.g., "100vw")
|
|
110
108
|
* @param props.desktopSize - Size on desktop (e.g., "50vw")
|
|
111
109
|
* @param props.block - Display as block element
|
|
112
|
-
* @param props.
|
|
110
|
+
* @param props.priority - Prioritize loading for LCP images
|
|
113
111
|
*
|
|
114
112
|
* @example
|
|
115
113
|
* ```tsx
|
|
@@ -123,12 +121,12 @@ const getFinalPlaceholder = (
|
|
|
123
121
|
*
|
|
124
122
|
* @example
|
|
125
123
|
* ```tsx
|
|
126
|
-
* // LCP image with
|
|
124
|
+
* // LCP image with priority
|
|
127
125
|
* <Image
|
|
128
126
|
* src="/hero.jpg"
|
|
129
127
|
* alt="Hero image"
|
|
130
128
|
* aspectRatio={16/9}
|
|
131
|
-
*
|
|
129
|
+
* priority // Preloads image for LCP
|
|
132
130
|
* />
|
|
133
131
|
* ```
|
|
134
132
|
*
|
|
@@ -147,7 +145,6 @@ const getFinalPlaceholder = (
|
|
|
147
145
|
export function Image({
|
|
148
146
|
style,
|
|
149
147
|
className,
|
|
150
|
-
loading,
|
|
151
148
|
objectFit = "cover",
|
|
152
149
|
quality = 90,
|
|
153
150
|
alt = "",
|
|
@@ -163,38 +160,35 @@ export function Image({
|
|
|
163
160
|
ref,
|
|
164
161
|
aspectRatio,
|
|
165
162
|
placeholder = "blur",
|
|
166
|
-
|
|
163
|
+
priority = false,
|
|
167
164
|
...props
|
|
168
165
|
}: ImageProps) {
|
|
169
|
-
// Determine loading strategy
|
|
170
|
-
const finalLoading = loading ?? (preload ? "eager" : "lazy")
|
|
171
|
-
|
|
172
166
|
// Generate responsive sizes if not provided
|
|
173
167
|
const finalSizes =
|
|
174
168
|
sizes ||
|
|
175
|
-
`(max-width: ${breakpoints.desktop}px) ${mobileSize}, ${desktopSize}
|
|
169
|
+
`(max-width: ${breakpoints.desktop}px) ${mobileSize}, ${desktopSize}`;
|
|
176
170
|
|
|
177
171
|
// Early return after hooks
|
|
178
|
-
if (!src) return null
|
|
172
|
+
if (!src) return null;
|
|
179
173
|
|
|
180
174
|
// Determine SVG status and placeholder logic
|
|
181
|
-
const isSvg = typeof src === "string" && src.includes(".svg")
|
|
175
|
+
const isSvg = typeof src === "string" && src.includes(".svg");
|
|
182
176
|
const shouldUsePlaceholder = shouldUseBlurPlaceholder(
|
|
183
177
|
src,
|
|
184
178
|
placeholder,
|
|
185
179
|
props.blurDataURL
|
|
186
|
-
)
|
|
180
|
+
);
|
|
187
181
|
const blurDataURL = generateBlurDataURL(
|
|
188
182
|
shouldUsePlaceholder,
|
|
189
183
|
aspectRatio,
|
|
190
184
|
props.blurDataURL
|
|
191
|
-
)
|
|
185
|
+
);
|
|
192
186
|
const finalPlaceholder = getFinalPlaceholder(
|
|
193
187
|
shouldUsePlaceholder,
|
|
194
188
|
aspectRatio,
|
|
195
189
|
props.blurDataURL,
|
|
196
190
|
placeholder
|
|
197
|
-
)
|
|
191
|
+
);
|
|
198
192
|
|
|
199
193
|
return (
|
|
200
194
|
<NextImage
|
|
@@ -202,7 +196,7 @@ export function Image({
|
|
|
202
196
|
fill={!block}
|
|
203
197
|
{...(width !== undefined && { width })}
|
|
204
198
|
{...(height !== undefined && { height })}
|
|
205
|
-
|
|
199
|
+
priority={priority}
|
|
206
200
|
quality={quality}
|
|
207
201
|
alt={alt}
|
|
208
202
|
style={{
|
|
@@ -217,8 +211,7 @@ export function Image({
|
|
|
217
211
|
onDragStart={(e) => e.preventDefault()}
|
|
218
212
|
{...(finalPlaceholder && { placeholder: finalPlaceholder })}
|
|
219
213
|
{...(blurDataURL && { blurDataURL })}
|
|
220
|
-
preload={preload}
|
|
221
214
|
{...props}
|
|
222
215
|
/>
|
|
223
|
-
)
|
|
216
|
+
);
|
|
224
217
|
}
|
|
@@ -112,6 +112,12 @@ export function Link({
|
|
|
112
112
|
return <div {...getDivProps(props)}>{children}</div>
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// Block dangerous URIs (javascript:, data:, vbscript:)
|
|
116
|
+
const isDangerousHref = /^(javascript|data|vbscript):/i.test(href)
|
|
117
|
+
if (isDangerousHref) {
|
|
118
|
+
return <span {...getDivProps(props)}>{children}</span>
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
// For SSR, check if it's external based on the href pattern
|
|
116
122
|
const isExternalSSR =
|
|
117
123
|
href.startsWith("http://") || href.startsWith("https://")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Metadata } from "next"
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Metadata Generation Utilities
|
|
@@ -8,26 +8,26 @@ import type { Metadata } from "next"
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
interface GenerateMetadataOptions {
|
|
11
|
-
title?: string
|
|
12
|
-
description?: string
|
|
13
|
-
keywords?: string[]
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
keywords?: string[];
|
|
14
14
|
image?: {
|
|
15
|
-
url?: string
|
|
16
|
-
width?: number
|
|
17
|
-
height?: number
|
|
18
|
-
alt?: string
|
|
19
|
-
}
|
|
20
|
-
url?: string
|
|
21
|
-
siteName?: string
|
|
22
|
-
noIndex?: boolean
|
|
23
|
-
type?: "website" | "article"
|
|
24
|
-
publishedTime?: string
|
|
25
|
-
modifiedTime?: string
|
|
26
|
-
authors?: string[]
|
|
15
|
+
url?: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
alt?: string;
|
|
19
|
+
};
|
|
20
|
+
url?: string;
|
|
21
|
+
siteName?: string;
|
|
22
|
+
noIndex?: boolean;
|
|
23
|
+
type?: "website" | "article";
|
|
24
|
+
publishedTime?: string;
|
|
25
|
+
modifiedTime?: string;
|
|
26
|
+
authors?: string[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const APP_BASE_URL =
|
|
30
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
30
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Generate complete metadata object for pages
|
|
@@ -62,13 +62,13 @@ export function generatePageMetadata(
|
|
|
62
62
|
publishedTime,
|
|
63
63
|
modifiedTime,
|
|
64
64
|
authors,
|
|
65
|
-
} = options
|
|
65
|
+
} = options;
|
|
66
66
|
|
|
67
|
-
const fullUrl = url ? `${APP_BASE_URL}${url}` : APP_BASE_URL
|
|
68
|
-
const imageUrl = image?.url || "/opengraph-image.jpg"
|
|
69
|
-
const imageWidth = image?.width || 1200
|
|
70
|
-
const imageHeight = image?.height || 630
|
|
71
|
-
const imageAlt = image?.alt || title || siteName
|
|
67
|
+
const fullUrl = url ? `${APP_BASE_URL}${url}` : APP_BASE_URL;
|
|
68
|
+
const imageUrl = image?.url || "/opengraph-image.jpg";
|
|
69
|
+
const imageWidth = image?.width || 1200;
|
|
70
|
+
const imageHeight = image?.height || 630;
|
|
71
|
+
const imageAlt = image?.alt || title || siteName;
|
|
72
72
|
|
|
73
73
|
const metadata: Metadata = {
|
|
74
74
|
metadataBase: new URL(APP_BASE_URL),
|
|
@@ -113,14 +113,14 @@ export function generatePageMetadata(
|
|
|
113
113
|
other: {
|
|
114
114
|
"fb:app_id": process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "",
|
|
115
115
|
},
|
|
116
|
-
}
|
|
116
|
+
};
|
|
117
117
|
|
|
118
118
|
if (noIndex) {
|
|
119
119
|
metadata.robots = {
|
|
120
120
|
index: false,
|
|
121
121
|
follow: false,
|
|
122
|
-
}
|
|
122
|
+
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
return metadata
|
|
125
|
+
return metadata;
|
|
126
126
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "bsmnt-experiments-starter",
|
|
3
3
|
"description": "Basement Next.js starter template",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"private": true,
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"class-variance-authority": "^0.7.1",
|
|
32
32
|
"leva": "^0.9.35",
|
|
33
33
|
"lucide-react": "^0.474.0",
|
|
34
|
-
"next": "16.1.
|
|
34
|
+
"next": "16.1.6",
|
|
35
35
|
"react": "19.2.4",
|
|
36
36
|
"react-dom": "19.2.4",
|
|
37
37
|
"react-use": "^17.6.0",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"zustand": "^5.0.10"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@biomejs/biome": "2.3.
|
|
44
|
+
"@biomejs/biome": "2.3.14",
|
|
45
45
|
"@clack/prompts": "^0.11.0",
|
|
46
46
|
"@csstools/postcss-global-data": "^3.1.0",
|
|
47
|
-
"@next/bundle-analyzer": "16.1.
|
|
47
|
+
"@next/bundle-analyzer": "16.1.6",
|
|
48
48
|
"@svgr/webpack": "^8.1.0",
|
|
49
49
|
"@tailwindcss/postcss": "^4.1.18",
|
|
50
50
|
"@types/bun": "^1.3.6",
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import type { Metadata, Viewport } from "next"
|
|
2
|
-
import { Geist } from "next/font/google"
|
|
3
|
-
import { type PropsWithChildren, Suspense } from "react"
|
|
4
|
-
import { Link } from "@/components/ui/link"
|
|
5
|
-
import { themes } from "@/lib/styles/colors"
|
|
6
|
-
import { fontsVariable } from "@/lib/styles/fonts"
|
|
7
|
-
import AppData from "@/package.json"
|
|
8
|
-
import "@/lib/styles/css/index.css"
|
|
9
|
-
import { cn } from "@/lib/styles/cn"
|
|
1
|
+
import type { Metadata, Viewport } from "next";
|
|
2
|
+
import { Geist } from "next/font/google";
|
|
3
|
+
import { type PropsWithChildren, Suspense } from "react";
|
|
4
|
+
import { Link } from "@/components/ui/link";
|
|
5
|
+
import { themes } from "@/lib/styles/colors";
|
|
6
|
+
import { fontsVariable } from "@/lib/styles/fonts";
|
|
7
|
+
import AppData from "@/package.json";
|
|
8
|
+
import "@/lib/styles/css/index.css";
|
|
9
|
+
import { cn } from "@/lib/styles/cn";
|
|
10
10
|
|
|
11
|
-
const APP_NAME = AppData.name
|
|
12
|
-
const APP_DEFAULT_TITLE = "Basement Starter"
|
|
13
|
-
const APP_TITLE_TEMPLATE = "%s - Basement Starter"
|
|
14
|
-
const APP_DESCRIPTION = AppData.description
|
|
11
|
+
const APP_NAME = AppData.name;
|
|
12
|
+
const APP_DEFAULT_TITLE = "Basement Starter";
|
|
13
|
+
const APP_TITLE_TEMPLATE = "%s - Basement Starter";
|
|
14
|
+
const APP_DESCRIPTION = AppData.description;
|
|
15
15
|
const APP_BASE_URL =
|
|
16
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
16
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
17
17
|
|
|
18
18
|
const geist = Geist({
|
|
19
19
|
subsets: ["latin"],
|
|
20
|
-
})
|
|
20
|
+
});
|
|
21
21
|
|
|
22
22
|
export const metadata: Metadata = {
|
|
23
23
|
alternates: {
|
|
@@ -70,12 +70,12 @@ export const metadata: Metadata = {
|
|
|
70
70
|
template: APP_TITLE_TEMPLATE,
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
|
-
}
|
|
73
|
+
};
|
|
74
74
|
|
|
75
75
|
export const viewport: Viewport = {
|
|
76
76
|
colorScheme: "normal",
|
|
77
77
|
themeColor: themes.dark.primary,
|
|
78
|
-
}
|
|
78
|
+
};
|
|
79
79
|
|
|
80
80
|
export default async function Layout({ children }: PropsWithChildren) {
|
|
81
81
|
return (
|
|
@@ -100,5 +100,5 @@ export default async function Layout({ children }: PropsWithChildren) {
|
|
|
100
100
|
{children}
|
|
101
101
|
</body>
|
|
102
102
|
</html>
|
|
103
|
-
)
|
|
103
|
+
);
|
|
104
104
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { MetadataRoute } from "next"
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
2
|
|
|
3
3
|
const APP_BASE_URL =
|
|
4
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
4
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
5
5
|
|
|
6
6
|
export default function robots(): MetadataRoute.Robots {
|
|
7
7
|
return {
|
|
@@ -11,5 +11,5 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
11
11
|
disallow: [],
|
|
12
12
|
},
|
|
13
13
|
sitemap: `${APP_BASE_URL}/sitemap.xml`,
|
|
14
|
-
}
|
|
14
|
+
};
|
|
15
15
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { MetadataRoute } from "next"
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
2
|
|
|
3
3
|
const APP_BASE_URL =
|
|
4
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
4
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
5
5
|
|
|
6
6
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
7
7
|
const baseRoutes: MetadataRoute.Sitemap = [
|
|
@@ -11,6 +11,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
11
11
|
changeFrequency: "daily",
|
|
12
12
|
priority: 1,
|
|
13
13
|
},
|
|
14
|
-
]
|
|
15
|
-
return baseRoutes
|
|
14
|
+
];
|
|
15
|
+
return baseRoutes;
|
|
16
16
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"root":
|
|
2
|
+
"root": true,
|
|
3
3
|
"$schema": "node_modules/@biomejs/biome/configuration_schema.json",
|
|
4
4
|
"assist": {
|
|
5
5
|
"actions": {
|
|
@@ -245,6 +245,6 @@
|
|
|
245
245
|
"vcs": {
|
|
246
246
|
"clientKind": "git",
|
|
247
247
|
"enabled": true,
|
|
248
|
-
"useIgnoreFile":
|
|
248
|
+
"useIgnoreFile": false
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -102,14 +102,14 @@ const getFinalPlaceholder = (
|
|
|
102
102
|
* - Automatic responsive sizes generation
|
|
103
103
|
* - Smart blur placeholders with aspect ratio support
|
|
104
104
|
* - Performance optimizations (lazy loading by default)
|
|
105
|
-
* -
|
|
105
|
+
* - Priority flag for LCP images
|
|
106
106
|
*
|
|
107
107
|
* @param props - Image props extending Next.js Image
|
|
108
108
|
* @param props.aspectRatio - Aspect ratio for layout stability and blur placeholder
|
|
109
109
|
* @param props.mobileSize - Size on mobile (e.g., "100vw")
|
|
110
110
|
* @param props.desktopSize - Size on desktop (e.g., "50vw")
|
|
111
111
|
* @param props.block - Display as block element
|
|
112
|
-
* @param props.
|
|
112
|
+
* @param props.priority - Prioritize loading for LCP images
|
|
113
113
|
*
|
|
114
114
|
* @example
|
|
115
115
|
* ```tsx
|
|
@@ -123,12 +123,12 @@ const getFinalPlaceholder = (
|
|
|
123
123
|
*
|
|
124
124
|
* @example
|
|
125
125
|
* ```tsx
|
|
126
|
-
* // LCP image with
|
|
126
|
+
* // LCP image with priority
|
|
127
127
|
* <Image
|
|
128
128
|
* src="/hero.jpg"
|
|
129
129
|
* alt="Hero image"
|
|
130
130
|
* aspectRatio={16/9}
|
|
131
|
-
*
|
|
131
|
+
* priority // Preloads image for LCP
|
|
132
132
|
* />
|
|
133
133
|
* ```
|
|
134
134
|
*
|
|
@@ -147,7 +147,6 @@ const getFinalPlaceholder = (
|
|
|
147
147
|
export function Image({
|
|
148
148
|
style,
|
|
149
149
|
className,
|
|
150
|
-
loading,
|
|
151
150
|
objectFit = "cover",
|
|
152
151
|
quality = 90,
|
|
153
152
|
alt = "",
|
|
@@ -163,12 +162,9 @@ export function Image({
|
|
|
163
162
|
ref,
|
|
164
163
|
aspectRatio,
|
|
165
164
|
placeholder = "blur",
|
|
166
|
-
|
|
165
|
+
priority = false,
|
|
167
166
|
...props
|
|
168
167
|
}: ImageProps) {
|
|
169
|
-
// Determine loading strategy
|
|
170
|
-
const finalLoading = loading ?? (preload ? "eager" : "lazy")
|
|
171
|
-
|
|
172
168
|
// Generate responsive sizes if not provided
|
|
173
169
|
const finalSizes =
|
|
174
170
|
sizes ||
|
|
@@ -202,7 +198,7 @@ export function Image({
|
|
|
202
198
|
fill={!block}
|
|
203
199
|
{...(width !== undefined && { width })}
|
|
204
200
|
{...(height !== undefined && { height })}
|
|
205
|
-
|
|
201
|
+
priority={priority}
|
|
206
202
|
quality={quality}
|
|
207
203
|
alt={alt}
|
|
208
204
|
style={{
|
|
@@ -217,7 +213,6 @@ export function Image({
|
|
|
217
213
|
onDragStart={(e) => e.preventDefault()}
|
|
218
214
|
{...(finalPlaceholder && { placeholder: finalPlaceholder })}
|
|
219
215
|
{...(blurDataURL && { blurDataURL })}
|
|
220
|
-
preload={preload}
|
|
221
216
|
{...props}
|
|
222
217
|
/>
|
|
223
218
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import NextLink from "next/link"
|
|
4
|
+
import Link from "next/link"
|
|
4
5
|
import { usePathname } from "next/navigation"
|
|
5
6
|
import {
|
|
6
7
|
type AnchorHTMLAttributes,
|
|
@@ -112,13 +113,19 @@ export function Link({
|
|
|
112
113
|
return <div {...getDivProps(props)}>{children}</div>
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// Block dangerous URIs (javascript:, data:, vbscript:)
|
|
117
|
+
const isDangerousHref = /^(javascript|data|vbscript):/i.test(href)
|
|
118
|
+
if (isDangerousHref) {
|
|
119
|
+
return <span {...getDivProps(props)}>{children}</span>
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
// For SSR, check if it's external based on the href pattern
|
|
116
123
|
const isExternalSSR =
|
|
117
124
|
href.startsWith("http://") || href.startsWith("https://")
|
|
118
125
|
|
|
119
126
|
if (isExternalSSR || isExternal) {
|
|
120
127
|
return (
|
|
121
|
-
<
|
|
128
|
+
<Link
|
|
122
129
|
href={href}
|
|
123
130
|
target="_blank"
|
|
124
131
|
rel="noopener noreferrer"
|
|
@@ -127,7 +134,7 @@ export function Link({
|
|
|
127
134
|
{...props}
|
|
128
135
|
>
|
|
129
136
|
{children}
|
|
130
|
-
</
|
|
137
|
+
</Link>
|
|
131
138
|
)
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Metadata } from "next"
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Metadata Generation Utilities
|
|
@@ -8,26 +8,26 @@ import type { Metadata } from "next"
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
interface GenerateMetadataOptions {
|
|
11
|
-
title?: string
|
|
12
|
-
description?: string
|
|
13
|
-
keywords?: string[]
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
keywords?: string[];
|
|
14
14
|
image?: {
|
|
15
|
-
url?: string
|
|
16
|
-
width?: number
|
|
17
|
-
height?: number
|
|
18
|
-
alt?: string
|
|
19
|
-
}
|
|
20
|
-
url?: string
|
|
21
|
-
siteName?: string
|
|
22
|
-
noIndex?: boolean
|
|
23
|
-
type?: "website" | "article"
|
|
24
|
-
publishedTime?: string
|
|
25
|
-
modifiedTime?: string
|
|
26
|
-
authors?: string[]
|
|
15
|
+
url?: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
alt?: string;
|
|
19
|
+
};
|
|
20
|
+
url?: string;
|
|
21
|
+
siteName?: string;
|
|
22
|
+
noIndex?: boolean;
|
|
23
|
+
type?: "website" | "article";
|
|
24
|
+
publishedTime?: string;
|
|
25
|
+
modifiedTime?: string;
|
|
26
|
+
authors?: string[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const APP_BASE_URL =
|
|
30
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
30
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Generate complete metadata object for pages
|
|
@@ -62,13 +62,13 @@ export function generatePageMetadata(
|
|
|
62
62
|
publishedTime,
|
|
63
63
|
modifiedTime,
|
|
64
64
|
authors,
|
|
65
|
-
} = options
|
|
65
|
+
} = options;
|
|
66
66
|
|
|
67
|
-
const fullUrl = url ? `${APP_BASE_URL}${url}` : APP_BASE_URL
|
|
68
|
-
const imageUrl = image?.url || "/opengraph-image.jpg"
|
|
69
|
-
const imageWidth = image?.width || 1200
|
|
70
|
-
const imageHeight = image?.height || 630
|
|
71
|
-
const imageAlt = image?.alt || title || siteName
|
|
67
|
+
const fullUrl = url ? `${APP_BASE_URL}${url}` : APP_BASE_URL;
|
|
68
|
+
const imageUrl = image?.url || "/opengraph-image.jpg";
|
|
69
|
+
const imageWidth = image?.width || 1200;
|
|
70
|
+
const imageHeight = image?.height || 630;
|
|
71
|
+
const imageAlt = image?.alt || title || siteName;
|
|
72
72
|
|
|
73
73
|
const metadata: Metadata = {
|
|
74
74
|
metadataBase: new URL(APP_BASE_URL),
|
|
@@ -113,14 +113,14 @@ export function generatePageMetadata(
|
|
|
113
113
|
other: {
|
|
114
114
|
"fb:app_id": process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "",
|
|
115
115
|
},
|
|
116
|
-
}
|
|
116
|
+
};
|
|
117
117
|
|
|
118
118
|
if (noIndex) {
|
|
119
119
|
metadata.robots = {
|
|
120
120
|
index: false,
|
|
121
121
|
follow: false,
|
|
122
|
-
}
|
|
122
|
+
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
return metadata
|
|
125
|
+
return metadata;
|
|
126
126
|
}
|