DEV Community

Cover image for Check Out My New Markdown Blog App!
Burak Bilen
Burak Bilen

Posted on

Check Out My New Markdown Blog App!

I have been working on my latest blog integrations to my personal web application since the last month, I'd love to share how was the process, what did I learn through the journey!

Click here to go Blog Page
Click here to go the animation library that I've been building since the last month

Tech stack

  • Front-end
    -- Next.js
    -- Motion Provider (for animations)
    -- Typescript
    -- React 19
    -- Redux
    -- next-mdx-remote
    -- TailwindCSS V4
    -- Shadcn
    -- Radix UI

  • Back-end
    -- Supabase
    -- NextAUTH
    -- PostgreSQL

  • Hosting & Domain
    -- Netlify
    -- Namecheap

How the blogging system works?

As you may know, reusability is the most important thing that every developer should consider while developing any react application. To stay consistent among the designed components, I used next.js's SSG for my server-side site generated pages at the build time. Remember that I've been using page router for the entire project.

However, before you go with the page router you may consider as a full-stack developer:

  • Whenever you create a new row in db for blogs which means when you create a new blog, you have to navigate to your project in your code editor then you have to run npx next build to re-create your pages inside blog pages. Therefore we can put it in the list of cons because you can not directly integrate ISR (incremental server rendering) that detects the new blog from the db then render it on UI as long as you continue with the page router.
  • Whenever you create a new blog and after running npx next build, you will have pages based on the rows in your database. SEO IS CRUCIAL -when it comes to URL- you have to transform the title to a slug like, If you create a blog page named How to animate? then you should use the URL of the page how-to-animate.

Here how I used my SSG integration inside pages/blogs/[slug].tsx:

import { db } from "@/db";
import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm";
import { serialize } from "next-mdx-remote/serialize";
import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import Hydrate from "@/components/Blog/hydrate";
import Head from "next/head";
import Image from "next/image";
import { BlogPageProps } from "@/interfaces";
import { useDispatch } from "react-redux";
import { useEffect } from "react";
import { setBlog } from "@/redux/slices/blogSlice";
import Reccomendation from "@/components/Blog/reccomendation";

const BlogPage: NextPage<BlogPageProps> = ({ source, frontMatter }) => {
  const publishedDate = new Date(frontMatter.published_at).toLocaleDateString();
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(setBlog(frontMatter));
  }, [dispatch]);

  return (
    <>
      <Head>
        <title>{`Burak Bilen | ${frontMatter.title}`}</title>
        <meta
          name="description"
          content={`Read ${frontMatter.title} published on ${frontMatter.published_at}.`}
        />
        <meta property="og:title" content={frontMatter.title} />
        <meta
          property="og:description"
          content={`Read ${frontMatter.title} published on ${publishedDate}.`}
        />
        <meta property="og:type" content="article" />
        <meta property="og:image" content={frontMatter.banner_image} />
        <meta
          property="og:url"
          content={`https://burakdev.com/blogs/${frontMatter.slug}`}
        />
        <meta property="og:site_name" content="burakdev" />
      </Head>
      <main>
        <Image
          alt={`burakdev | ${frontMatter.title}`}
          src={`${frontMatter.banner_image}`}
          height={900}
          className="sr-only"
          width={1200}
        />
        <Hydrate frontMatter={frontMatter} source={source} />
      </main>
      <Reccomendation />
    </>
  );
};

export default BlogPage;

