diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..d1e226f --- /dev/null +++ b/app/blog/[slug]/page.tsx @@ -0,0 +1,155 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { cookies } from 'next/headers' +import Link from 'next/link' +import Image from 'next/image' +import { MDXRemote } from 'next-mdx-remote/rsc' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faLink } from '@fortawesome/free-solid-svg-icons' +import date from 'date-and-time' +import { nodeTypes } from '@mdx-js/mdx' +import rehypeRaw from 'rehype-raw' +import remarkGfm from 'remark-gfm' +import rehypeSlug from 'rehype-slug' +import remarkMath from 'remark-math' +import rehypeKatex from 'rehype-katex' +import { getHighlighter } from 'shiki' + +import 'katex/dist/katex.min.css' + +import { remarkSyntaxHighlightingPlugin } from '@/utils/remarkSyntaxHighlightingPlugin' +import { getPostBySlug } from '@/utils/blog' +import { BlogPostComments } from '@/components/BlogPostComments' +import { getTheme } from '@/theme/theme.server' + +const Heading = ( + props: React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLHeadingElement + > +): JSX.Element => { + const { children, id = '' } = props + return ( +

+ + + + {children} +

+ ) +} + +interface BlogPostPageProps { + params: { + slug: string + } +} + +export const generateMetadata = async ( + props: BlogPostPageProps +): Promise => { + const post = await getPostBySlug(props.params.slug) + if (post == null || !post.frontmatter.isPublished) { + return notFound() + } + const title = `${post.frontmatter.title} | Théo LUDWIG` + const description = post.frontmatter.description + return { + title, + description, + openGraph: { + title, + description + }, + twitter: { + title, + description + } + } +} + +const BlogPostPage = async (props: BlogPostPageProps): Promise => { + const { params } = props + + const post = await getPostBySlug(params.slug) + if (post == null || !post.frontmatter.isPublished) { + return notFound() + } + + const cookiesStore = cookies() + const theme = getTheme() + + const highlighter = await getHighlighter({ + theme: `${theme}-plus` + }) + + return ( +
+
+

{post.frontmatter.title}

+

+ {date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')} +

+
+
+
+ { + const { src = '', alt = 'Blog Image' } = properties + const source = src.replace('../public/', '/') + return ( + + {alt} + + ) + }, + a: (props) => { + const { href = '' } = props + if (href.startsWith('#')) { + return + } + return ( + + ) + } + }} + /> + +
+
+
+ ) +} + +export default BlogPostPage diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 0000000..61b4c8f --- /dev/null +++ b/app/blog/page.tsx @@ -0,0 +1,40 @@ +import { Suspense } from 'react' +import type { Metadata } from 'next' + +import { BlogPosts } from '@/components/BlogPosts' +import { Loader } from '@/components/Loader/Loader' + +const title = 'Blog | Théo LUDWIG' +const description = + 'The latest news about my journey of learning computer science.' + +export const metadata: Metadata = { + title, + description, + openGraph: { + title, + description + }, + twitter: { + title, + description + } +} + +const BlogPage = async (): Promise => { + return ( +
+
+

Blog

+

+ {description} +

+
+ }> + + +
+ ) +} + +export default BlogPage diff --git a/app/layout.tsx b/app/layout.tsx index 0c048dd..118af0a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -68,8 +68,8 @@ const RootLayout = (props: RootLayoutProps): JSX.Element => { colorScheme: theme }} > - -
+ +
{children}
diff --git a/components/BlogPostComments.tsx b/components/BlogPostComments.tsx new file mode 100644 index 0000000..8f08f98 --- /dev/null +++ b/components/BlogPostComments.tsx @@ -0,0 +1,33 @@ +'use client' + +import Giscus from '@giscus/react' + +import { useTheme } from '@/theme/theme.client' +import type { CookiesStore } from '@/utils/constants' + +interface BlogPostCommentsProps { + cookiesStore: CookiesStore +} + +export const BlogPostComments = (props: BlogPostCommentsProps): JSX.Element => { + const { cookiesStore } = props + + const theme = useTheme(cookiesStore) + + return ( + + ) +} diff --git a/components/BlogPosts.tsx b/components/BlogPosts.tsx new file mode 100644 index 0000000..11ac8e0 --- /dev/null +++ b/components/BlogPosts.tsx @@ -0,0 +1,42 @@ +import Link from 'next/link' +import date from 'date-and-time' + +import { ShadowContainer } from '@/components/design/ShadowContainer' +import { getPosts } from '@/utils/blog' + +export const BlogPosts = async (): Promise => { + const posts = await getPosts() + + return ( +
+
+ {posts.map((post, index) => { + const postPublishedOn = date.format( + new Date(post.frontmatter.publishedOn), + 'DD/MM/YYYY' + ) + return ( + + +

+ {post.frontmatter.title} +

+

+ {postPublishedOn} +

+

+ {post.frontmatter.description} +

+
+ + ) + })} +
+
+ ) +} diff --git a/components/Header/Locales/index.tsx b/components/Header/Locales/index.tsx index 689ecd2..5f02dff 100644 --- a/components/Header/Locales/index.tsx +++ b/components/Header/Locales/index.tsx @@ -1,5 +1,6 @@ 'use client' +import { usePathname } from 'next/navigation' import { useCallback, useEffect, useState, useRef } from 'react' import classNames from 'clsx' @@ -16,6 +17,7 @@ export interface LocalesProps { export const Locales = (props: LocalesProps): JSX.Element => { const { currentLocale, cookiesStore } = props + const pathname = usePathname() const [hiddenMenu, setHiddenMenu] = useState(true) const languageClickRef = useRef(null) @@ -48,6 +50,10 @@ export const Locales = (props: LocalesProps): JSX.Element => { setLocale(locale) } + if (pathname.startsWith('/blog')) { + return <> + } + return (
= (props) => { - const { showLocale = false } = props - +export const Header = (): JSX.Element => { const cookiesStore = cookies() const i18n = getI18n() @@ -44,12 +38,10 @@ export const Header: React.FC = (props) => { Blog
- {showLocale ? ( - - ) : null} +
diff --git a/tailwind.config.js b/tailwind.config.js index d3551e1..3abe877 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -19,8 +19,8 @@ const tailwindConfig = { } }, boxShadow: { - dark: '0px 0px 6px 6px rgba(0, 0, 0, 0.25)', - light: '0px 0px 6px 6px rgba(0, 0, 0, 0.10)', + dark: '0px 0px 4px 4px rgba(0, 0, 0, 0.25)', + light: '0px 0px 4px 4px rgba(0, 0, 0, 0.10)', darkFlag: '0px 1px 10px hsla(0, 0%, 100%, 0.2)', lightFlag: '0px 1px 10px rgba(0, 0, 0, 0.25)' }, diff --git a/utils/blog.ts b/utils/blog.ts index a31803b..380ec3f 100644 --- a/utils/blog.ts +++ b/utils/blog.ts @@ -1,23 +1,11 @@ import fs from 'node:fs' import path from 'node:path' -import type { MDXRemoteSerializeResult } from 'next-mdx-remote' -import { nodeTypes } from '@mdx-js/mdx' -import rehypeRaw from 'rehype-raw' -import { serialize } from 'next-mdx-remote/serialize' -import remarkGfm from 'remark-gfm' -import rehypeSlug from 'rehype-slug' -import remarkMath from 'remark-math' -import rehypeKatex from 'rehype-katex' +import { cache } from 'react' import matter from 'gray-matter' -import { getHighlighter } from 'shiki' - -import { remarkSyntaxHighlightingPlugin } from './remarkSyntaxHighlightingPlugin' export const POSTS_PATH = path.join(process.cwd(), 'posts') -export type MDXSource = MDXRemoteSerializeResult> - export interface FrontMatter { title: string description: string @@ -25,17 +13,13 @@ export interface FrontMatter { publishedOn: string } -export interface PostMetadata { +export interface Post { frontmatter: FrontMatter slug: string content: string } -export interface Post extends PostMetadata { - source: MDXSource -} - -export const getPosts = async (): Promise => { +export const getPosts = cache(async (): Promise => { const posts = await fs.promises.readdir(POSTS_PATH) const postsWithTime = await Promise.all( posts.map(async (postFilename) => { @@ -68,34 +52,14 @@ export const getPosts = async (): Promise => { return b.time - a.time }) return postsWithTimeSorted -} +}) -export const getPostBySlug = async ( - slug?: string | string[] -): Promise => { - const posts = await getPosts() - const post = posts.find((post) => { - return post.slug === slug - }) - if (post == null) { - return undefined +export const getPostBySlug = cache( + async (slug: string): Promise => { + const posts = await getPosts() + const post = posts.find((post) => { + return post.slug === slug + }) + return post } - const highlighter = await getHighlighter({ - theme: 'dark-plus' - }) - const source = await serialize(post.content, { - mdxOptions: { - remarkPlugins: [ - remarkGfm, - [remarkSyntaxHighlightingPlugin, { highlighter }], - remarkMath - ], - rehypePlugins: [ - rehypeSlug, - [rehypeRaw, { passThrough: nodeTypes }], - rehypeKatex - ] - } - }) - return { ...post, source } -} +)