bsmnt 0.2.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/configs/skills.d.ts +27 -0
  2. package/dist/configs/skills.d.ts.map +1 -0
  3. package/dist/configs/skills.js +18 -0
  4. package/dist/configs/skills.js.map +1 -0
  5. package/dist/configs/skills.json +26 -0
  6. package/dist/helpers/add/hooks-config.d.ts.map +1 -1
  7. package/dist/helpers/add/hooks-config.js +0 -6
  8. package/dist/helpers/add/hooks-config.js.map +1 -1
  9. package/dist/helpers/create/copy-template.d.ts +1 -1
  10. package/dist/helpers/create/copy-template.d.ts.map +1 -1
  11. package/dist/helpers/create/index.d.ts.map +1 -1
  12. package/dist/helpers/create/index.js +2 -1
  13. package/dist/helpers/create/index.js.map +1 -1
  14. package/dist/helpers/create/setup-agent.d.ts.map +1 -1
  15. package/dist/helpers/create/setup-agent.js +15 -5
  16. package/dist/helpers/create/setup-agent.js.map +1 -1
  17. package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
  18. package/dist/helpers/integrate/merge-config.js +1 -2
  19. package/dist/helpers/integrate/merge-config.js.map +1 -1
  20. package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
  21. package/dist/helpers/integrate/sanity/config.js +5 -10
  22. package/dist/helpers/integrate/sanity/config.js.map +1 -1
  23. package/dist/helpers/integrate/sanity/mergers/layout-merger.d.ts.map +1 -1
  24. package/dist/helpers/integrate/sanity/mergers/layout-merger.js +13 -12
  25. package/dist/helpers/integrate/sanity/mergers/layout-merger.js.map +1 -1
  26. package/dist/helpers/skills/index.d.ts +10 -0
  27. package/dist/helpers/skills/index.d.ts.map +1 -0
  28. package/dist/helpers/skills/index.js +136 -0
  29. package/dist/helpers/skills/index.js.map +1 -0
  30. package/dist/index.js +102 -35
  31. package/dist/index.js.map +1 -1
  32. package/package.json +3 -2
  33. package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +2 -1
  34. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/confirm-publish-action.ts +31 -0
  35. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.config.ts +17 -0
  36. package/src/helpers/integrate/sanity/files/lib/utils/json-ld.tsx +249 -0
  37. package/src/template-hooks/config.js +0 -6
  38. package/src/templates/next-default/app/layout.tsx +18 -0
  39. package/src/templates/next-default/lib/hooks/use-device-detection.ts +1 -1
  40. package/src/templates/next-default/lib/hooks/use-media-breakpoint.ts +1 -1
  41. package/src/templates/next-default/lib/hooks/use-media.ts +29 -0
  42. package/src/templates/next-default/lib/utils/json-ld.tsx +199 -0
  43. package/src/templates/next-default/package.json +1 -1
  44. package/src/templates/next-default/tsconfig.json +1 -0
  45. package/src/templates/next-experiments/app/layout.tsx +18 -0
  46. package/src/templates/next-experiments/lib/hooks/use-device-detection.ts +1 -1
  47. package/src/templates/next-experiments/lib/hooks/use-media-breakpoint.ts +1 -1
  48. package/src/templates/next-experiments/lib/hooks/use-media.ts +29 -0
  49. package/src/templates/next-experiments/lib/utils/json-ld.tsx +199 -0
  50. package/src/templates/next-experiments/package.json +1 -1
  51. package/src/templates/next-experiments/tsconfig.json +1 -0
  52. package/src/templates/next-pagebuilder/.env.example +11 -0
  53. package/src/templates/next-pagebuilder/README.md +23 -0
  54. package/src/templates/next-pagebuilder/_gitignore +67 -0
  55. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +68 -0
  56. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +13 -0
  57. package/src/templates/next-pagebuilder/app/api/[[...slug]]/route.ts +100 -0
  58. package/src/templates/next-pagebuilder/app/api/draft-mode/disable/route.ts +7 -0
  59. package/src/templates/next-pagebuilder/app/api/draft-mode/enable/route.ts +20 -0
  60. package/src/templates/next-pagebuilder/app/api/revalidate/route.ts +121 -0
  61. package/src/templates/next-pagebuilder/app/favicon.ico +0 -0
  62. package/src/templates/next-pagebuilder/app/layout.tsx +80 -0
  63. package/src/templates/next-pagebuilder/app/robots.ts +15 -0
  64. package/src/templates/next-pagebuilder/app/sitemap.md/route.ts +124 -0
  65. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +80 -0
  66. package/src/templates/next-pagebuilder/app/studio/[[...tool]]/page.tsx +8 -0
  67. package/src/templates/next-pagebuilder/biome.json +239 -0
  68. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +95 -0
  69. package/src/templates/next-pagebuilder/components/layout/header/components/cta-button.tsx +28 -0
  70. package/src/templates/next-pagebuilder/components/layout/header/components/mega-menu-panel.tsx +90 -0
  71. package/src/templates/next-pagebuilder/components/layout/header/components/nav-item-renderer.tsx +98 -0
  72. package/src/templates/next-pagebuilder/components/layout/header/components/nav-leaf-item.tsx +33 -0
  73. package/src/templates/next-pagebuilder/components/layout/header/components/types.ts +7 -0
  74. package/src/templates/next-pagebuilder/components/layout/header/header-client.tsx +110 -0
  75. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +8 -0
  76. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +45 -0
  77. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +30 -0
  78. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +83 -0
  79. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +27 -0
  80. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +17 -0
  81. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +17 -0
  82. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-card.tsx +66 -0
  83. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-grid.tsx +42 -0
  84. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +28 -0
  85. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts +16 -0
  86. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +36 -0
  87. package/src/templates/next-pagebuilder/components/page-builder/types.ts +23 -0
  88. package/src/templates/next-pagebuilder/components/page-document/index.tsx +91 -0
  89. package/src/templates/next-pagebuilder/components/sanity/draft-mode-toggle.tsx +27 -0
  90. package/src/templates/next-pagebuilder/components/sanity/rich-text.tsx +87 -0
  91. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +27 -0
  92. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +216 -0
  93. package/src/templates/next-pagebuilder/components/ui/link/index.tsx +152 -0
  94. package/src/templates/next-pagebuilder/components/ui/sanity-image/index.tsx +41 -0
  95. package/src/templates/next-pagebuilder/lib/integrations/check-integration.ts +5 -0
  96. package/src/templates/next-pagebuilder/lib/integrations/sanity/client.ts +27 -0
  97. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  98. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-builder-input.tsx +36 -0
  99. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  100. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/rich-text.tsx +84 -0
  101. package/src/templates/next-pagebuilder/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  102. package/src/templates/next-pagebuilder/lib/integrations/sanity/env.ts +34 -0
  103. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +35 -0
  104. package/src/templates/next-pagebuilder/lib/integrations/sanity/icons.ts +58 -0
  105. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +61 -0
  106. package/src/templates/next-pagebuilder/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  107. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-builder-config.ts +132 -0
  108. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-category.ts +28 -0
  109. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +281 -0
  110. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.cli.ts +29 -0
  111. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +211 -0
  112. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/index.ts +4 -0
  113. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  114. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  115. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  116. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  117. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/author.ts +70 -0
  118. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  119. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/index.ts +96 -0
  120. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/company-data.ts +62 -0
  121. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  122. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/navbar.ts +74 -0
  123. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  124. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  125. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/metadata.ts +68 -0
  126. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  127. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
  128. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  129. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page.ts +232 -0
  130. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  131. package/src/templates/next-pagebuilder/lib/integrations/sanity/singletons.ts +44 -0
  132. package/src/templates/next-pagebuilder/lib/integrations/sanity/structure.ts +453 -0
  133. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/image.ts +8 -0
  134. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/link.ts +137 -0
  135. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
  136. package/src/templates/next-pagebuilder/lib/scripts/sanity-typegen.ts +45 -0
  137. package/src/templates/next-pagebuilder/lib/styles/cn.ts +5 -0
  138. package/src/templates/next-pagebuilder/lib/styles/global.css +70 -0
  139. package/src/templates/next-pagebuilder/lib/utils/base-url.ts +17 -0
  140. package/src/templates/next-pagebuilder/lib/utils/format-date.ts +8 -0
  141. package/src/templates/next-pagebuilder/lib/utils/json-ld.tsx +213 -0
  142. package/src/templates/next-pagebuilder/lib/utils/metadata.ts +167 -0
  143. package/src/templates/next-pagebuilder/lib/utils/sitemap.ts +37 -0
  144. package/src/templates/next-pagebuilder/lib/utils/slug-tag.ts +6 -0
  145. package/src/templates/next-pagebuilder/next.config.ts +134 -0
  146. package/src/templates/next-pagebuilder/package.json +71 -0
  147. package/src/templates/next-pagebuilder/postcss.config.mjs +39 -0
  148. package/src/templates/next-pagebuilder/proxy.ts +81 -0
  149. package/src/templates/next-pagebuilder/svg.d.ts +5 -0
  150. package/src/templates/next-pagebuilder/tsconfig.json +38 -0
  151. package/src/templates/next-webgl/app/layout.tsx +18 -0
  152. package/src/templates/next-webgl/lib/hooks/use-device-detection.ts +1 -1
  153. package/src/templates/next-webgl/lib/hooks/use-media-breakpoint.ts +1 -1
  154. package/src/templates/next-webgl/lib/hooks/use-media.ts +29 -0
  155. package/src/templates/next-webgl/lib/utils/json-ld.tsx +199 -0
  156. package/src/templates/next-webgl/package.json +1 -1
  157. package/src/templates/next-webgl/tsconfig.json +1 -0
  158. package/plugins/no-anchor-element.grit +0 -11
  159. package/plugins/no-relative-parent-imports.grit +0 -6
  160. package/plugins/no-unnecessary-forwardref.grit +0 -5
  161. package/src/helpers/integrate/sanity/files/lib/scripts/copy-sanity-mcp.ts +0 -23
  162. package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +0 -297
  163. package/src/template-hooks/use-media.ts +0 -33
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "next-pagebuilder",
3
+ "description": "Basement Next.js starter template",
4
+ "version": "0.1.0",
5
+ "private": true,
6
+ "scripts": {
7
+ "analyze": "cross-env ANALYZE=true bun run build",
8
+ "analyze:experimental": "next experimental-analyze",
9
+ "build": "next build",
10
+ "dev": "next dev",
11
+ "format": "biome format --write .",
12
+ "generate": "bun ./lib/scripts/generate.ts",
13
+ "lighthouse": "bunx @unlighthouse/cli --site http://localhost:3000",
14
+ "lint": "biome lint --max-diagnostics=200",
15
+ "lint:fix": "biome lint --write --unsafe --max-diagnostics=200",
16
+ "start": "next start",
17
+ "test": "bun test",
18
+ "typecheck": "tsgo --noEmit",
19
+ "typecheck:compare": "bun run typecheck:tsc && bun run typecheck",
20
+ "typecheck:tsc": "tsc --noEmit --incremental false",
21
+ "sanity:extract": "cd lib/integrations/sanity && bun --env-file ../../../.env.local sanity schema extract",
22
+ "sanity:typegen": "bun ./lib/scripts/sanity-typegen.ts",
23
+ "predev": "bun run sanity:typegen",
24
+ "prebuild": "bun run sanity:typegen"
25
+ },
26
+ "dependencies": {
27
+ "@phosphor-icons/react": "^2.1.10",
28
+ "@portabletext/types": "^4.0.2",
29
+ "@sanity/asset-utils": "^2.3.0",
30
+ "@sanity/image-url": "^2.0.3",
31
+ "@sanity/table": "^2.0.1",
32
+ "@sanity/vision": "^5.17.1",
33
+ "@sanity/visual-editing": "^5.3.1",
34
+ "class-variance-authority": "^0.7.1",
35
+ "next": "16.2.3",
36
+ "next-sanity": "^12.1.4",
37
+ "react": "19.2.4",
38
+ "react-dom": "19.2.4",
39
+ "react-use": "^17.6.0",
40
+ "sanity": "^5.17.1",
41
+ "sanity-plugin-media": "^4.1.1",
42
+ "schema-dts": "^2.0.0",
43
+ "tailwind-merge": "^3.5.0",
44
+ "zod": "^4.3.6"
45
+ },
46
+ "devDependencies": {
47
+ "@biomejs/biome": "2.4.8",
48
+ "@clack/prompts": "^1.1.0",
49
+ "@csstools/postcss-global-data": "^4.0.0",
50
+ "@next/bundle-analyzer": "16.2.1",
51
+ "@svgr/webpack": "^8.1.0",
52
+ "@tailwindcss/postcss": "^4.2.2",
53
+ "@types/bun": "^1.3.11",
54
+ "@types/node": "^25.5.0",
55
+ "@types/react": "^19.2.14",
56
+ "@types/react-dom": "^19.2.3",
57
+ "@typescript/native-preview": "^7.0.0-dev.20260323.1",
58
+ "babel-plugin-react-compiler": "1.0.0",
59
+ "cross-env": "^10.1.0",
60
+ "postcss-functions": "^4.0.2",
61
+ "postcss-preset-env": "^11.2.0",
62
+ "tailwindcss": "^4.2.2",
63
+ "typescript": "^5.9.3"
64
+ },
65
+ "trustedDependencies": [
66
+ "@biomejs/biome",
67
+ "@tailwindcss/oxide",
68
+ "esbuild",
69
+ "sharp"
70
+ ]
71
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * PostCSS preset-env config
3
+ * @see Docs {@link https://github.com/csstools/postcss-plugins/blob/main/plugin-packs/postcss-preset-env/README.md#options}
4
+ * @see Features Flags {@link https://github.com/csstools/postcss-plugins/blob/main/plugin-packs/postcss-preset-env/FEATURES.md}
5
+ * @type {import('postcss-preset-env').pluginOptions}
6
+ */
7
+ const presetEnvConfig = {
8
+ autoprefixer: {
9
+ flexbox: "no-2009",
10
+ },
11
+ stage: 3,
12
+ features: {
13
+ "custom-properties": false,
14
+ "custom-media-queries": true,
15
+ "nesting-rules": true,
16
+ },
17
+ }
18
+
19
+ /**
20
+ * PostCSS global data config
21
+ * Makes sure the css module files have access to these context files
22
+ * @see {@link https://github.com/csstools/postcss-global-data?tab=readme-ov-file#options}
23
+ * @type {import('@csstools/postcss-global-data').pluginOptions}
24
+ */
25
+ const globalDataConfig = {
26
+ files: ["./lib/styles/global.css"],
27
+ }
28
+
29
+ const postcssConfig = {
30
+ // NOTE: Order is important
31
+ plugins: {
32
+ "@tailwindcss/postcss": {},
33
+ "@csstools/postcss-global-data": globalDataConfig,
34
+ // NOTE: This has to be last config
35
+ "postcss-preset-env": presetEnvConfig,
36
+ },
37
+ }
38
+
39
+ export default postcssConfig
@@ -0,0 +1,81 @@
1
+ import type { NextRequest } from "next/server"
2
+ import { NextResponse } from "next/server"
3
+ import {
4
+ acceptHeaderRoutes,
5
+ ignoredPaths,
6
+ mdExtensionRoutes,
7
+ } from "@/lib/integrations/sanity/markdown-proxy.config"
8
+
9
+ export function proxy(request: NextRequest) {
10
+ const { pathname } = request.nextUrl
11
+ const acceptHeader = request.headers.get("accept") || ""
12
+
13
+ if (ignoredPaths.some((p) => pathname.startsWith(p))) {
14
+ return NextResponse.next()
15
+ }
16
+
17
+ if (pathname === "/" && acceptHeader.includes("text/markdown")) {
18
+ const url = request.nextUrl.clone()
19
+ url.pathname = "/api/index.md"
20
+ return NextResponse.rewrite(url)
21
+ }
22
+
23
+ // 1. Rewrite .md extension URLs to API routes
24
+ // e.g. /about.md → /api/about.md
25
+ for (const route of mdExtensionRoutes) {
26
+ const match = route.regex.exec(pathname)
27
+ if (match) {
28
+ const slug = match[1]
29
+ const url = request.nextUrl.clone()
30
+ url.pathname = route.apiPath.replace("[slug]", slug!)
31
+ return NextResponse.rewrite(url)
32
+ }
33
+ }
34
+
35
+ // 2. Accept: text/markdown — rewrite to markdown API route
36
+ if (acceptHeader.includes("text/markdown")) {
37
+ for (const route of acceptHeaderRoutes) {
38
+ const match = route.regex.exec(pathname)
39
+ if (match) {
40
+ const slug = match[1]
41
+ const url = request.nextUrl.clone()
42
+ url.pathname = route.apiPath.replace("[slug]", slug!)
43
+ return NextResponse.rewrite(url)
44
+ }
45
+ }
46
+ }
47
+
48
+ // 3. Add Link header for markdown discoverability on HTML page requests
49
+ const response = NextResponse.next()
50
+
51
+ if (pathname === "/") {
52
+ response.headers.set(
53
+ "Link",
54
+ '</index.md>; rel="alternate"; type="text/markdown"'
55
+ )
56
+ response.headers.set("Vary", "Accept")
57
+ return response
58
+ }
59
+
60
+ for (const route of acceptHeaderRoutes) {
61
+ const match = route.regex.exec(pathname)
62
+ if (match) {
63
+ const slug = match[1]
64
+ const mdUrl = route.publicPath.replace("[slug]", slug!)
65
+ response.headers.set(
66
+ "Link",
67
+ `<${mdUrl}>; rel="alternate"; type="text/markdown"`
68
+ )
69
+ response.headers.set("Vary", "Accept")
70
+ break
71
+ }
72
+ }
73
+
74
+ return response
75
+ }
76
+
77
+ export const config = {
78
+ matcher: [
79
+ "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|sitemap.md|robots.txt).*)",
80
+ ],
81
+ }
@@ -0,0 +1,5 @@
1
+ declare module "*.svg" {
2
+ import type { FC, SVGProps } from "react"
3
+ const content: FC<SVGProps<SVGSVGElement>>
4
+ export default content
5
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "noImplicitOverride": true,
17
+ "exactOptionalPropertyTypes": true,
18
+ "useUnknownInCatchVariables": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noImplicitReturns": true,
21
+ "noUncheckedSideEffectImports": true,
22
+ "noUnusedLocals": true,
23
+ "noUnusedParameters": true,
24
+ "noUncheckedIndexedAccess": true,
25
+ "plugins": [{ "name": "next" }],
26
+ "types": ["bun", "node"],
27
+ "paths": { "@/*": ["./*"] }
28
+ },
29
+ "include": [
30
+ "next-env.d.ts",
31
+ "**/*.ts",
32
+ "**/*.tsx",
33
+ ".next/types/**/*.ts",
34
+ ".next/dev/types/**/*.ts",
35
+ "**/*.mts"
36
+ ],
37
+ "exclude": ["node_modules"]
38
+ }
@@ -6,6 +6,11 @@ import { fontsVariable } from "@/lib/styles/fonts";
6
6
  import AppData from "@/package.json";
7
7
  import "@/lib/styles/index.css";
8
8
  import { cn } from "@/lib/styles/cn";
9
+ import {
10
+ JsonLd,
11
+ generateWebSiteJsonLd,
12
+ generateOrganizationJsonLd,
13
+ } from "@/lib/utils/json-ld";
9
14
 
10
15
  const APP_NAME = AppData.name;
11
16
  const APP_DEFAULT_TITLE = "Basement Starter";
@@ -86,6 +91,19 @@ export default async function Layout({ children }: PropsWithChildren) {
86
91
  suppressHydrationWarning
87
92
  >
88
93
  <body>
94
+ <JsonLd
95
+ data={generateWebSiteJsonLd({
96
+ name: APP_DEFAULT_TITLE,
97
+ description: APP_DESCRIPTION,
98
+ })}
99
+ />
100
+ <JsonLd
101
+ data={generateOrganizationJsonLd({
102
+ name: APP_NAME,
103
+ logo: "/opengraph-image.jpg",
104
+ })}
105
+ />
106
+
89
107
  {/* Skip link for keyboard navigation accessibility */}
90
108
  <Suspense fallback={null}>
91
109
  <Link
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from "react"
2
- import { useMedia } from "react-use"
2
+ import { useMedia } from "./use-media"
3
3
 
4
4
  const MOBILE_BREAKPOINT = 640
5
5
 
@@ -1,4 +1,4 @@
1
- import { useMedia } from "react-use"
1
+ import { useMedia } from "./use-media"
2
2
 
3
3
  const BREAKPOINTS = {
4
4
  "desktop-large": 1920,
@@ -0,0 +1,29 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ export function useMedia(mediaQuery: string, initialValue?: boolean) {
4
+ const [isVerified, setIsVerified] = useState<boolean | undefined>(initialValue)
5
+
6
+ useEffect(() => {
7
+ if (typeof window === "undefined" || !("matchMedia" in window)) {
8
+ console.warn("matchMedia is not supported by your current browser")
9
+ return
10
+ }
11
+ const mediaQueryList = window.matchMedia(mediaQuery)
12
+ const changeHandler = () => setIsVerified(!!mediaQueryList.matches)
13
+
14
+ changeHandler()
15
+ if (typeof mediaQueryList.addEventListener === "function") {
16
+ mediaQueryList.addEventListener("change", changeHandler)
17
+ return () => {
18
+ mediaQueryList.removeEventListener("change", changeHandler)
19
+ }
20
+ } else if (typeof mediaQueryList.addListener === "function") {
21
+ mediaQueryList.addListener(changeHandler)
22
+ return () => {
23
+ mediaQueryList.removeListener(changeHandler)
24
+ }
25
+ }
26
+ }, [mediaQuery])
27
+
28
+ return isVerified
29
+ }
@@ -0,0 +1,199 @@
1
+ import type {
2
+ Article,
3
+ BreadcrumbList,
4
+ Organization,
5
+ SearchAction,
6
+ Thing,
7
+ WebPage,
8
+ WebSite,
9
+ WithContext,
10
+ } from "schema-dts";
11
+
12
+ const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
13
+
14
+ function isAbsoluteUrl(value: string) {
15
+ return /^https?:\/\//.test(value);
16
+ }
17
+
18
+ function resolveUrl(value?: string) {
19
+ if (!value) return APP_BASE_URL;
20
+ if (isAbsoluteUrl(value)) return value;
21
+ if (!APP_BASE_URL) return undefined;
22
+
23
+ return new URL(value, APP_BASE_URL).toString();
24
+ }
25
+
26
+ /* -------------------------------- Component ------------------------------- */
27
+
28
+ export function JsonLd<T extends Thing>({
29
+ data,
30
+ }: {
31
+ data: WithContext<T>;
32
+ }) {
33
+ return (
34
+ <script
35
+ type="application/ld+json"
36
+ dangerouslySetInnerHTML={{
37
+ __html: JSON.stringify(data).replace(/</g, "\\u003c"),
38
+ }}
39
+ />
40
+ );
41
+ }
42
+
43
+ /* -------------------------------- Generators ------------------------------ */
44
+
45
+ interface WebSiteJsonLdOptions {
46
+ name: string;
47
+ url?: string;
48
+ description?: string;
49
+ /** URL to site-wide search (e.g. "/search?q={search_term_string}") */
50
+ searchUrl?: string;
51
+ }
52
+
53
+ export function generateWebSiteJsonLd(
54
+ options: WebSiteJsonLdOptions
55
+ ): WithContext<WebSite> {
56
+ const { name, url, description, searchUrl } = options;
57
+ const resolvedUrl = resolveUrl(url);
58
+ const resolvedSearchUrl = resolveUrl(searchUrl);
59
+
60
+ return {
61
+ "@context": "https://schema.org",
62
+ "@type": "WebSite",
63
+ name,
64
+ ...(resolvedUrl && { url: resolvedUrl }),
65
+ ...(description && { description }),
66
+ ...(resolvedSearchUrl && {
67
+ potentialAction: {
68
+ "@type": "SearchAction",
69
+ target: resolvedSearchUrl,
70
+ "query-input": "required name=search_term_string",
71
+ } as SearchAction & { "query-input": string },
72
+ }),
73
+ };
74
+ }
75
+
76
+ interface OrganizationJsonLdOptions {
77
+ name: string;
78
+ url?: string;
79
+ logo?: string;
80
+ description?: string;
81
+ sameAs?: string[];
82
+ }
83
+
84
+ export function generateOrganizationJsonLd(
85
+ options: OrganizationJsonLdOptions
86
+ ): WithContext<Organization> {
87
+ const { name, url, logo, description, sameAs } = options;
88
+ const resolvedUrl = resolveUrl(url);
89
+ const resolvedLogo = resolveUrl(logo);
90
+
91
+ return {
92
+ "@context": "https://schema.org",
93
+ "@type": "Organization",
94
+ name,
95
+ ...(resolvedUrl && { url: resolvedUrl }),
96
+ ...(resolvedLogo && { logo: resolvedLogo }),
97
+ ...(description && { description }),
98
+ ...(sameAs && { sameAs }),
99
+ };
100
+ }
101
+
102
+ interface WebPageJsonLdOptions {
103
+ title: string;
104
+ url?: string;
105
+ description?: string;
106
+ image?: string;
107
+ datePublished?: string;
108
+ dateModified?: string;
109
+ }
110
+
111
+ export function generateWebPageJsonLd(
112
+ options: WebPageJsonLdOptions
113
+ ): WithContext<WebPage> {
114
+ const { title, url, description, image, datePublished, dateModified } =
115
+ options;
116
+ const resolvedUrl = resolveUrl(url);
117
+ const resolvedImage = resolveUrl(image);
118
+
119
+ return {
120
+ "@context": "https://schema.org",
121
+ "@type": "WebPage",
122
+ name: title,
123
+ ...(resolvedUrl && { url: resolvedUrl }),
124
+ ...(description && { description }),
125
+ ...(resolvedImage && { image: resolvedImage }),
126
+ ...(datePublished && { datePublished }),
127
+ ...(dateModified && { dateModified }),
128
+ };
129
+ }
130
+
131
+ interface ArticleJsonLdOptions {
132
+ title: string;
133
+ url?: string;
134
+ description?: string;
135
+ image?: string;
136
+ datePublished?: string;
137
+ dateModified?: string;
138
+ authorName?: string;
139
+ authorUrl?: string;
140
+ }
141
+
142
+ export function generateArticleJsonLd(
143
+ options: ArticleJsonLdOptions
144
+ ): WithContext<Article> {
145
+ const {
146
+ title,
147
+ url,
148
+ description,
149
+ image,
150
+ datePublished,
151
+ dateModified,
152
+ authorName,
153
+ authorUrl,
154
+ } = options;
155
+ const resolvedUrl = resolveUrl(url);
156
+ const resolvedImage = resolveUrl(image);
157
+
158
+ return {
159
+ "@context": "https://schema.org",
160
+ "@type": "Article",
161
+ headline: title,
162
+ ...(resolvedUrl && { url: resolvedUrl }),
163
+ ...(description && { description }),
164
+ ...(resolvedImage && { image: resolvedImage }),
165
+ ...(datePublished && { datePublished }),
166
+ ...(dateModified && { dateModified }),
167
+ ...(authorName && {
168
+ author: {
169
+ "@type": "Person",
170
+ name: authorName,
171
+ ...(authorUrl && { url: authorUrl }),
172
+ },
173
+ }),
174
+ };
175
+ }
176
+
177
+ interface BreadcrumbItem {
178
+ name: string;
179
+ url: string;
180
+ }
181
+
182
+ export function generateBreadcrumbJsonLd(
183
+ items: BreadcrumbItem[]
184
+ ): WithContext<BreadcrumbList> {
185
+ return {
186
+ "@context": "https://schema.org",
187
+ "@type": "BreadcrumbList",
188
+ itemListElement: items.map((item, index) => {
189
+ const resolvedItemUrl = resolveUrl(item.url);
190
+
191
+ return {
192
+ "@type": "ListItem",
193
+ position: index + 1,
194
+ name: item.name,
195
+ ...(resolvedItemUrl && { item: resolvedItemUrl }),
196
+ };
197
+ }),
198
+ };
199
+ }
@@ -28,7 +28,6 @@
28
28
  "next": "^16",
29
29
  "react": "^19",
30
30
  "react-dom": "^19",
31
- "react-use": "^17.6.0",
32
31
  "tailwind-merge": "^3.4.0",
33
32
  "three": "^0.182.0",
34
33
  "zod": "^4.3.6"
@@ -47,6 +46,7 @@
47
46
  "babel-plugin-react-compiler": "1.0.0",
48
47
  "cross-env": "^10.1.0",
49
48
  "postcss-preset-env": "^10.6.1",
49
+ "schema-dts": "^2.0.0",
50
50
  "tailwindcss": "^4",
51
51
  "typescript": "^5"
52
52
  },
@@ -2,6 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2023",
4
4
  "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "types": ["bun"],
5
6
  "allowJs": true,
6
7
  "skipLibCheck": true,
7
8
  "strict": true,
@@ -1,11 +0,0 @@
1
- language js
2
-
3
- `<a $attrs>$content</a>` as $anchor where {
4
- ! $anchor <: within `if ($condition) { return ($jsx) }` where {
5
- $condition <: contains or {
6
- `isExternal`,
7
- `isExternalSSR`
8
- }
9
- } ,
10
- register_diagnostic(span=$anchor, message="Use custom Link component instead of <a> element. The Link component handles both internal and external links automatically.", severity="error")
11
- }
@@ -1,6 +0,0 @@
1
- language js
2
-
3
- `import $imports from $source` as $import where {
4
- $source <: r"['\"]\.\.\/\.\.\/.*['\"]",
5
- register_diagnostic(span=$import, message="Use alias imports (~/dir/) instead of deep relative imports (../../). Single level imports (../) are allowed for colocated files.", severity="error")
6
- }
@@ -1,5 +0,0 @@
1
- language js
2
-
3
- `forwardRef($func)` as $ref where {
4
- register_diagnostic(span=$ref, message="forwardRef is unnecessary in React 19 with the compiler", severity="warning")
5
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Cross-platform Sanity MCP setup helper
3
- * Copies the Cursor deeplink to clipboard on macOS/Linux/Windows
4
- */
5
-
6
- import { copyToClipboard } from "./utils";
7
-
8
- const link =
9
- "cursor://anysphere.cursor-deeplink/mcp/install?name=Sanity&config=eyJ1cmwiOiJodHRwczovL21jcC5zYW5pdHkuaW8iLCJ0eXBlIjoiaHR0cCJ9Cg==";
10
-
11
- const copied = await copyToClipboard(link);
12
-
13
- console.log("\nšŸ”— Sanity MCP Setup for Cursor\n");
14
-
15
- if (copied) {
16
- console.log("āœ… Link copied to clipboard!");
17
- console.log(" Paste it in your browser or Cursor address bar.\n");
18
- } else {
19
- console.log("šŸ“‹ Copy this link manually:\n");
20
- console.log(` ${link}\n`);
21
- }
22
-
23
- console.log("This will install the Sanity MCP server in Cursor.");