create-audora-next 0.1.7 → 2.0.1
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/README.md +25 -5
- package/assets/audora-blog.png +0 -0
- package/assets/audora-next.webp +0 -0
- package/index.ts +9 -4
- package/package.json +8 -2
- package/templates/blog/README.md +164 -0
- package/templates/blog/bun.lock +1341 -0
- package/templates/blog/env.example.template +5 -0
- package/templates/blog/eslint.config.mjs +18 -0
- package/templates/blog/gitignore.template +41 -0
- package/templates/blog/husky.template/pre-commit +16 -0
- package/templates/blog/lint-staged.config.mjs +17 -0
- package/templates/blog/next.config.ts +38 -0
- package/templates/blog/package.json +59 -0
- package/templates/blog/postcss.config.mjs +7 -0
- package/templates/blog/public/favicon/apple-touch-icon.png +0 -0
- package/templates/blog/public/favicon/favicon-96x96.png +0 -0
- package/templates/blog/public/favicon/favicon.ico +0 -0
- package/templates/blog/public/favicon/favicon.svg +1 -0
- package/templates/blog/public/favicon/site.webmanifest +21 -0
- package/templates/blog/public/favicon/web-app-manifest-192x192.png +0 -0
- package/templates/blog/public/favicon/web-app-manifest-512x512.png +0 -0
- package/templates/blog/public/images/screenshot-desktop-dark.webp +0 -0
- package/templates/blog/public/images/screenshot-desktop-light.webp +0 -0
- package/templates/blog/public/images/screenshot-mobile-dark.webp +0 -0
- package/templates/blog/public/images/screenshot-mobile-light.webp +0 -0
- package/templates/blog/src/app/blogs/[slug]/page.tsx +171 -0
- package/templates/blog/src/app/blogs/page.tsx +108 -0
- package/templates/blog/src/app/layout.tsx +60 -0
- package/templates/blog/src/app/llms-full.txt/route.ts +97 -0
- package/templates/blog/src/app/llms.txt/route.ts +40 -0
- package/templates/blog/src/app/manifest.ts +61 -0
- package/templates/blog/src/app/page.tsx +57 -0
- package/templates/blog/src/app/robots.ts +16 -0
- package/templates/blog/src/app/sitemap.ts +52 -0
- package/templates/blog/src/blogs/components/animated-blog-list.tsx +33 -0
- package/templates/blog/src/blogs/components/blog-post-card.tsx +46 -0
- package/templates/blog/src/blogs/components/blog-section.tsx +34 -0
- package/templates/blog/src/blogs/components/blog-table-of-contents.tsx +369 -0
- package/templates/blog/src/blogs/components/copy-button.tsx +46 -0
- package/templates/blog/src/blogs/components/mdx.tsx +225 -0
- package/templates/blog/src/blogs/content/cosketch/cosketch-canvas-engine.mdx +186 -0
- package/templates/blog/src/blogs/content/cosketch/cosketch-docker-architecture.mdx +175 -0
- package/templates/blog/src/blogs/content/cosketch/cosketch-eraser-and-selection.mdx +207 -0
- package/templates/blog/src/blogs/content/hello-world.mdx +66 -0
- package/templates/blog/src/blogs/data/mdx.ts +68 -0
- package/templates/blog/src/blogs/utils/extract-headings.ts +38 -0
- package/templates/blog/src/components/copyable-code.tsx +41 -0
- package/templates/blog/src/components/footer.tsx +25 -0
- package/templates/blog/src/components/header.tsx +27 -0
- package/templates/blog/src/components/icons.tsx +84 -0
- package/templates/blog/src/components/section-heading.tsx +11 -0
- package/templates/blog/src/components/theme-provider.tsx +11 -0
- package/templates/blog/src/components/theme-toggle.tsx +20 -0
- package/templates/blog/src/components/view-all-link.tsx +56 -0
- package/templates/blog/src/config/site.ts +19 -0
- package/templates/blog/src/data/llms.ts +112 -0
- package/templates/blog/src/data/site.ts +52 -0
- package/templates/blog/src/lib/seo.ts +190 -0
- package/templates/blog/src/lib/utils.ts +83 -0
- package/templates/blog/src/styles/globals.css +99 -0
- package/templates/blog/src/utils/cn.ts +7 -0
- package/templates/blog/tsconfig.json +34 -0
|
@@ -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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|