Blog Post Page from MDX Content

Blog post page reading content from the MDX markdown content through Velite and tailwindcss/typography


Required dependencies

The tailwind typography module is used for displaying the blog content.

# npm i @tailwindcss/typography --save-dev

The typography module is then specified in the tailwind configuration file tailwind.config.ts:

import type { Config } from "tailwindcss";
 
const config = {
  darkMode: ["class"],
  ...
  theme: {
    ...
     extend: {
      ...
      animation: {
        "accordion-down": "accordion-down 0.2s ease-out",
        "accordion-up": "accordion-up 0.2s ease-out",
      },
    },
  },
  plugins: [
    require("tailwindcss-animate"),
    require("@tailwindcss/typography")
  ],
} satisfies Config;
 
export default config;
 

MDX component

The functionality for displaying MDX content in react is encapsulated in src/components/mdx-components.tsx:

import Image from "next/image";
import * as runtime from "react/jsx-runtime";
 
const useMDXComponent = (code: string) => {
  const fn = new Function(code);
  return fn({ ...runtime }).default;
};
 
const components = {
  Image,
};
 
interface MdxProps {
  code: string;
}
 
export function MDXContent({ code }: MdxProps) {
  const Component = useMDXComponent(code);
  return <Component components={components} />;
}

Page for displaying an individual blog item

The MDX component created earlier is used on the page for displaying a blog post src/app/blog/[...slug]/page.tsx:

import { posts } from "#site/content";
import { MDXContent } from "@/components/mdx-components";
import { notFound } from "next/navigation";
 
interface PostPageProps {
  params: {
    slug: string[];
  };
}
 
async function getPostFromParams(params: PostPageProps["params"]) {
  const slug = params?.slug?.join("/");
  const post = posts.find((post) => post.slugAsParams === slug);
 
  return post;
}
 
export async function generateStaticParams(): Promise<
  PostPageProps["params"][]
> {
  return posts.map((post) => ({ slug: post.slugAsParams.split("/") }));
}
 
export default async function PostPage({ params }: PostPageProps) {
  const post = await getPostFromParams(params);
 
  if (!post || !post.published) {
    notFound();
  }
 
  return (
    <article className="container py-6 prose dark:prose-invert max-w-3xl mx-auto">
      <h1 className="mb-2">{post.title}</h1>
      {post.description ? (
        <p className="text-xl mt-0 text-muted-foreground">{post.description}</p>
      ) : null}
      <hr className="my-4" />
      <MDXContent code={post.body} />
    </article>
  );
}

Blog post URL

Once the change are done, the blog pages can be accessed at their respective relative path, for instnace http://localhost:3000/blog/2023/06-21-summer-solstice.

Once the site is rebuilt the site map also contains entries for the new pages. With the entries created in the previous chapter we should observe in public\sitemap\sitemap-0.xml:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://example.com</loc><lastmod>2024-07-02T15:43:23.213Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://example.com/about</loc><lastmod>2024-07-02T15:43:23.214Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://example.com/blog/2023/06-21-summer-solstice</loc><lastmod>2024-07-02T15:43:23.214Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://example.com/blog/2024/06-21-summer-solstice</loc><lastmod>2024-07-02T15:43:23.214Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
</urlset>


Last Updated: