create-audora-next 0.1.7 → 2.0.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 (61) hide show
  1. package/README.md +15 -5
  2. package/index.ts +9 -4
  3. package/package.json +6 -1
  4. package/templates/blog/README.md +164 -0
  5. package/templates/blog/bun.lock +1341 -0
  6. package/templates/blog/env.example.template +5 -0
  7. package/templates/blog/eslint.config.mjs +18 -0
  8. package/templates/blog/gitignore.template +41 -0
  9. package/templates/blog/husky.template/pre-commit +16 -0
  10. package/templates/blog/lint-staged.config.mjs +17 -0
  11. package/templates/blog/next.config.ts +38 -0
  12. package/templates/blog/package.json +59 -0
  13. package/templates/blog/postcss.config.mjs +7 -0
  14. package/templates/blog/public/favicon/apple-touch-icon.png +0 -0
  15. package/templates/blog/public/favicon/favicon-96x96.png +0 -0
  16. package/templates/blog/public/favicon/favicon.ico +0 -0
  17. package/templates/blog/public/favicon/favicon.svg +1 -0
  18. package/templates/blog/public/favicon/site.webmanifest +21 -0
  19. package/templates/blog/public/favicon/web-app-manifest-192x192.png +0 -0
  20. package/templates/blog/public/favicon/web-app-manifest-512x512.png +0 -0
  21. package/templates/blog/public/images/screenshot-desktop-dark.webp +0 -0
  22. package/templates/blog/public/images/screenshot-desktop-light.webp +0 -0
  23. package/templates/blog/public/images/screenshot-mobile-dark.webp +0 -0
  24. package/templates/blog/public/images/screenshot-mobile-light.webp +0 -0
  25. package/templates/blog/src/app/blogs/[slug]/page.tsx +171 -0
  26. package/templates/blog/src/app/blogs/page.tsx +108 -0
  27. package/templates/blog/src/app/layout.tsx +60 -0
  28. package/templates/blog/src/app/llms-full.txt/route.ts +97 -0
  29. package/templates/blog/src/app/llms.txt/route.ts +40 -0
  30. package/templates/blog/src/app/manifest.ts +61 -0
  31. package/templates/blog/src/app/page.tsx +57 -0
  32. package/templates/blog/src/app/robots.ts +16 -0
  33. package/templates/blog/src/app/sitemap.ts +52 -0
  34. package/templates/blog/src/blogs/components/animated-blog-list.tsx +33 -0
  35. package/templates/blog/src/blogs/components/blog-post-card.tsx +46 -0
  36. package/templates/blog/src/blogs/components/blog-section.tsx +34 -0
  37. package/templates/blog/src/blogs/components/blog-table-of-contents.tsx +369 -0
  38. package/templates/blog/src/blogs/components/copy-button.tsx +46 -0
  39. package/templates/blog/src/blogs/components/mdx.tsx +225 -0
  40. package/templates/blog/src/blogs/content/cosketch/cosketch-canvas-engine.mdx +186 -0
  41. package/templates/blog/src/blogs/content/cosketch/cosketch-docker-architecture.mdx +175 -0
  42. package/templates/blog/src/blogs/content/cosketch/cosketch-eraser-and-selection.mdx +207 -0
  43. package/templates/blog/src/blogs/content/hello-world.mdx +66 -0
  44. package/templates/blog/src/blogs/data/mdx.ts +68 -0
  45. package/templates/blog/src/blogs/utils/extract-headings.ts +38 -0
  46. package/templates/blog/src/components/copyable-code.tsx +41 -0
  47. package/templates/blog/src/components/footer.tsx +25 -0
  48. package/templates/blog/src/components/header.tsx +27 -0
  49. package/templates/blog/src/components/icons.tsx +84 -0
  50. package/templates/blog/src/components/section-heading.tsx +11 -0
  51. package/templates/blog/src/components/theme-provider.tsx +11 -0
  52. package/templates/blog/src/components/theme-toggle.tsx +20 -0
  53. package/templates/blog/src/components/view-all-link.tsx +56 -0
  54. package/templates/blog/src/config/site.ts +19 -0
  55. package/templates/blog/src/data/llms.ts +112 -0
  56. package/templates/blog/src/data/site.ts +52 -0
  57. package/templates/blog/src/lib/seo.ts +190 -0
  58. package/templates/blog/src/lib/utils.ts +83 -0
  59. package/templates/blog/src/styles/globals.css +99 -0
  60. package/templates/blog/src/utils/cn.ts +7 -0
  61. package/templates/blog/tsconfig.json +34 -0
