mirror of
https://github.com/theoludwig/theoludwig.git
synced 2025-05-29 22:37:44 +02:00
perf!: monorepo setup + fully static + webp images
BREAKING CHANGE: minimum supported Node.js >= 22.0.0 and pnpm >= 9.5.0
This commit is contained in:
25
packages/blog/src/BlogPost.ts
Normal file
25
packages/blog/src/BlogPost.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export interface FrontMatter {
|
||||
title: string
|
||||
description: string
|
||||
isPublished: boolean
|
||||
publishedOn: string
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
frontmatter: FrontMatter
|
||||
slug: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export const BLOG_POST_MOCK = {
|
||||
slug: "hello-world",
|
||||
content:
|
||||
"\nHello, world! 👋\n\n## Introduction\n\nThis blog is here to document my journey of learning computer science.",
|
||||
frontmatter: {
|
||||
title: "👋 Hello, world!",
|
||||
description:
|
||||
"First post of the blog, introduction and explanation of how this blog is made.",
|
||||
isPublished: true,
|
||||
publishedOn: "2022-02-20T08:00:18.758Z",
|
||||
},
|
||||
} satisfies BlogPost
|
27
packages/blog/src/BlogPostComments.tsx
Normal file
27
packages/blog/src/BlogPostComments.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import Giscus from "@giscus/react"
|
||||
import { useTheme } from "@repo/ui/Header/SwitchTheme"
|
||||
|
||||
interface BlogPostCommentsProps {}
|
||||
|
||||
export const BlogPostComments: React.FC<BlogPostCommentsProps> = () => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Giscus
|
||||
id="comments"
|
||||
repo="theoludwig/theoludwig"
|
||||
repoId="MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ="
|
||||
category="General"
|
||||
categoryId="DIC_kwDOFWUewM4CQ_WK"
|
||||
mapping="pathname"
|
||||
reactionsEnabled="1"
|
||||
emitMetadata="0"
|
||||
inputPosition="top"
|
||||
theme={theme}
|
||||
lang="en"
|
||||
loading="lazy"
|
||||
/>
|
||||
)
|
||||
}
|
127
packages/blog/src/BlogPostContent.tsx
Normal file
127
packages/blog/src/BlogPostContent.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { nodeTypes } from "@mdx-js/mdx"
|
||||
import rehypeShiki from "@shikijs/rehype"
|
||||
import { MDXRemote } from "next-mdx-remote/rsc"
|
||||
import Image from "next/image"
|
||||
import { FaLink } from "react-icons/fa"
|
||||
import rehypeKatex from "rehype-katex"
|
||||
import rehypeRaw from "rehype-raw"
|
||||
import rehypeSlug from "rehype-slug"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import remarkMath from "remark-math"
|
||||
|
||||
import { Link } from "@repo/i18n/navigation"
|
||||
import "katex/dist/katex.min.css"
|
||||
import { BlogPostComments } from "./BlogPostComments"
|
||||
|
||||
const Heading: React.FC<
|
||||
React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLHeadingElement>,
|
||||
HTMLHeadingElement
|
||||
> & { as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" }
|
||||
> = (props) => {
|
||||
const { children, as, id = "", ...rest } = props
|
||||
|
||||
const ComponentAs = as
|
||||
|
||||
return (
|
||||
<ComponentAs id={id} {...rest}>
|
||||
<Link href={`#${id}`} className="group relative hover:no-underline">
|
||||
<FaLink className="absolute bottom-2 left-[-26px] mr-2 hidden size-4 !text-black group-hover:inline dark:!text-white" />
|
||||
{children}
|
||||
</Link>
|
||||
</ComponentAs>
|
||||
)
|
||||
}
|
||||
|
||||
export interface BlogPostContentProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
export const BlogPostContent: React.FC<BlogPostContentProps> = async (
|
||||
props,
|
||||
) => {
|
||||
const { content } = props
|
||||
|
||||
return (
|
||||
<div className="prose mb-10">
|
||||
<div className="px-4 sm:px-8">
|
||||
<MDXRemote
|
||||
source={content}
|
||||
options={{
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm, remarkMath],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypeRaw, { passThrough: nodeTypes }],
|
||||
rehypeKatex,
|
||||
[
|
||||
rehypeShiki,
|
||||
{
|
||||
themes: {
|
||||
light: "light-plus",
|
||||
dark: "dark-plus",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}}
|
||||
components={{
|
||||
h1: (props) => {
|
||||
return <Heading as="h1" {...props} />
|
||||
},
|
||||
h2: (props) => {
|
||||
return <Heading as="h2" {...props} />
|
||||
},
|
||||
h3: (props) => {
|
||||
return <Heading as="h3" {...props} />
|
||||
},
|
||||
h4: (props) => {
|
||||
return <Heading as="h4" {...props} />
|
||||
},
|
||||
h5: (props) => {
|
||||
return <Heading as="h5" {...props} />
|
||||
},
|
||||
h6: (props) => {
|
||||
return <Heading as="h6" {...props} />
|
||||
},
|
||||
img: (properties) => {
|
||||
const { src = "", alt = "Blog Image" } = properties
|
||||
const source = src.replace("../../../apps/website/public/", "/")
|
||||
return (
|
||||
<span className="flex flex-col items-center justify-center">
|
||||
<Image
|
||||
src={source}
|
||||
alt={alt}
|
||||
width={1000}
|
||||
height={1000}
|
||||
quality={100}
|
||||
className="size-auto"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
a: (props) => {
|
||||
const { href = "", ...rest } = props
|
||||
if (href.startsWith("#")) {
|
||||
return <a {...props} />
|
||||
}
|
||||
if (href.startsWith("../posts/")) {
|
||||
return (
|
||||
<Link
|
||||
href={href
|
||||
.replace("../posts/", "/blog/")
|
||||
.replace(".md", "")}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <a target="_blank" {...props} />
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<BlogPostComments />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
29
packages/blog/src/BlogPostUI.tsx
Normal file
29
packages/blog/src/BlogPostUI.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { getISODate } from "@repo/utils/dates"
|
||||
import "katex/dist/katex.min.css"
|
||||
|
||||
import { Typography } from "@repo/ui/design/Typography"
|
||||
import { MainLayout } from "@repo/ui/MainLayout"
|
||||
import type { BlogPost } from "./BlogPost"
|
||||
import { BlogPostContent } from "./BlogPostContent"
|
||||
|
||||
export interface BlogPostUIProps {
|
||||
blogPost: BlogPost
|
||||
}
|
||||
|
||||
export const BlogPostUI: React.FC<BlogPostUIProps> = (props) => {
|
||||
const { blogPost } = props
|
||||
|
||||
return (
|
||||
<MainLayout className="break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center">
|
||||
<div className="my-12 flex flex-col items-center text-center">
|
||||
<Typography variant="h2" as="h1">
|
||||
{blogPost.frontmatter.title}
|
||||
</Typography>
|
||||
<p className="mt-2">
|
||||
{getISODate(new Date(blogPost.frontmatter.publishedOn))}
|
||||
</p>
|
||||
</div>
|
||||
<BlogPostContent content={blogPost.content} />
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
42
packages/blog/src/BlogPosts.tsx
Normal file
42
packages/blog/src/BlogPosts.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Link } from "@repo/i18n/navigation"
|
||||
import { Section, SectionContent } from "@repo/ui/design/Section"
|
||||
import { Typography } from "@repo/ui/design/Typography"
|
||||
import { getISODate } from "@repo/utils/dates"
|
||||
import type { BlogPost } from "./BlogPost"
|
||||
|
||||
export interface BlogPostsProps {
|
||||
posts: BlogPost[]
|
||||
}
|
||||
|
||||
export const BlogPosts: React.FC<BlogPostsProps> = (props) => {
|
||||
const { posts } = props
|
||||
|
||||
return (
|
||||
<ul className="list-none">
|
||||
{posts.map((post) => {
|
||||
const postPublishedOn = getISODate(
|
||||
new Date(post.frontmatter.publishedOn),
|
||||
)
|
||||
|
||||
return (
|
||||
<li key={post.slug}>
|
||||
<Link href={`/blog/${post.slug}`}>
|
||||
<Section verticalSpacing>
|
||||
<SectionContent
|
||||
className="cursor-pointer p-6 transition-all duration-300 ease-in-out hover:scale-[1.02] sm:p-6"
|
||||
shadowContainer
|
||||
>
|
||||
<Typography variant="h4" as="h3">
|
||||
{post.frontmatter.title}
|
||||
</Typography>
|
||||
<p className="mt-2">{postPublishedOn}</p>
|
||||
<p className="mt-3">{post.frontmatter.description}</p>
|
||||
</SectionContent>
|
||||
</Section>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
59
packages/blog/src/blog.ts
Normal file
59
packages/blog/src/blog.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
import matter from "gray-matter"
|
||||
import type { BlogPost, FrontMatter } from "./BlogPost"
|
||||
|
||||
export const BLOG_POSTS_PATH = path.join(
|
||||
process.cwd(),
|
||||
"..",
|
||||
"..",
|
||||
"packages",
|
||||
"blog",
|
||||
"posts",
|
||||
)
|
||||
|
||||
export const getBlogPosts = async (): Promise<BlogPost[]> => {
|
||||
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
|
||||
const blogPostsWithTime = await Promise.all(
|
||||
blogPosts.map(async (blogPostFilename) => {
|
||||
const [slug, extension] = blogPostFilename.split(".")
|
||||
if (slug == null || extension == null) {
|
||||
throw new Error("Invalid blog post filename.")
|
||||
}
|
||||
const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`)
|
||||
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
||||
encoding: "utf8",
|
||||
})
|
||||
const { data, content } = matter(blogPostContent) as unknown as {
|
||||
data: FrontMatter
|
||||
content: string
|
||||
}
|
||||
const date = new Date(data.publishedOn)
|
||||
return {
|
||||
slug,
|
||||
content,
|
||||
frontmatter: data,
|
||||
time: date.getTime(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
const blogPostsSortedByPublicationDate = blogPostsWithTime
|
||||
.filter((post) => {
|
||||
return post.frontmatter.isPublished
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return b.time - a.time
|
||||
})
|
||||
return blogPostsSortedByPublicationDate
|
||||
}
|
||||
|
||||
export const getBlogPostBySlug = async (
|
||||
slug: string,
|
||||
): Promise<BlogPost | undefined> => {
|
||||
const blogPosts = await getBlogPosts()
|
||||
const blogPost = blogPosts.find((blogPost) => {
|
||||
return blogPost.slug === slug
|
||||
})
|
||||
return blogPost
|
||||
}
|
19
packages/blog/src/stories/BlogPostUI.stories.tsx
Normal file
19
packages/blog/src/stories/BlogPostUI.stories.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { BLOG_POST_MOCK } from "../BlogPost"
|
||||
import { BlogPostUI as BlogPostUIComponent } from "../BlogPostUI"
|
||||
|
||||
const meta = {
|
||||
title: "Feature/Blog/BlogPostUI",
|
||||
component: BlogPostUIComponent,
|
||||
} satisfies Meta<typeof BlogPostUIComponent>
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const BlogPostUI: Story = {
|
||||
args: {
|
||||
blogPost: BLOG_POST_MOCK,
|
||||
},
|
||||
}
|
19
packages/blog/src/stories/BlogPosts.stories.tsx
Normal file
19
packages/blog/src/stories/BlogPosts.stories.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { BLOG_POST_MOCK } from "../BlogPost"
|
||||
import { BlogPosts as BlogPostsComponent } from "../BlogPosts"
|
||||
|
||||
const meta = {
|
||||
title: "Feature/Blog/BlogPosts",
|
||||
component: BlogPostsComponent,
|
||||
} satisfies Meta<typeof BlogPostsComponent>
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const BlogPosts: Story = {
|
||||
args: {
|
||||
posts: [BLOG_POST_MOCK],
|
||||
},
|
||||
}
|
Reference in New Issue
Block a user