Static server rendering with Next.js
In our current version of MaffinBlog we have a function getPosts.tsx
that retrieves all Github issues that we want to display as blog posts in our Blog. However, the function currently executes at request time and the first time that gets called is quite costly due to network latency and markdown parsing that happens in the background. We want to exploit generateStaticParams
from Next.js so all our pages are generated before-hand!
In this post we will
- ⚡ Make your Blog lightning fast by pre-rendering everything at build time.
- 🏷️ Generate tag pages for better discoverability
- ➕ Bonus point: Since our blog is now fully static, we will be deploying it in Github pages!
⚡ Static rendering of our posts
Currently we retrieve the post in our PostDetailPage
component like this:
export default async function PostDetailPage(
{ params: { slug } }: { params: { slug: string } },
): Promise<JSX.Element> {
const post = await getPost(slug);
if (post === null) {
notFound();
}
return([...]);
which currently renders at request time. To prove that, in the terminal you can run yarn build
and see that Next.js didn't render your blog page (you would see /posts/<your-slug>
if it would).
Route (app) Size First Load JS
┌ ○ / 175 B 81.7 kB
└ λ /posts/[slug] 430 B 86.9 kB
[...]
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
This is because Next.js has no information at build time of which blog post slugs exist. In order to give this information at build time, you need to use generateStaticParams
function which generate as many pages as different slugs
exist.
Note that static rendering at build time has the caveat of needing a re-deploy when you create a new issue or update one. We are okay with this as we want the speed that comes with this and we can workaround this using github hooks
This is our generateStaticParams
for our blog details page:
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function PostDetailPage(
{ params: { slug } }: { params: { slug: string } },
): Promise<JSX.Element> {
const post = await getPost(slug);
if (post === null) {
notFound();
}
return([...]);
And now, if you run yarn build
you'll see that the page generated as expected:
Route (app) Size First Load JS
┌ ○ / 175 B 81.7 kB
└ ● /posts/[slug] 430 B 86.9 kB
└ /posts/github-issues-as-nextjs-blog-cms
[...]
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
Even after doing this,
dev
mode renders your page at request time. To make sure it works you should be checkingyarn build
output.
It's important to know that generateStaticParams
as its name says it only generates static params. In your component you still have to write code to retrieve your object and parse it as expected. This helps to keep compatibility between different approaches of rendering your components.
🏷️ Tag pages
Using the approach above we can also do the same for our tags pages. Let's say we want to have something like /tags/[tag]
and then render a the list of blog posts that for that specific tag. Let's start by creating a file in src/app/tags/[tag]/page.tsx
with the contents:
import React from 'react';
import Link from 'next/link';
import { getPosts, getPostsByTag } from '@/app/api/getPosts';
export async function generateStaticParams() {
const posts = await getPosts();
const tags = new Set<string>();
posts.forEach((post) => tags.forEach((tag) => tags.add(tag)));
return Array.from(tags).map((tag) => ({
tag,
}));
}
export default async function TagPage(
{ params: { tag } }: { params: { tag: string } },
): Promise<JSX.Element> {
const posts = await getPostsByTag(tag);
return (
<>
{posts.map((post) => (
<article key={post.slug}>
<h2 className="text-2xl font-bold leading-8 tracking-tight">
<Link
href={`/posts/${post.slug}`}
className="text-gray-900 dark:text-gray-100"
>
{post.title}
</Link>
</h2>
</article>
))}
</>
);
}
And the new getPostsByTag
function:
export async function getPostsByTag(tag: string): Promise<Post[]> {
const posts = await getPosts();
return posts.filter((post) => post.tags.includes(tag));
}
Running yarn build
now will show you that it detects the new tag pages as static ones:
┌ ○ / 178 B 81.7 kB
├ ● /posts/[slug] 430 B 86.9 kB
├ ├ /posts/static-server-rendering-nextjs
├ └ /posts/github-issues-as-nextjs-blog-cms
└ ● /tags/[tag] 178 B 81.7 kB
├ /tags/next.js
├ /tags/engineering
└ /tags/octokit
➕ Deploying our static page to github pages
I want Maffin Blog infrastructure to be as simple as possible and that means no servers. Thus it means the blog has to be fully static which is what we've been working on in the previous pages. Doing this allows us to host our site in any hosting that is able to serve static content and our choice is... well Github Pages!
In order to support that, you need to add the following to next.config.js
:
images: {
unoptimized: true, // image optimization is not supported with export
},
output: 'export', // tells the compiler to generate the `out` folder with the static content
By doing this, we loose some built in functionality from Next.js. The docs about static export are really good explaining the tradeoffs.
This is all we need really, now to test it you can run again yarn build
and at the end you should see an out
folder at the root of your project that contains all the static content.
Next step is to configure Github so it hosts your blog. The first step is to add a new workflow configuration so it builds and deploys it (extracted from github pages examples for Next.js):
Let's start with adding a new workflow configuration that looks like this (extracted from github pages examples for Next.js):
# Sample workflow for building and deploying a Next.js site to GitHub Pages
#
# To get started with Next.js see: https://nextjs.org/docs/getting-started
#
name: Deploy Maffin Blog site to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["static_rendering"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.17.0]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- name: Setup Pages
uses: actions/configure-pages@v3
with:
# Automatically inject basePath in your Next.js configuration file and disable
# server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
#
# You may remove this line if you want to manage the configuration yourself.
static_site_generator: next
- name: Restore cache
uses: actions/cache@v3
with:
path: |
.next/cache
# Generate a new cache whenever packages or source files change.
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-
- name: Install dependencies
run: yarn
- name: Build with Next.js
run: yarn build
# Only needed if you are using a custom domain
- name: Set CNAME file for proper static asset loading
run: echo '<your custom domain>' > ./out/CNAME
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./out
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
Note that we are deploying the static_rendering
branch. You can update if needed and when you've proven it works, make sure to set master
. Before pushing though, you'll need to navigate to settings -> environments
in your github repo and make sure you add a new rule for your branch (deployment branches).
After this, you can push your branch and you'll see the workflow in action:
Once succeeded visit the URL and you should see your blog!
💚 Conclusion
This post demonstrates how versatile Nextjs is by allowing us to just generate a bunch of static files to be hosted wherever. Not only this but we can still have dynamic behavior client side if needed. Also, Github is awesome and makes it very easy to deploy with workflows as you can see!
Feel free to navigate the code freely in here. It refers to the commit I used to ship the changes described here and you can find unit tests for the functionality added too and it adds some extra code for formatting the tag pages with Tailwind plus some refactors to Post list pages.
That's it! If you enjoyed the post feel free to react to it in the Github tracker (for now).