xpcoffee icon
This is my site. Please treat it gently. ❤

NextJs + S3

A collection of notes for issues, workarounds, and solutions I've found building emerickbosch.com as a static site using S3 website hosting and NextJs.

See actual source code on the Github repo.

.html files not matching paths

Problem

Paths are like /article/foo, but the actual html is /article/foo.html. When navigating to /article/foo in the bucket, the file is not found.

Solution

Drop the .html from the S3 object key to make it match the path exactly.

deploy.tsx
/**
 * Given a relative filename, returns the S3 object key.
 */
function removeHtmlSuffix(fileName: string) {
  // root files that bucket hosting points to
  if (fileName.endsWith("index.html") || fileName.endsWith("404.html")) {
    return fileName;
  }
  if (fileName.endsWith(".html")) {
    return fileName.substring(0, fileName.length - ".html".length);
  }
  return fileName;
}

When uploading, it's important to tell S3 what the content-type of the file is.

deploy.tsx
import mime from "mime-types";
import { S3 } from "@aws-sdk/client-s3";

const DIST_FOLDER = "/your/folder/with/static/website";

async function upload(filePath) {
    const key = removeHtmlSuffix(path.relative(DIST_FOLDER, filePath));
    const opts = {
      Key: key,
      Bucket: config.bucketName,
      Body: createReadStream(filePath),
      ContentType: mime.lookup(filePath) || undefined,
    };
    const s3 = new S3(/* your S3 config */);
    await s3.putObject(opts);
}

Permalinks

Problem

I want to have RSS feeds which point to articles using permalinks. I also want to be able change my titles and slugs (my notes are living documents).

So I cannot count on slugs as permalinks; I need another path.

Solution

Add UIDs to the frontmatter of my articles.

/articles/myarticle.mdx
---
title: NextJs + S3 
guid: a006ac19-d6f4-498a-8210-51188fabe328
---

Use those UIDs as slugs when generating static pages.

/src/app/articles/[slug]/page.tsx
export const generateStaticParams = async () => {
  const slugs = [];
  for (const metadata of getMarkdownMetadata()) {
    slugs.push({ slug: metadata.slug });
    slugs.push({ slug: metadata.id }); // permalinks for each article; allows titles to change
  }
  return slugs;
};

Redirect to the real articles in those pages.

/src/app/articles/[slug]/page.tsx
const Article = ({ params }: Props) => {
  const slug = params.slug;

  const permalinkedArticle = getMarkdownMetadata().find(
    ({ id }) => slug === id,
  );
  if (permalinkedArticle) {
    return (
      <ArticleRedirect articlePath={`/articles/${permalinkedArticle.slug}`} />
    );
  }
  
  // the rest of the component
}

Code blocks

  • rehype-prism-plus: syntax, line numbers, line highlighting
  • rehype-code-titles: code block titles

Notes:

  • rehype-mdx-code-props is not compatible with rehype-prism-plus.