Skip to content

Commit a7b6fd8

Browse files
authored
Merge pull request #194 from 1chooo/feature/#188
Feature/#188
2 parents 193bd9e + 53cc845 commit a7b6fd8

File tree

12 files changed

+694
-24
lines changed

12 files changed

+694
-24
lines changed

apps/web/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
22

3+
.swc
4+
5+
36
.next
47

58
# dependencies

apps/web/next.config.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const nextConfig = {
2222
// https://nextjs.org/docs/pages/building-your-application/configuring/mdx
2323
// Configure `pageExtensions` to include markdown and MDX files
2424
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
25+
transpilePackages: ['next-mdx-remote'],
2526
}
2627

2728
export default nextConfig

apps/web/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"bootstrap": "^5.3.3",
1313
"caniuse-lite": "^1.0.30001653",
1414
"next": "^14.2.7",
15+
"next-mdx-remote": "^5.0.0",
1516
"react": "^18.2.0",
1617
"react-bootstrap": "^2.10.4",
1718
"react-dom": "^18.2.0",

apps/web/src/app/db/blog.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
4+
type Metadata = {
5+
title: string;
6+
publishedAt: string;
7+
summary: string;
8+
image?: string;
9+
};
10+
11+
function parseFrontmatter(fileContent: string) {
12+
let frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
13+
let match = frontmatterRegex.exec(fileContent);
14+
let frontMatterBlock = match![1];
15+
let content = fileContent.replace(frontmatterRegex, '').trim();
16+
let frontMatterLines = frontMatterBlock.trim().split('\n');
17+
let metadata: Partial<Metadata> = {};
18+
19+
frontMatterLines.forEach((line) => {
20+
let [key, ...valueArr] = line.split(': ');
21+
let value = valueArr.join(': ').trim();
22+
value = value.replace(/^['"](.*)['"]$/, '$1'); // Remove quotes
23+
metadata[key.trim() as keyof Metadata] = value;
24+
});
25+
26+
return { metadata: metadata as Metadata, content };
27+
}
28+
29+
function getMDXFiles(dir: string) {
30+
return fs.readdirSync(dir).filter((file) => path.extname(file) === '.mdx');
31+
}
32+
33+
function readMDXFile(filePath: string) {
34+
if (!filePath || !fs.existsSync(filePath) || path.extname(filePath) !== '.mdx') {
35+
throw new Error('Invalid file path or file does not exist');
36+
}
37+
let rawContent = fs.readFileSync(filePath, 'utf-8');
38+
return parseFrontmatter(rawContent);
39+
}
40+
41+
// function extractTweetIds(content) {
42+
// let tweetMatches = content.match(/<StaticTweet\sid="[0-9]+"\s\/>/g);
43+
// return tweetMatches?.map((tweet) => tweet.match(/[0-9]+/g)[0]) || [];
44+
// }
45+
46+
function getMDXData(dir: string) {
47+
let mdxFiles = getMDXFiles(dir);
48+
return mdxFiles.map((file) => {
49+
let { metadata, content } = readMDXFile(path.join(dir, file));
50+
let slug = path.basename(file, path.extname(file));
51+
// let tweetIds = extractTweetIds(content);
52+
return {
53+
metadata,
54+
slug,
55+
// tweetIds,
56+
content,
57+
};
58+
});
59+
}
60+
61+
export function getBlogPosts() {
62+
return getMDXData(path.join(process.cwd(), 'src/contents'));
63+
}

apps/web/src/app/post/[slug]/page.tsx

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { Metadata } from 'next';
2+
import { Suspense } from 'react';
3+
import { notFound } from 'next/navigation';
4+
// import { CustomMDX } from 'app/components/mdx';
5+
import MarkdownRenderer from '@/components/markdown/markdown-renderer';
6+
import { getBlogPosts } from '../../db/blog';
7+
import { unstable_noStore as noStore } from 'next/cache';
8+
import Header from '@/components/markdown/header';
9+
10+
export async function generateMetadata({
11+
params,
12+
}: {
13+
params: { slug: string };
14+
}): Promise<Metadata | undefined> {
15+
let post = getBlogPosts().find((post) => post.slug === params.slug);
16+
if (!post) {
17+
return;
18+
}
19+
20+
let {
21+
title,
22+
publishedAt: publishedTime,
23+
summary: description,
24+
image,
25+
} = post.metadata;
26+
let ogImage = image
27+
? `https://leerob.io${image}`
28+
: `https://leerob.io/og?title=${title}`;
29+
30+
return {
31+
title,
32+
description,
33+
openGraph: {
34+
title,
35+
description,
36+
type: 'article',
37+
publishedTime,
38+
url: `https://leerob.io/blog/${post.slug}`,
39+
images: [
40+
{
41+
url: ogImage,
42+
},
43+
],
44+
},
45+
twitter: {
46+
card: 'summary_large_image',
47+
title,
48+
description,
49+
images: [ogImage],
50+
},
51+
};
52+
}
53+
54+
function formatDate(date: string) {
55+
noStore();
56+
let currentDate = new Date().getTime();
57+
if (!date.includes('T')) {
58+
date = `${date}T00:00:00`;
59+
}
60+
let targetDate = new Date(date).getTime();
61+
let timeDifference = Math.abs(currentDate - targetDate);
62+
let daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
63+
64+
let fullDate = new Date(date).toLocaleString('en-us', {
65+
month: 'long',
66+
day: 'numeric',
67+
year: 'numeric',
68+
});
69+
70+
if (daysAgo < 1) {
71+
return 'Today';
72+
} else if (daysAgo < 7) {
73+
return `${fullDate} (${daysAgo}d ago)`;
74+
} else if (daysAgo < 30) {
75+
const weeksAgo = Math.floor(daysAgo / 7)
76+
return `${fullDate} (${weeksAgo}w ago)`;
77+
} else if (daysAgo < 365) {
78+
const monthsAgo = Math.floor(daysAgo / 30)
79+
return `${fullDate} (${monthsAgo}mo ago)`;
80+
} else {
81+
const yearsAgo = Math.floor(daysAgo / 365)
82+
return `${fullDate} (${yearsAgo}y ago)`;
83+
}
84+
}
85+
86+
export default function Blog({ params }: { params: { slug: string } }) {
87+
let post = getBlogPosts().find((post) => post.slug === params.slug);
88+
89+
if (!post) {
90+
notFound();
91+
}
92+
93+
return (
94+
<article>
95+
<section>
96+
{/* <script
97+
type="application/ld+json"
98+
suppressHydrationWarning
99+
dangerouslySetInnerHTML={{
100+
__html: JSON.stringify({
101+
'@context': 'https://schema.org',
102+
'@type': 'BlogPosting',
103+
headline: post.metadata.title,
104+
datePublished: post.metadata.publishedAt,
105+
dateModified: post.metadata.publishedAt,
106+
description: post.metadata.summary,
107+
image: post.metadata.image
108+
? `https://leerob.io${post.metadata.image}`
109+
: `https://leerob.io/og?title=${post.metadata.title}`,
110+
url: `https://leerob.io/blog/${post.slug}`,
111+
author: {
112+
'@type': 'Person',
113+
name: 'Lee Robinson',
114+
},
115+
}),
116+
}}
117+
/> */}
118+
<Header title="Hugo's Blog" />
119+
<h1 className="title font-medium text-2xl tracking-tighter max-w-[650px]">
120+
{post.metadata.title}
121+
</h1>
122+
<div className="flex justify-between items-center mt-2 mb-8 text-sm max-w-[650px]">
123+
<Suspense fallback={<p className="h-5" />}>
124+
<p className="text-sm text-neutral-600 dark:text-neutral-400">
125+
{formatDate(post.metadata.publishedAt)}
126+
</p>
127+
</Suspense>
128+
{/* <Suspense fallback={<p className="h-5" />}>
129+
<Views slug={post.slug} />
130+
</Suspense> */}
131+
</div>
132+
<div className="prose prose-quoteless prose-neutral dark:prose-invert">
133+
<MarkdownRenderer content={post.content} />
134+
</div>
135+
</section>
136+
</article>
137+
);
138+
}
139+
140+
// let incrementViews = cache(increment);
141+
142+
// async function Views({ slug }: { slug: string }) {
143+
// let views = await getViewsCount();
144+
// incrementViews(slug);
145+
// return <ViewCounter allViews={views} slug={slug} />;
146+
// }

apps/web/src/app/post/page.tsx

+44-23
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,51 @@
1-
'use client';
1+
import Link from 'next/link';
2+
import { getBlogPosts } from '../db/blog';
3+
import Header from '@/components/markdown/header';
24

3-
import React, { useEffect } from 'react';
4-
import { usePathname } from 'next/navigation';
5-
import { initializeCustomSelect, filterItemsByCategory } from '@/utils/dom-utils';
5+
export const metadata = {
6+
title: 'Blog',
7+
description: 'Read my thoughts on software development, design, and more.',
8+
};
69

7-
import BlogPosts from '@/components/posts/blog-post';
8-
import PageContent from '@/components/page-content';
9-
10-
11-
const Post = () => {
12-
const pathname = usePathname();
13-
14-
useEffect(() => {
15-
initializeCustomSelect(filterItemsByCategory);
16-
}, []);
10+
export default function BlogPage() {
11+
let allBlogs = getBlogPosts();
1712

1813
return (
19-
<PageContent
20-
documentTitle='Blog'
21-
title="Hugo's Blog"
22-
page="blog"
23-
pathName={pathname}
24-
>
25-
<BlogPosts />
26-
</PageContent >
14+
<article>
15+
<section>
16+
<Header title="Hugo's Blog" />
17+
{allBlogs
18+
.sort((a, b) => {
19+
if (
20+
new Date(a.metadata.publishedAt) > new Date(b.metadata.publishedAt)
21+
) {
22+
return -1;
23+
}
24+
return 1;
25+
})
26+
.map((post) => (
27+
<Link
28+
key={post.slug}
29+
className="flex flex-col space-y-1 mb-4"
30+
href={`/post/${post.slug}`}
31+
>
32+
<div className="w-full flex flex-col">
33+
<p className="text-neutral-900 dark:text-neutral-100 tracking-tight">
34+
{post.metadata.title}
35+
</p>
36+
{/* <Suspense fallback={<p className="h-6" />}>
37+
<Views slug={post.slug} />
38+
</Suspense> */}
39+
</div>
40+
</Link>
41+
))}
42+
</section>
43+
</article >
2744
);
2845
}
2946

30-
export default Post;
47+
// async function Views({ slug }: { slug: string }) {
48+
// let views = await getViewsCount();
49+
50+
// return <ViewCounter allViews={views} slug={slug} />;
51+
// }

0 commit comments

Comments
 (0)