Styling the Article Detail Page - Modern Next.js Blog Series #13
- Published on
This article is also published at it 邦幫忙 2022 iThome Ironman Contest
TL;DR
This is the 13th article in the "Modern Blog 30 Days" series. In the last article, we beautified the homepage style using Tailwind CSS, and in this article, we will continue to beautify the style of the article detail pages!
Screenshot results as follows:
The code changes for this article are as follows:
Installing the sass package to Support SCSS in Next.js
In the forthcoming styling, we will add many deep CSS properties for article content.
To make CSS more readable, we will switch to using the more convenient and readable SCSS syntax.
To support SCSS syntax in Next.js, we need to install the sass package:
Copied!pnpm add -D sass
No configuration is needed after installation, and you can now import .scss files in Next.js.
For more details, refer to the official Next.js documentation: Basic Features: Built-in CSS Support | Next.js
Styling the Article Detail Page
Let's start styling! We will add 5 new files and modify 1 file.
The style mainly modifies the project based on timlrx/tailwind-nextjs-starter-blog.
Add /src/components/PageTitle.tsx
:
Copied!type Props = { children: React.ReactNode; }; export default function PageTitle({ children }: Props) { return ( <h1 className="md:leading-14 text-3xl font-extrabold leading-9 tracking-tight text-gray-900 transition-colors dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl"> {children} </h1> ); }
Add /src/components/PostBody/PostBody.module.scss
:
Copied!.postBody { :global(.rehype-code-title) { @apply -mb-3 rounded-tl rounded-tr bg-slate-600 px-4 pt-1 pb-2 font-mono text-sm text-gray-200; } div:global(.rehype-code-title) + pre { @apply rounded-tl-none rounded-tr-none; } img { @apply ml-auto mr-auto; } blockquote { @apply not-italic; p:first-of-type::before { content: none; } p:last-of-type::after { content: none; } } }
Add /src/components/PostBody/PostBody.tsx
:
Copied!import clsx from "clsx"; import styles from "./PostBody.module.scss"; type Props = { children: React.ReactNode; }; export default function PostBody({ children }: Props) { return ( <div className={clsx( "prose dark:prose-dark mx-auto transition-colors", styles.postBody )} > {children} </div> ); }
Add /src/components/PostBody/index.ts
:
Copied!import PostBody from "./PostBody"; export default PostBody;
Add /src/components/PostLayout.tsx
:
Copied!import { useRouter } from "next/router"; import CustomLink from "@/components/CustomLink"; import PageTitle from "@/components/PageTitle"; import PostBody from "@/components/PostBody"; import formatDate from "@/lib/formatDate"; export interface PostForPostLayout { date: string; title: string; } export type RelatedPostForPostLayout = { title: string; path: string; } | null; type Props = { post: PostForPostLayout; nextPost: RelatedPostForPostLayout; prevPost: RelatedPostForPostLayout; children: React.ReactNode; }; export default function PostLayout({ post, nextPost, prevPost, children, }: Props) { const { date, title } = post; const { locale } = useRouter(); return ( <article> <div className="divide-y divide-gray-200 transition-colors dark:divide-gray-700"> <header className="py-6"> <div className="space-y-1 text-center"> <div className="mb-4"> <PageTitle>{title}</PageTitle> </div> <dl className="space-y-10"> <div> <dt className="sr-only">發佈時間</dt> <dd className="text-base font-medium leading-6 text-gray-500 transition-colors dark:text-gray-400"> <time dateTime={date}>{formatDate(date, locale)}</time> </dd> </div> </dl> </div> </header> <div className="divide-y divide-gray-200 pt-10 pb-8 transition-colors dark:divide-gray-700"> <PostBody>{children}</PostBody> </div> <div className="divide-y divide-gray-200 pb-8 transition-colors dark:divide-gray-700" // style={{ gridTemplateRows: 'auto 1fr' }} > <footer> <div className="flex flex-col gap-4 pt-4 text-base font-medium sm:flex-row sm:justify-between xl:gap-8 xl:pt-8"> {prevPost ? ( <div className="basis-6/12"> <h2 className="mb-1 text-xs uppercase tracking-wide text-gray-500 transition-colors dark:text-gray-400"> Previous Post </h2> <CustomLink href={prevPost.path} className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400 transition-colors" > ← {prevPost.title} </CustomLink> </div> ) : ( <div /> )} {nextPost && ( <div className="basis-6/12"> <h2 className="mb-1 text-left text-xs uppercase tracking-wide text-gray-500 transition-colors dark:text-gray-400 sm:text-right"> Next Post </h2> <CustomLink href={nextPost.path} className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400 block transition-colors sm:text-right" > {nextPost.title} → </CustomLink> </div> )} </div> </footer> </div> </div> </article> ); }
Modify /src/pages/posts/[slug].tsx
:
Copied!import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; import { useMDXComponent } from "next-contentlayer/hooks"; import PostLayout, { PostForPostLayout, RelatedPostForPostLayout, } from "@/components/PostLayout"; import { allPosts, allPostsNewToOld } from "@/lib/contentLayerAdapter"; type PostForPostPage = PostForPostLayout & { title: string; description: string; body: { code: string; }; }; type Props = { post: PostForPostPage; prevPost: RelatedPostForPostLayout; nextPost: RelatedPostForPostLayout; }; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps<Props> = ({ params }) => { const postIndex = allPostsNewToOld.findIndex( (post) => post.slug === params?.slug ); if (postIndex === -1) { return { notFound: true, }; } const prevFull = allPostsNewToOld[postIndex + 1] || null; const prevPost: RelatedPostForPostLayout = prevFull ? { title: prevFull.title, path: prevFull.path } : null; const nextFull = allPostsNewToOld[postIndex - 1] || null; const nextPost: RelatedPostForPostLayout = nextFull ? { title: nextFull.title, path: nextFull.path } : null; const postFull = allPostsNewToOld[postIndex]; const post: PostForPostPage = { title: postFull.title, date: postFull.date, description: postFull.description, body: { code: postFull.body.code, }, }; if (!post) { return { notFound: true, }; } return { props: { post, prevPost, nextPost, }, }; }; const PostPage: NextPage<Props> = ({ post, prevPost, nextPost }) => { const { description, title, body: { code }, } = post; const MDXContent = useMDXComponent(code); return ( <> <Head> <title>{title}</title> <meta name="description" content={description} /> <link rel="icon" href="/favicon.ico" /> </Head> <PostLayout post={post} prevPost={prevPost} nextPost={nextPost}> <MDXContent /> </PostLayout> </> ); }; export default PostPage;
Results
Done! Use pnpm dev
and visit any article detail page to see the beautified styles!
http://localhost:3000/posts/markdown-demo
Screenshot results as follows:
The code changes for this article are as follows:
References
- timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
- Basic Features: Built-in CSS Support | Next.js
Next Article
Congratulations! We've successfully used Tailwind CSS to style the article detail page in Next.js!
However, if you insert code blocks in your article, you'll find that the code does not have Syntax Highlighting, making it very hard to read.
In the next article, we will use rehype-prism-plus to add Syntax Highlighting to the articles!