Skip to content

Instantly share code, notes, and snippets.

@mbifulco

mbifulco/post.md Secret

Created December 2, 2023 22:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbifulco/daf23aed0d827ec6317962691cb9cd0d to your computer and use it in GitHub Desktop.
Save mbifulco/daf23aed0d827ec6317962691cb9cd0d to your computer and use it in GitHub Desktop.
An attempt at self-healing URLs using the next.js pages router

in addition to usability and accessibility

Self-healing URLs in Next.js with the pages router

We'll use Next.js Dynamic Routes to create a page that will display a post's content based on the url. Start by creating a file called pages/posts/[slug].tsx. The slug part of the file name is surrounded by square brackets, which tells Next.js that this is a dynamic route.

Using page lifecycle functions from Next.js pages router

We will use 3 page lifecycle functions from Next.js to create self-healing URLs: getStaticProps, getStaticPaths, and getServerSideProps. These functions are used to pre-render pages in Next.js, and they're a great way to fetch data from an API or database and pass it to a page as props.

getStaticPaths: tell Next.js which URLs to pre-render

The getStaticPaths function is used to tell Next.js which pages to pre-render. In this function, we get a list of all posts, and return an array of paths. Under the covers, Next is using this to decide which pages to statically pre-render at build time. These URLs will be the ones that we want users to see, and we'll redirect to them whenever possible if the human-readable part of the URL doesn't match.

import type { GetStaticPaths, GetStaticProps, NextPage } from "next";
import slugify from "slugify";

/**
 * Converts input to a URL-safe slug
 * @param {string} title - the title of the post
 * @returns {string} a URL-safe slug based on the post's title
 */
const titleToSlug = (title: string) => {
  const uriSlug = slugify(title, {
    lower: true, // convert everything to lower case
    trim: true, // remove leading and trailing spaces
  });

  // encode special characters like spaces and quotes to be URL-safe
  return encodeURI(uriSlug);
};

// simplified for sake of example
type Post = {
  id: string;
  title: string;
  content: string;
};

type PostPageParams = {
  slug: string;
};

type PostPageProps = {
  post: Post;
};

export const getStaticPaths: GetStaticPaths = async () => {
  // getAllPosts is an abstracted function that returns all posts for your site,
  // either from a database, or markdown files, or wherever you're storing them
  const posts = await getAllPosts();

  // iterate over the posts and create a path for each one
  // using this pattern:
  // `https://example.com/posts/${POST_TITLE}-${POST_ID}`
  const paths = posts.map((post) => ({
    params: {
      slug: `${titleToSlug(post.title)}-${post.id}`
    },
  }))

  return { paths, fallback: false }
}

getStaticProps: fetch a post from an API or database to render

The getStaticProps function is used to fetch a post from an API or database to render on the page. We'll use a getPostById function (not implemented here) to fetch the post's content from the database, and then we'll return the post as a prop. This is passed automatically to the PostPage component's props argument.

export const getStaticProps: GetStaticProps<
  PostPageProps,
  PostPageParams
> = async ({ params }) => {
  if (!params || !params.slug) {
    throw new Error('Slug not provided');
  }

  // convert the slug to a URL-safe slug
  const slug = titleToSlug(params?.slug as string);

  // remove the last part of the slug for a post's ID
  const id = slug.split("-").pop();

  try {
    // note that this function isn't implemented here,
    // but it only searches for posts by _ID!_ so the title doesn't matter
    const post = await getPostById(id);
    return {
      props: { post },
    };
  } catch (error) {
    // if the post doesn't exist, this will redirect to the 404 page
    return { notFound: true }
  }
}

// For the sake of this example, we'll use a _very_ simple template for rendering the post itself:
const PostPage: NextPage<PostPageProps> = ({post}) => {
  return (
    <div>
      <h1>Post: {post.title}</h1>
    </div>
  );
}

export default PostPage;

getServerSideProps: check the incoming URL and redirect if necessary

The getServerSideProps function is used to check the incoming URL and redirect if necessary. We'll use this function to check if the URL matches the pattern we're looking for. If it doesn't, we'll redirect to the correct URL. This is a fallback in case the getStaticPaths function doesn't catch the incorrect URL.

export const getServerSideProps: GetServerSideProps<
  PostPageProps,
  PostPageParams
> = async ({ params, res }) => {
  const { slug } = params || {};

  if (!slug) {
    // redirect to the 404 page if the slug is missing
    res.writeHead(302, { Location: "/404" });
  }



At this point, we've got the hollow shell of a page - it just show's the post's title. Next, we'll use the `getStaticProps` function to fetch the post's content. We'll use the `getPostById` function to fetch the post's content from the database, and then we'll return the post as a prop. This is passed automatically to the `PostPage` component's `props` argument.

We're going to use the package [slugify](https://www.npmjs.com/package/slugify) to create a slug from the post's title.



Next we'll use the `getStaticPaths` function to tell Next.js which pages to pre-render. We'll use an abstracted `getAllPosts` function to get a list of all the posts, and then we'll create a `paths` array that contains the `slug` for each post. The `params` object will contain the `slug` for each post, which will be used to generate the URL for each post page. We'll also set `fallback` to `false` so that Next.js will return a 404 if the `slug` doesn't match a post.

```tsx
// simplified for sake of example
type Post = {
  id: string;
  title: string;
  content: string;
};

type PostPageProps = {
  post: Post;
};

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getAllPosts()

  const paths = posts.map((post) => ({
    params: { slug: `${post.title}-${post.id}` },
  }))

  return { paths, fallback: false }
}

const PostPage: NextPage<PostPageProps> = ({post}) => {
  return (
    <div>
      <h1>Post: {post.title}</h1>
    </div>
  );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment