Skip to content

Commit cb3c343

Browse files
feat(guides): add markdown-driven Guides section with two featured guides
- New /guides listing (featured slider + category tabs) and /guides/[slug] reader - Content folder content/guides/*.md, mirroring the existing blog system - Seed two featured guides: cracking tech roles & better habits/schedule - Custom minimal SVG illustrations for the referenced visuals - Add standalone Guides link to navbar (desktop + mobile) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 14cef00 commit cb3c343

12 files changed

Lines changed: 1107 additions & 1 deletion

app/guides/[slug]/page.tsx

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import matter from "gray-matter";
4+
import ReactMarkdown from "react-markdown";
5+
import remarkGfm from "remark-gfm";
6+
import Link from "next/link";
7+
import type { Metadata } from "next";
8+
9+
const guidesDir = path.join(process.cwd(), "content", "guides");
10+
11+
function readGuide(slug: string) {
12+
const filePath = path.join(guidesDir, `${slug}.md`);
13+
if (!fs.existsSync(filePath)) return null;
14+
const fileContent = fs.readFileSync(filePath, "utf-8");
15+
return matter(fileContent);
16+
}
17+
18+
export function generateStaticParams() {
19+
if (!fs.existsSync(guidesDir)) return [];
20+
return fs
21+
.readdirSync(guidesDir)
22+
.filter((f) => f.endsWith(".md"))
23+
.map((f) => ({ slug: f.replace(/\.md$/, "") }));
24+
}
25+
26+
export async function generateMetadata({
27+
params,
28+
}: {
29+
params: Promise<{ slug: string }>;
30+
}): Promise<Metadata> {
31+
const { slug } = await params;
32+
const guide = readGuide(slug);
33+
if (!guide) return { title: "Guide not found | Dev Weekends" };
34+
return {
35+
title: `${guide.data.title} | Dev Weekends Guides`,
36+
description: guide.data.description ?? "",
37+
};
38+
}
39+
40+
export default async function GuidePage({
41+
params,
42+
}: {
43+
params: Promise<{ slug: string }>;
44+
}) {
45+
const { slug } = await params;
46+
const guide = readGuide(slug);
47+
48+
if (!guide) {
49+
return (
50+
<div className="flex min-h-screen items-center justify-center bg-white text-black">
51+
<div className="text-center">
52+
<h1 className="mb-3 text-3xl">Guide not found</h1>
53+
<Link href="/guides" className="underline">
54+
Back to guides
55+
</Link>
56+
</div>
57+
</div>
58+
);
59+
}
60+
61+
const { data, content } = guide;
62+
63+
return (
64+
<div className="min-h-screen bg-white text-black">
65+
<div className="mx-auto max-w-5xl px-6 py-16">
66+
<Link
67+
href="/guides"
68+
className="text-sm text-gray-500 transition hover:text-black"
69+
>
70+
← Back to guides
71+
</Link>
72+
73+
{/* HEADER */}
74+
<header className="mb-12 mt-10 max-w-3xl">
75+
<p className="mb-4 text-xs uppercase tracking-[0.22em] text-[#16A34A]">
76+
{data.category}
77+
</p>
78+
79+
<h1 className="mb-6 font-serif text-4xl leading-tight md:text-5xl">
80+
{data.title}
81+
</h1>
82+
83+
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-500">
84+
{data.author && <span>{data.author}</span>}
85+
{data.author && data.readingTime && <span></span>}
86+
{data.readingTime && <span>{data.readingTime}</span>}
87+
{data.date && <span></span>}
88+
{data.date && <span>{data.date}</span>}
89+
</div>
90+
</header>
91+
92+
{/* COVER */}
93+
{data.image && (
94+
<div className="mb-14 overflow-hidden rounded-2xl border border-gray-100">
95+
{/* eslint-disable-next-line @next/next/no-img-element */}
96+
<img
97+
src={data.image}
98+
alt={data.title}
99+
className="h-auto w-full"
100+
/>
101+
</div>
102+
)}
103+
104+
{/* MARKDOWN */}
105+
<article className="mx-auto max-w-2xl">
106+
<div className="prose prose-lg prose-neutral max-w-none">
107+
<ReactMarkdown
108+
remarkPlugins={[remarkGfm]}
109+
components={{
110+
p({ node, children }) {
111+
const hasImg = node?.children?.some(
112+
(child: any) => child.tagName === "img"
113+
);
114+
if (hasImg) {
115+
return <div className="my-12">{children}</div>;
116+
}
117+
return (
118+
<p className="my-6 text-[1.075rem] leading-[1.85] text-gray-800">
119+
{children}
120+
</p>
121+
);
122+
},
123+
124+
img({ src = "", alt = "" }) {
125+
return (
126+
<span className="my-12 block overflow-hidden rounded-2xl border border-gray-100">
127+
{/* eslint-disable-next-line @next/next/no-img-element */}
128+
<img
129+
src={typeof src === "string" ? src : ""}
130+
alt={alt}
131+
className="h-auto w-full"
132+
loading="lazy"
133+
/>
134+
</span>
135+
);
136+
},
137+
138+
h1: ({ children }) => (
139+
<h1 className="mb-8 mt-16 font-serif text-4xl tracking-tight text-black md:text-5xl">
140+
{children}
141+
</h1>
142+
),
143+
h2: ({ children }) => (
144+
<h2 className="mb-6 mt-16 font-serif text-3xl tracking-tight text-black">
145+
{children}
146+
</h2>
147+
),
148+
h3: ({ children }) => (
149+
<h3 className="mb-3 mt-10 font-serif text-xl font-semibold text-black">
150+
{children}
151+
</h3>
152+
),
153+
ul: ({ children }) => (
154+
<ul className="my-6 list-disc space-y-2 pl-6 leading-[1.75] text-gray-800">
155+
{children}
156+
</ul>
157+
),
158+
ol: ({ children }) => (
159+
<ol className="my-6 list-decimal space-y-2 pl-6 leading-[1.75] text-gray-800">
160+
{children}
161+
</ol>
162+
),
163+
li: ({ children }) => <li className="pl-1">{children}</li>,
164+
blockquote: ({ children }) => (
165+
<blockquote className="my-8 rounded-r-lg border-l-2 border-[#22C55E] bg-[#22C55E]/5 py-2 pl-6 pr-4 italic text-gray-700">
166+
{children}
167+
</blockquote>
168+
),
169+
hr: () => <hr className="my-16 h-px border-0 bg-gray-200" />,
170+
strong: ({ children }) => (
171+
<strong className="font-semibold text-black">
172+
{children}
173+
</strong>
174+
),
175+
a: ({ href, children }) => (
176+
<a
177+
href={href}
178+
target={href?.startsWith("http") ? "_blank" : undefined}
179+
rel={
180+
href?.startsWith("http")
181+
? "noopener noreferrer"
182+
: undefined
183+
}
184+
className="text-black underline decoration-gray-300 underline-offset-4 transition hover:decoration-black"
185+
>
186+
{children}
187+
</a>
188+
),
189+
}}
190+
>
191+
{content}
192+
</ReactMarkdown>
193+
</div>
194+
</article>
195+
196+
{/* FOOTER CTA */}
197+
<div className="mx-auto mt-20 max-w-2xl border-t border-gray-200 pt-10">
198+
<Link
199+
href="/guides"
200+
className="text-sm font-medium text-gray-500 transition hover:text-black"
201+
>
202+
← Explore more guides
203+
</Link>
204+
</div>
205+
</div>
206+
</div>
207+
);
208+
}

0 commit comments

Comments
 (0)