export const getStaticPaths: GetStaticPaths = async () => {
  const { data, error } = await db.from("blog_posts").select("slug");

  if (error) {
    console.error("Error fetching data:", error);
    return { paths: [], fallback: false };
  }

  const paths = data.map((post: { slug: string }) => ({
    params: { slug: post.slug },
  }));

  return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const slug = params?.slug as string;

  const { data, error } = await db
    .from("blog_posts")
    .select("*")
    .eq("slug", slug)
    .single();

  if (error || !data) {
    console.error("Error fetching data:", error);
    return { notFound: true };
  }

  const mdxSource = await serialize(data.content, {
    mdxOptions: {
      remarkPlugins: [remarkGfm],
      rehypePlugins: [rehypeSlug],
    },
    scope: data,
  });

  return {
    props: {
      source: mdxSource,
      frontMatter: {
        id: data.id,
        title: data.title,
        slug: data.slug,
        tags: data.tags,
        banner_image: data.banner_image,
        description: data.description,
        published_at: data.published_at,
        level: data.level,
        like: data.like,
        view: data.view,
      },
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

What happens in this blogs/[slug].tsx ?

Let's break the big picture's workflow down:

  • getStaticPaths gets the paths from the db using supabase's select() logic, over here:
const { data, error } = await db.from("blog_posts").select("slug")
Enter fullscreen mode Exit fullscreen mode
  • If an error persist we return an error message with empty path array and fallback: false. And you may ask, fallback: false means other routes should 404. However if you are using app router by assigning the fallback prop as blocking and add revalidate: 3600 you will change the render method of Next.js to ISR which means the app will be able to revalidate the props of the page by sending request to the database, and If data fetching operation successfull, it will render automatically on the UI at runtime in your host without rebuilding.

  • After that we pass the paths data to getStaticProps using:

const paths = data.map((post: { slug: string }) => ({params: { slug: post.slug }
}));
Enter fullscreen mode Exit fullscreen mode
  • getStaticProps takes the data and at first checks if the slug is valid or not like:

    const slug = params?.slug as string;
    
  • The logic fetches the all data into the specific row according to our slug variable over here:

    const { data, error } = await db
        .from("blog_posts")
        .select("*")
        .eq("slug", slug)
        .single();
    
    if (error || !data) {
        console.error("Error fetching data:", error);
        return { notFound: true };
    }
    
  • After that, finally everything comes to serializing the page content. This is crucial. Because who wants to write HTML formatted blogs? Instead you would prefer Markdown formatted and in this case you have to serialize your data using next-mdx-remote/serialize

  • Serializing refers to the process of converting MDX content (a format that combines Markdown with JSX/React components) into a format that can be directly rendered by React.

  • Basically, you have page content's like # Hello World! represents <h1>Hello World!</h1> or > This is a blockquote represents <blockquote>This is a blockquote</blockquote>, this transformed data goes to our actual Page renderer in our case the page renderer is:

    const BlogPage: NextPage<BlogPageProps> = ({ source, frontMatter }) => {
    const publishedDate = new Date(frontMatter.published_at).toLocaleDateString();
    const dispatch = useDispatch();
    
    useEffect(() => {
        dispatch(setBlog(frontMatter));
    }, [dispatch]);
    
    return (
        <>
        <Head>
            <title>{`Burak Bilen | ${frontMatter.title}`}</title>
            <meta
            name="description"
            content={`Read ${frontMatter.title} published on ${frontMatter.published_at}.`}
            />
            <meta property="og:title" content={frontMatter.title} />
            <meta
            property="og:description"
            content={`Read ${frontMatter.title} published on ${publishedDate}.`}
            />
            <meta property="og:type" content="article" />
            <meta property="og:image" content={frontMatter.banner_image} />
            <meta
              property="og:url"
              content={`https://burakdev.com/blogs/${frontMatter.slug}`}
            />
            <meta property="og:site_name" content="burakdev" />
          </Head>
          <main>
            <Image
              alt={`burakdev | ${frontMatter.title}`}
              src={`${frontMatter.banner_image}`}
              height={900}
              className="sr-only"
              width={1200}
            />
            <Hydrate frontMatter={frontMatter} source={source} />
          </main>
          <Reccomendation />
        </>
      );
    };
    

Let's dive into rendering our blogs

  • You may remember we passed our serialized jsx formatted contents from getStaticProps here:
  return {
    props: {
      source: mdxSource,
      frontMatter: {
        id: data.id,
        title: data.title,
        slug: data.slug,
        tags: data.tags,
        banner_image: data.banner_image,
        description: data.description,
        published_at: data.published_at,
        level: data.level,
        like: data.like,
        view: data.view,
      },
    },
  };
Enter fullscreen mode Exit fullscreen mode
  • I forgot to mention how my db looks like:
create table public.blog_posts (
  id serial not null,
  title text not null default 'title'::text,
  slug character varying(255) not null,
  content text not null,
  tags text[] not null,
  published_at timestamp with time zone null default now(),
  description text not null default 'Burak Bilen '::text,
  banner_image text not null default 'https://ytpmpkgcjlcdidphswzv.supabase.co/storage/v1/object/public/banner//banner-bg.webp'::text,
  level numeric not null default '1'::numeric,
  view numeric not null default '1'::numeric,
  like numeric not null default '24'::numeric,
  constraint blog_posts_pkey primary key (id),
  constraint blog_posts_slug_key unique (slug),
  constraint blog_posts_level_check check ((level > (0)::numeric))
) TABLESPACE pg_default;
Enter fullscreen mode Exit fullscreen mode
  • Right now, it's time to wrap all this things. First of all we are getting our date data using:
const publishedDate = new Date(frontMatter.published_at).toLocaleDateString();
Enter fullscreen mode Exit fullscreen mode
  • Secondly, we are sending the all props we got from db to redux slice named blogSlice
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(setBlog(frontMatter));
  }, [dispatch]);
Enter fullscreen mode Exit fullscreen mode
  • After, we starting rendering our content as a plain html for seo wrapping HTML tags into next/head element:
<Head>
    <title>{`Burak Bilen | ${frontMatter.title}`}</title>
    <meta
      name="description"
      content={`Read ${frontMatter.title} published on ${frontMatter.published_at}.`}
    />
    <meta property="og:title" content={frontMatter.title} />
    <meta
      property="og:description"
      content={`Read ${frontMatter.title} published on ${publishedDate}.`}
    />
    <meta property="og:type" content="article" />
    <meta property="og:image" content={frontMatter.banner_image} />
    <meta
      property="og:url"
      content={`https://burakdev.com/blogs/${frontMatter.slug}`}
    />
    <meta property="og:site_name" content="burakdev" />
</Head>
Enter fullscreen mode Exit fullscreen mode
  • We pass all of our data as a prop to <Hydrate /> component here:
<Hydrate frontMatter={frontMatter} source={source} />
Enter fullscreen mode Exit fullscreen mode
  • Here is the <Hydrate /> component:
import { FC } from "react";
import { MDXRemote } from "next-mdx-remote";
import { MdxComponents } from "./components/mdx-components";
import { BannerCard } from "../banner-card";
import { useSelector } from "react-redux";
import { BlogPageProps, PingProps, ReduxThemeProps } from "@/interfaces";
import MotionContainer from "../MotionProvider/motion-container";
import { Badge } from "../ui/badge";
import Ping from "../ui/ping";
import SessionText from "./components/SessionText";
import Socials from "./components/Socials";
import Actions from "./components/Actions";

const Hydrate: FC<BlogPageProps> = ({ source, frontMatter }) => {
  const {
    banner_image,
    id,
    like,
    view,
    description,
    published_at,
    tags,
    title,
    level,
  } = frontMatter;

  const SESSION_NO = `JUSTCODESESSION00${id.toString()}`;
  const appTheme = useSelector(
    (state: { theme: ReduxThemeProps }) => state.theme
  );
  const publishedDate = new Date(published_at).toLocaleDateString();
  console.log(banner_image);

  const levelConfig: PingProps = {
    mode: level === 1 ? "success" : level === 2 ? "warning" : "error",
    size: "sm",
    isAnimated: true,
  };
  const blogLevel =
    level === 1 ? "Beginner" : level === 2 ? "Intermediate" : "Advanced";

  return (
    <>
      <header className="article-header">
        <div className="relative">
          <BannerCard
            theme={appTheme}
            description={description}
            delayLogic="sinusoidal"
            transition="cubicElastic"
            title={title}
            src={banner_image}
            imageAnimationDuration={5}
            animations={["rotateFlipY"]}
            className="mt-4 lg:mt-0 max-h-max min-h-60 "
            duration={1}
          />
          <ul className="absolute bottom-4 left-4 flex gap-2 flex-row">
            {tags.map((val, idx) => (
              <MotionContainer
                key={idx}
                elementType={"li"}
                mode={[idx % 2 === 0 ? "fadeDown" : "fadeUp", "filterBlurIn"]}
                children={<Badge>{val}</Badge>}
                configView={{ once: false, amount: "some" }}
                delay={idx * 0.2}
                duration={1}
                transition="smooth"
              />
            ))}
          </ul>
          <div className="absolute top-4 right-4 flex gap-1 flex-wrap text-muted-foreground font-mono font-light text-xs">
            <span>#</span>
            <div className="flex flex-wrap gap-2">
              <SessionText text={SESSION_NO} />
            </div>
          </div>
          <div className="absolute font-mono text-muted-foreground text-xs top-4 left-4 flex gap-2 items-center justify-center">
            <Ping {...levelConfig} />
            <span>{blogLevel}</span>
          </div>
          <span className="absolute bottom-4 right-4 text-muted-foreground font-mono font-light text-xs">
            {publishedDate}
          </span>
        </div>
        <aside className="sticky top-0 py-4 w-full max-h-max mt-2 flex items-center justify-between">
          <Actions blog_id={id} like={like} view={view} />
          <Socials />
        </aside>
      </header>
      <article>
        <section className="article-content">
          <MDXRemote {...source} components={MdxComponents} />
        </section>
      </article>
    </>
  );
};

export default Hydrate;
Enter fullscreen mode Exit fullscreen mode
  • Here is the most significant part for UI. My dear developer please use renderer logic.

  • Renderer logic is the component stack that you import in one file to say the compiler Hey compiler, I have some HTML elements that I want to render, but I want to render in my own method

  • In our project the actual renderer component that we defined in our <Hydrate /> component is <MDXRemote {...source} components={MdxComponents} />

  • Let's take a look at what's inside:

import React from "react";
import { MdHeading } from "../ui/MdHeading";
import { MdParagraph } from "../ui/MdParagraph";
import { MdLink } from "../ui/MdLink";
import { MdCode } from "../ui/MdCode";
import { MdBlockquote } from "../ui/MdBlockquote";
import { MdPre } from "../ui/MdPre";
import { MdImage } from "../ui/MdImage";
import { MdUl } from "../ui/MdUl";
import {
  MdTable,
  MdTbody,
  MdTd,
  MdTh,
  MdThead,
  MdTr,
} from "../ui/MdTableComponents";
import { MdHr } from "../ui/MdHr";

type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
  href: string;
};