@@ -0,0 +1,5 @@
1
+ # Environment Variables
2
+ # Copy this file to .env and fill in the values
3
+
4
+ # Site URL (used for SEO and sitemap generation)
5
+ NEXT_PUBLIC_SITE_URL=http://localhost:3000
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,41 @@
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
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env sh
2
+ GREEN='\033[0;32m'
3
+ RED='\033[0;31m'
4
+ YELLOW='\033[0;33m'
5
+ NC='\033[0m' # No Color
6
+
7
+
8
+ echo "${YELLOW} [Husky] starting pre-commit hooks...${NC}"
9
+ echo "${YELLOW} [Husky] running lint-staged...${NC}"
10
+
11
+ if bun lint-staged; then
12
+ echo "${GREEN} [Husky] pre-commit hooks passed.${NC}"
13
+ else
14
+ echo "${RED} [Husky] pre-commit hooks failed.${NC}"
15
+ exit 1
16
+ fi
@@ -0,0 +1,17 @@
1
+ import path from "path";
2
+
3
+ // See https://nextjs.org/docs/app/api-reference/config/eslint#running-lint-on-staged-files for details
4
+ const buildEslintCommand = (filenames) =>
5
+ `eslint --fix ${filenames
6
+ .map((f) => `"${path.relative(process.cwd(), f)}"`)
7
+ .join(" ")}`;
8
+
9
+ /**
10
+ * @type {import('lint-staged').Configuration}
11
+ */
12
+ const lintStagedConfig = {
13
+ "*.{js,jsx,ts,tsx}": [buildEslintCommand, "prettier --write"],
14
+ "*.mdx": "prettier --write",
15
+ };
16
+
17
+ export default lintStagedConfig;
@@ -0,0 +1,38 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ reactCompiler: true,
5
+
6
+ async headers() {
7
+ return [
8
+ {
9
+ source: "/(.*)",
10
+ headers: [
11
+ {
12
+ key: "X-Content-Type-Options",
13
+ value: "nosniff",
14
+ },
15
+ {
16
+ key: "X-Frame-Options",
17
+ value: "DENY",
18
+ },
19
+ {
20
+ key: "X-XSS-Protection",
21
+ value: "1; mode=block",
22
+ },
23
+ {
24
+ key: "Referrer-Policy",
25
+ value: "strict-origin-when-cross-origin",
26
+ },
27
+ {
28
+ key: "Permissions-Policy",
29
+ value:
30
+ "camera=(), microphone=(), geolocation=(), browsing-topics=()",
31
+ },
32
+ ],
33
+ },
34
+ ];
35
+ },
36
+ };
37
+
38
+ export default nextConfig;
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "next-starter",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build --turbopack",
8
+ "start": "next start",
9
+ "lint": "eslint",
10
+ "lint:fix": "eslint --fix .",
11
+ "check-types": "tsc --noEmit --pretty",
12
+ "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
13
+ "format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
14
+ "format": "prettier --write .",
15
+ "clean": "rm -rf .next node_modules",
16
+ "upgrade:next": "bunx @next/codemod@latest upgrade latest",
17
+ "upgrade:tailwind": "bunx @tailwindcss/upgrade@latest",
18
+ "prepare": "git rev-parse --is-inside-work-tree > /dev/null 2>&1 && husky || true"
19
+ },
20
+ "dependencies": {
21
+ "clsx": "^2.1.1",
22
+ "gray-matter": "^4.0.3",
23
+ "lucide-react": "^0.563.0",
24
+ "motion": "^12.30.0",
25
+ "next": "16.1.1",
26
+ "next-mdx-remote": "^5.0.0",
27
+ "next-themes": "^0.4.6",
28
+ "react": "19.2.3",
29
+ "react-dom": "19.2.3",
30
+ "rehype-autolink-headings": "^7.1.0",
31
+ "rehype-pretty-code": "^0.14.1",
32
+ "rehype-slug": "^6.0.0",
33
+ "tailwind-merge": "^3.4.0"
34
+ },
35
+ "devDependencies": {
36
+ "@tailwindcss/postcss": "^4.1.18",
37
+ "@types/node": "^20.19.28",
38
+ "@types/react": "^19.2.8",
39
+ "@types/react-dom": "^19.2.3",
40
+ "babel-plugin-react-compiler": "1.0.0",
41
+ "eslint": "^9.39.2",
42
+ "eslint-config-next": "16.1.1",
43
+ "husky": "^9.1.7",
44
+ "lint-staged": "^16.2.7",
45
+ "prettier": "^3.7.4",
46
+ "prettier-plugin-tailwindcss": "^0.7.2",
47
+ "schema-dts": "^1.1.5",
48
+ "tailwindcss": "^4.1.18",
49
+ "typescript": "^5.9.3"
50
+ },
51
+ "ignoreScripts": [
52
+ "sharp",
53
+ "unrs-resolver"
54
+ ],
55
+ "trustedDependencies": [
56
+ "sharp",
57
+ "unrs-resolver"
58
+ ]
59
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1017)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(2.5,0,0,2.5,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400" viewBox="0 0 400 400"><image width="400" height="400" xlink:href="data:image/jpeg;base64,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"></image></svg></g></g><defs><clipPath id="SvgjsClipPath1017"><rect width="1000" height="1000" x="0" y="0" rx="350" ry="350"></rect></clipPath></defs></svg>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "Audora",
3
+ "short_name": "Audora",
4
+ "icons": [
5
+ {
6
+ "src": "/favicons/web-app-manifest-192x192.png",
7
+ "sizes": "192x192",
8
+ "type": "image/png",
9
+ "purpose": "maskable"
10
+ },
11
+ {
12
+ "src": "/favicons/web-app-manifest-512x512.png",
13
+ "sizes": "512x512",
14
+ "type": "image/png",
15
+ "purpose": "maskable"
16
+ }
17
+ ],
18
+ "theme_color": "#000000",
19
+ "background_color": "#000000",
20
+ "display": "standalone"
21
+ }
@@ -0,0 +1,171 @@
1
+ import { notFound } from "next/navigation";
2
+ import { CustomMDX } from "@/blogs/components/mdx";
3
+ import { getBlogPost, getBlogPosts } from "@/blogs/data/mdx";
4
+ import { extractHeadings } from "@/blogs/utils/extract-headings";
5
+ import { BlogTableOfContents } from "@/blogs/components/blog-table-of-contents";
6
+ import { formatDateDisplay } from "@/lib/utils";
7
+ import { SITE_CONFIG } from "@/config/site";
8
+ import Link from "next/link";
9
+ import { ArrowLeft, Calendar, ExternalLink, Github } from "lucide-react";
10
+
11
+ const siteUrl = SITE_CONFIG.url.replace(/\/$/, "");
12
+
13
+ function resolveOgImage(pathOrUrl: string): string {
14
+ return pathOrUrl.startsWith("http")
15
+ ? pathOrUrl
16
+ : new URL(pathOrUrl, siteUrl).href;
17
+ }
18
+
19
+ export async function generateStaticParams() {
20
+ const posts = getBlogPosts();
21
+ return posts.map((post) => ({
22
+ slug: post.slug,
23
+ }));
24
+ }
25
+
26
+ export async function generateMetadata({
27
+ params,
28
+ }: {
29
+ params: Promise<{ slug: string }>;
30
+ }) {
31
+ const { slug } = await params;
32
+ const post = getBlogPost(slug);
33
+ if (!post) {
34
+ return;
35
+ }
36
+
37
+ const {
38
+ title,
39
+ publishedAt: publishedTime,
40
+ summary: description,
41
+ image,
42
+ } = post.metadata;
43
+ const ogImage = image
44
+ ? `${siteUrl}${image}`
45
+ : resolveOgImage(SITE_CONFIG.ogImage);
46
+
47
+ return {
48
+ title,
49
+ description,
50
+ openGraph: {
51
+ title,
52
+ description,
53
+ type: "article",
54
+ publishedTime,
55
+ url: `${siteUrl}/blogs/${post.slug}`,
56
+ images: [
57
+ {
58
+ url: ogImage,
59
+ },
60
+ ],
61
+ },
62
+ twitter: {
63
+ card: "summary_large_image",
64
+ title,
65
+ description,
66
+ images: [ogImage],
67
+ },
68
+ };
69
+ }
70
+
71
+ export default async function Blog({
72
+ params,
73
+ }: {
74
+ params: Promise<{ slug: string }>;
75
+ }) {
76
+ const { slug } = await params;
77
+ const post = getBlogPost(slug);
78
+
79
+ if (!post) {
80
+ notFound();
81
+ }
82
+
83
+ // Extract headings for table of contents
84
+ const headings = extractHeadings(post.content);
85
+
86
+ return (
87
+ <section className="container mx-auto max-w-(--content-max-width) px-4 py-16">
88
+ <script
89
+ type="application/ld+json"
90
+ suppressHydrationWarning
91
+ dangerouslySetInnerHTML={{
92
+ __html: JSON.stringify({
93
+ "@context": "https://schema.org",
94
+ "@type": "BlogPosting",
95
+ headline: post.metadata.title,
96
+ datePublished: post.metadata.publishedAt,
97
+ dateModified: post.metadata.publishedAt,
98
+ description: post.metadata.summary,
99
+ image: post.metadata.image
100
+ ? `${siteUrl}${post.metadata.image}`
101
+ : resolveOgImage(SITE_CONFIG.ogImage),
102
+ url: `${siteUrl}/blogs/${post.slug}`,
103
+ author: {
104
+ "@type": "Person",
105
+ name: "Narsi Bhati",
106
+ },
107
+ }),
108
+ }}
109
+ />
110
+
111
+ {/* Blog content with TOC positioned outside on xl+ */}
112
+ <div className="relative">
113
+ {/* Main content - uses original max width */}
114
+ <div className="max-w-(--content-max-width)">
115
+ <h1 className="title max-w-[650px] text-4xl font-bold tracking-tighter">
116
+ {post.metadata.title}
117
+ </h1>
118
+ <div className="mt-2 flex max-w-[650px] items-center justify-between text-sm">
119
+ <p className="flex items-center gap-1.5 text-sm text-neutral-600 dark:text-neutral-400">
120
+ <Calendar className="h-4 w-4 shrink-0" />
121
+ {formatDateDisplay(post.metadata.publishedAt)}
122
+ </p>
123
+ </div>
124
+ {post.metadata.summary && (
125
+ <div className="mt-4 mb-8 max-w-[650px]">
126
+ <p className="border-l-2 border-neutral-300 pl-4 text-base leading-relaxed text-neutral-500 italic dark:border-neutral-600 dark:text-neutral-400">
127
+ {post.metadata.summary}
128
+ </p>
129
+ </div>
130
+ )}
131
+ <article className="prose prose-quoteless prose-neutral dark:prose-invert">
132
+ <CustomMDX source={post.content} />
133
+ </article>
134
+ <div className="mt-8 flex flex-wrap items-center justify-between gap-4">
135
+ <Link
136
+ href="/blogs"
137
+ className="inline-flex items-center gap-1.5 text-sm text-neutral-500 transition-colors hover:text-neutral-900 dark:hover:text-neutral-100"
138
+ >
139
+ <ArrowLeft className="h-4 w-4" />
140
+ Back to blogs
141
+ </Link>
142
+ {post.metadata.repoUrl && (
143
+ <a
144
+ href={post.metadata.repoUrl}
145
+ target="_blank"
146
+ rel="noopener noreferrer"
147
+ className="inline-flex items-center gap-1.5 text-sm text-neutral-500 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
148
+ >
149
+ <Github className="h-4 w-4" />
150
+ View on GitHub
151
+ <ExternalLink className="h-3.5 w-3.5" />
152
+ </a>
153
+ )}
154
+ </div>
155
+ </div>
156
+
157
+ {/* Table of Contents - Sticky on desktop for easy navigation */}
158
+ <aside className="absolute top-0 left-[calc(100%+2rem)] hidden h-full w-52 xl:block">
159
+ <div className="sticky top-24">
160
+ <BlogTableOfContents headings={headings} />
161
+ </div>
162
+ </aside>
163
+ </div>
164
+
165
+ {/* Mobile TOC - Shows floating button */}
166
+ <div className="xl:hidden">
167
+ <BlogTableOfContents headings={headings} />
168
+ </div>
169
+ </section>
170
+ );
171
+ }
@@ -0,0 +1,108 @@
1
+ import Link from "next/link";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { getBlogPosts } from "@/blogs/data/mdx";
4
+ import SectionHeading from "@/components/section-heading";
5
+ import { BlogPostCard } from "@/blogs/components/blog-post-card";
6
+
7
+ const POSTS_PER_PAGE = 10;
8
+
9
+ export const metadata = {
10
+ title: "Blogs",
11
+ description: "Read my thoughts on software development, design, and more.",
12
+ };
13
+
14
+ export default async function BlogPage({
15
+ searchParams,
16
+ }: {
17
+ searchParams: Promise<{ page?: string }>;
18
+ }) {
19
+ const params = await searchParams;
20
+ const allBlogs = getBlogPosts();
21
+
22
+ const sorted = [...allBlogs].sort(
23
+ (a, b) =>
24
+ new Date(b.metadata.publishedAt).getTime() -
25
+ new Date(a.metadata.publishedAt).getTime(),
26
+ );
27
+
28
+ const totalPosts = sorted.length;
29
+ const totalPages = Math.max(1, Math.ceil(totalPosts / POSTS_PER_PAGE));
30
+ const rawPage = parseInt(params.page ?? "1", 10);
31
+ const page = Math.max(
32
+ 1,
33
+ Math.min(Number.isNaN(rawPage) ? 1 : rawPage, totalPages),
34
+ );
35
+
36
+ const start = (page - 1) * POSTS_PER_PAGE;
37
+ const posts = sorted.slice(start, start + POSTS_PER_PAGE);
38
+
39
+ return (
40
+ <section className="py-4">
41
+ <div className="container mx-auto mt-4 max-w-(--content-max-width) px-4">
42
+ <SectionHeading>Blogs</SectionHeading>
43
+ {totalPosts === 0 ? (
44
+ <p className="text-muted-foreground">No posts yet.</p>
45
+ ) : (
46
+ <>
47
+ <div className="flex flex-col gap-4">
48
+ {posts.map((post) => (
49
+ <BlogPostCard
50
+ key={post.slug}
51
+ slug={post.slug}
52
+ title={post.metadata.title}
53
+ publishedAt={post.metadata.publishedAt}
54
+ summary={post.metadata.summary}
55
+ />
56
+ ))}
57
+ </div>
58
+
59
+ {totalPages > 1 && (
60
+ <nav
61
+ className="mt-10 flex flex-wrap items-center justify-between gap-4 border-t border-border pt-6"
62
+ aria-label="Blog pagination"
63
+ >
64
+ <p className="text-sm text-muted-foreground">
65
+ Page{" "}
66
+ <span className="font-medium text-foreground">{page}</span> of{" "}
67
+ <span className="font-medium text-foreground">
68
+ {totalPages}
69
+ </span>
70
+ </p>
71
+ <div className="flex items-center gap-2">
72
+ {page > 1 ? (
73
+ <Link
74
+ href={page === 2 ? "/blogs" : `/blogs?page=${page - 1}`}
75
+ className="inline-flex items-center gap-1.5 rounded-md border border-border bg-muted/50 px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted"
76
+ >
77
+ <ChevronLeft className="h-4 w-4" />
78
+ Previous
79
+ </Link>
80
+ ) : (
81
+ <span className="inline-flex items-center gap-1.5 rounded-md border border-border/50 bg-muted/30 px-3 py-1.5 text-sm text-muted-foreground">
82
+ <ChevronLeft className="h-4 w-4" />
83
+ Previous
84
+ </span>
85
+ )}
86
+ {page < totalPages ? (
87
+ <Link
88
+ href={`/blogs?page=${page + 1}`}
89
+ className="inline-flex items-center gap-1.5 rounded-md border border-border bg-muted/50 px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted"
90
+ >
91
+ Next
92
+ <ChevronRight className="h-4 w-4" />
93
+ </Link>
94
+ ) : (
95
+ <span className="inline-flex items-center gap-1.5 rounded-md border border-border/50 bg-muted/30 px-3 py-1.5 text-sm text-muted-foreground">
96
+ Next
97
+ <ChevronRight className="h-4 w-4" />
98
+ </span>
99
+ )}
100
+ </div>
101
+ </nav>
102
+ )}
103
+ </>
104
+ )}
105
+ </div>
106
+ </section>
107
+ );
108
+ }
@@ -0,0 +1,60 @@
1
+ import type { Metadata, Viewport } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { ThemeProvider } from "@/components/theme-provider";
4
+ import { Header } from "@/components/header";
5
+ import { Footer } from "@/components/footer";
6
+ import {
7
+ getMetadata,
8
+ getViewport,
9
+ getWebSiteJsonLd,
10
+ getOrganizationJsonLd,
11
+ } from "@/lib/seo";
12
+ import "@/styles/globals.css";
13
+
14
+ const geistSans = Geist({
15
+ variable: "--font-geist-sans",
16
+ subsets: ["latin"],
17
+ });
18
+
19
+ const geistMono = Geist_Mono({
20
+ variable: "--font-geist-mono",
21
+ subsets: ["latin"],
22
+ });
23
+
24
+ export const metadata: Metadata = getMetadata();
25
+ export const viewport: Viewport = getViewport();
26
+
27
+ export default function RootLayout({
28
+ children,
29
+ }: Readonly<{
30
+ children: React.ReactNode;
31
+ }>) {
32
+ const jsonLd = [getWebSiteJsonLd(), getOrganizationJsonLd()];
33
+
34
+ return (
35
+ <html lang="en" suppressHydrationWarning>
36
+ <head>
37
+ <script
38
+ id="schema-ld"
39
+ type="application/ld+json"
40
+ dangerouslySetInnerHTML={{
41
+ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
42
+ }}
43
+ />
44
+ </head>
45
+ <body
46
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
47
+ >
48
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
49
+ <div className="relative flex min-h-screen flex-col">
50
+ <div className="mx-auto w-full max-w-5xl border-x border-border/50 min-h-screen flex flex-col bg-background">
51
+ <Header />
52
+ <main className="flex-1">{children}</main>
53
+ <Footer />
54
+ </div>
55
+ </div>
56
+ </ThemeProvider>
57
+ </body>
58
+ </html>
59
+ );
60
+ }