1
1
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:
2024-07-30 23:59:06 +02:00
parent 0f44e64c0c
commit 7bde328b96
336 changed files with 22933 additions and 26923 deletions

View 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

View 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"
/>
)
}

View 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>
)
}

View 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>
)
}

View 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
View 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
}

View 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,
},
}

View 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],
},
}