export const MdxComponents = {
  h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
    <MdHeading as="h1" size="xl" className="my-4" {...props} />
  ),
  h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
    <MdHeading as="h2" size="lg" className="my-4" {...props} />
  ),
  h3: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
    <MdHeading as="h3" size="md" className="my-4" {...props} />
  ),
  h4: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
    <MdHeading as="h4" size="sm" className="my-4" {...props} />
  ),
  h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
    <MdHeading as="h5" size="xs" className="my-4" {...props} />
  ),
  p: (props: React.HTMLAttributes<HTMLParagraphElement>) => (
    <MdParagraph className="my-8" {...props} />
  ),
  a: (props: AnchorProps) => <MdLink {...props} />,
  code: (props: React.HTMLAttributes<HTMLElement>) => <MdCode {...props} />,
  blockquote: (props: React.HTMLAttributes<HTMLQuoteElement>) => (
    <MdBlockquote {...props} />
  ),
  pre: (props: React.HTMLAttributes<HTMLPreElement>) => <MdPre {...props} />,
  img: (
    props: React.HTMLAttributes<HTMLImageElement> & { alt: string; src: string }
  ) => (
    <MdImage
      {...props}
      alt={props.alt}
      src={props.src}
      className="rounded-md"
    />
  ),
  ul: (props: React.HTMLAttributes<HTMLUListElement>) => <MdUl {...props} />,
  table: (props: React.HTMLAttributes<HTMLTableElement>) => (
    <MdTable {...props} />
  ),
  thead: (props: React.HTMLAttributes<HTMLTableSectionElement>) => (
    <MdThead {...props} />
  ),
  tbody: (props: React.HTMLAttributes<HTMLTableSectionElement>) => (
    <MdTbody {...props} />
  ),
  tr: (props: React.HTMLAttributes<HTMLTableRowElement>) => <MdTr {...props} />,
  th: (props: React.ThHTMLAttributes<HTMLTableCellElement>) => (
    <MdTh {...props} />
  ),
  td: (props: React.TdHTMLAttributes<HTMLTableCellElement>) => (
    <MdTd {...props} />
  ),
  hr: (props: React.HTMLAttributes<HTMLHRElement>) => (
    <MdHr className="my-24" {...props} />
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • You get it already but basically we create custom components to assign HTML elements. With this approach instead of rendering the serialized HTML, we render whatever we want on UI at build time. For example here how I made the blockquote component as a animationed component that renders on UI:
import MotionQueue from "@/components/MotionProvider/motion-queue";
import { AnimationQueueAnimationProps } from "@/components/MotionProvider/types";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import extractTextFromReactNode from "@/utils/extractTextFromReactNode";
import { Info } from "lucide-react";
import React, { FC } from "react";

export const MdBlockquote: FC<React.HTMLAttributes<HTMLQuoteElement>> = ({
  className,
  ...props
}) => {
  const text = extractTextFromReactNode(props.children).split(/\s+/);

  return (
    <blockquote className="flex flex-col gap-2 my-6">
      <Alert>
        <AlertTitle className="font-bold lg:text-xl text-lg text-primary flex flex-row-reverse gap-2 justify-between">
          <Info />
          <span>Burak Says,</span>
        </AlertTitle>
        <AlertDescription className="text-muted-foreground text-sm flex flex-wrap gap-1 -ml-1 my-2">
          <MotionQueue
            elementType={"span"}
            animations={
              Array.from({ length: text.length }).fill({
                mode: ["filterBlurIn", "fadeRight"],
                duration: 0.88,
                configView: { once: false, amount: 0.5 },
              }) as AnimationQueueAnimationProps[]
            }
            isDynamicallyQueued
            delayLogic="linear"
            duration={0.25}
            children={text}
          />
        </AlertDescription>
      </Alert>
    </blockquote>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • Here is an another example for headings
import React, { FC } from "react";
import { cn } from "@/lib/utils";
import { MdHeadingProps } from "../types/interfaces";

export const MdHeading: FC<MdHeadingProps> = ({
  as: Component = "h1",
  size = "xl",
  className,
  ...props
}) => {
  const sizeClasses = {
    xl: "text-4xl font-bold",
    lg: "text-3xl font-bold",
    md: "text-2xl font-semibold",
    sm: "text-xl font-semibold",
    xs: "text-lg font-medium",
  };

  return <Component className={cn(sizeClasses[size], className)} {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

Ultimately, nowadays the web requires a good patience and intelligent approaches from developers to create magic but the magic is happening just we should learn how to control it.

Top comments (0)