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,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
+ }
@@ -26,7 +26,6 @@
26
26
  "next": "^16",
27
27
  "react": "^19",
28
28
  "react-dom": "^19",
29
- "react-use": "^17.6.0",
30
29
  "tailwind-merge": "^3.4.0",
31
30
  "zod": "^4.3.6"
32
31
  },
@@ -43,6 +42,7 @@
43
42
  "babel-plugin-react-compiler": "1.0.0",
44
43
  "cross-env": "^10.1.0",
45
44
  "postcss-preset-env": "^10.6.1",
45
+ "schema-dts": "^2.0.0",
46
46
  "tailwindcss": "^4",
47
47
  "typescript": "^5"
48
48
  },
@@ -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,
@@ -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"
@@ -46,6 +45,7 @@
46
45
  "babel-plugin-react-compiler": "1.0.0",
47
46
  "cross-env": "^10.1.0",
48
47
  "postcss-preset-env": "^10.6.1",
48
+ "schema-dts": "^2.0.0",
49
49
  "tailwindcss": "^4",
50
50
  "typescript": "^5"
51
51
  },
@@ -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,
@@ -0,0 +1,11 @@
1
+ # Base URL
2
+ NEXT_PUBLIC_BASE_URL=http://localhost:3000
3
+
4
+ # Draft Mode
5
+ DRAFT_MODE_TOKEN=
6
+
7
+ # Sanity CMS
8
+ NEXT_PUBLIC_SANITY_PROJECT_ID=
9
+ NEXT_PUBLIC_SANITY_DATASET=production
10
+ SANITY_API_READ_TOKEN=
11
+ SANITY_REVALIDATE_SECRET=
@@ -0,0 +1,23 @@
1
+ # Page Builder Template
2
+
3
+ Next.js + Sanity CMS page builder architecture. All URL structure is managed in Sanity, not in code.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ bun dev
9
+ ```
10
+
11
+ ## Architecture
12
+
13
+ - Single catch-all route `[[...slug]]` handles all pages
14
+ - Sanity page builder with composable content blocks
15
+ - Blog with categories, filtering, and pagination
16
+ - Visual editing and live preview support
17
+
18
+ ## Commands
19
+
20
+ - `bun dev` - Start dev server
21
+ - `bun build` - Production build
22
+ - `bun lint` - Run linter
23
+ - `bun sanity:typegen` - Generate Sanity types
@@ -0,0 +1,67 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .turbo
8
+ .npmrc
9
+ .yarn/*
10
+ !.yarn/patches
11
+ !.yarn/plugins
12
+ !.yarn/releases
13
+ !.yarn/versions
14
+
15
+ # testing
16
+ /coverage
17
+
18
+ # next.js
19
+ /.next/
20
+ /out/
21
+
22
+ # TypeScript
23
+ *.tsbuildinfo
24
+
25
+ # production
26
+ /build
27
+ /public/sw.js
28
+
29
+ # misc
30
+ .DS_Store
31
+ *.pem
32
+ .cursor/scratchpad.md
33
+
34
+ # debug
35
+ npm-debug.log*
36
+ yarn-debug.log*
37
+ yarn-error.log*
38
+
39
+ # local env files
40
+ .env
41
+ .env.local
42
+ .env*.local
43
+ .env.development.local
44
+ .env.test.local
45
+ .env.production.local
46
+
47
+ # vercel
48
+ .vercel
49
+
50
+ # tldr
51
+ .tldr
52
+
53
+ # eslint
54
+ .eslintcache
55
+
56
+ # certificates
57
+ certificates
58
+
59
+ next-env.d.ts
60
+ # claude
61
+ .claude
62
+
63
+ # unlighthouse
64
+ .unlighthouse/
65
+
66
+ # storybook
67
+ *storybook.log