Chris Padilla/Blog
You can follow by RSS! (What's RSS?) Full posts list here.
- /blog
- /blog/[tag]
- /blog/[tag]/[page]
Migrating Blog Previews from SSR to SSG in Next
I've re-thought how I'm rendering my blog pages.
When I developed the site initially, I wasn't too worried about the difference between SSR and SSG. If anything, I wanted to lean on server rendering blog content so scheduled posts would load after their posting time passed.
Since then, my workflow has changed over to always publishing posts when I push the changes to GitHub. Posts are markdown files that are saved in the same repo as this site, so anytime a new post goes up, the site is rebuilt.
All that to say that I've been missing out on a juicy optimization opportunity with Next's static page generation!
So it's just as easy as switching the function name from getServerSideProps
over to getStaticProps
, right? Not quite in my case!
Page Structure
The pages that I'm looking to switch over are my blog feed pages. So that includes:
/blog is easy enough!
/blog/[tag] is the landing page for any of that tags clicked on either in a post or on my homepage. Tags could be what I have listed as "primary tags" such as "music", "art", or "tech." They could also be smaller tags, such as "React" that I have linked on individual blog pages, but that I don't list on my landing page or blog index page.
Some of those tags are now rendering content through pagination! So I have to take into account [page] numbers as well.
To get the full benefit of Static rendering with dynamic routes, I'll have to provide the routes that I want generated. Next has a fallback to server render requests that don't have a static page, so I'll exclude my smaller tags and focus in on the primary tags. I'll also want to generate the paginated links, measure how many pages need to be rendered per tag.
getStaticPaths
getStaticPaths
is the function where I'll be passing in my routes. This is added outside of the component alongside getStaticProps
to then generate the content for those routes.
For Tags Rendered as a List Page
This is pretty straightforward for tag pages. Just one level of looping involved:
export async function getStaticPaths() {
return {
paths: getBlogTagParams(),
fallback: 'blocking',
};
}
const getBlogTagParams = () => {
const tagsDisplayedAsList = ['books', 'notes', 'tech', 'art', 'music'];
return tagsDisplayedAsList.map((tag) => {
return {
params: {
tag,
},
};
});
};
For Paginated Tags
For music and art, it's one more level of looping and a few more lines of code:
export async function getStaticPaths() {
return {
paths: getBlogPageParams(),
fallback: 'blocking',
};
}
const getBlogPageParams = () => {
const allPostFields = ['title', 'date', 'hidden', 'tags'];
const allPosts = getAllPosts(allPostFields);
const publishedPosts = allPosts.filter(filterBlogPosts);
const res = [];
const fullPostPreviewTags = ['art', 'music'];
fullPostPreviewTags.forEach((tag) => {
const capitalizedTag = capitalizeFirstLetter(tag);
const regex = new RegExp(capitalizedTag, 'i');
let thisTagsPosts = publishedPosts.filter((post) =>
post.tags.some((e) => regex.test(e))
);
const count = thisTagsPosts.length;
const lastPage = Math.ceil(count / 5);
const pageNumbers = Array.from({ length: lastPage }, (_, i) =>
(i + 1).toString()
);
const tagSlug = lowercaseFirstLetter(tag);
const thisTagAndPageParams = pageNumbers.map((pageNum) => ({
params: {
tag: tagSlug,
page: pageNum,
},
}));
res.push(...thisTagAndPageParams);
});
const feedCount = publishedPosts.length;
const feedLastPage = Math.ceil(feedCount / 5);
const feedPageNumbers = Array.from({ length: feedLastPage }, (_, i) =>
(i + 1).toString()
);
const feedTagAndPageParams = feedPageNumbers.map((pageNum) => ({
params: {
tag: 'feed',
page: pageNum,
},
}));
res.push(...feedTagAndPageParams);
return res;
};
There's a fair amount of data massaging in there. The key point of note is that for each tag, I'm calculating the number of pages by dividing total posts by how many are rendered to each page:
const count = thisTagsPosts.length;
const lastPage = Math.ceil(count / 5);
const pageNumbers = Array.from({ length: lastPage }, (_, i) =>
(i + 1).toString()
);
From there, then it's a matter of piping that into the params object for getStaticPaths
Voilà! Now the site won't need to parse every blog post to render each of these preview pages! The static file will already have been generated and ready to go on request.
Floating Improv
☁️
The backing is prerecorded, but Miranda from the other room thought that I just got WAY better all of a sudden 😂
Lizalfos
Mitigating Content Layout Shift with Next Image Component
One aspect of developing my art grid was moving away from the Next image component and choosing a more flexible option with special css.
There are still plenty of spots on the site I wanted to keep using it, though! This week, I wanted to jot down why.
CLS
Content Layout Shift is something I've written about a few times. Surprisingly, this is my first touch on images, the largest culprits of creating CLS!
A page is typically peppered with images throughout. An image can be wholly optimized to be very lightweight, but can still contribute to a negative user experience if the dimensions are not accounted for.
Say you're reading this blog. And a put an image right above this paragraph. If an image FINALLY loads as you're reading this, all of a sudden this paragraph is pushed down and now you have to scroll to find your place. Ugh.
The Solution
The way to mitigate this is pretty simple: Set dimensions on your images.
Easy if you already know what they are: Use css to set explicit widths and heights. You can even use media queries to set them depending on the screen size.
It gets a little trickier if you're doing this dynamically, not knowing what dimensions your image will be. The best bet, then, is to set a container for the image to reside in , and have the image fill the container. Thinking of a div with an img child.
Next Image Component
It's not too involved to code this yourself, but Next.js comes out of the box with a component to handle this along with many other goodies. The Image Component in Next offers Size optimization for local images and lazy loading.
Here I'm using the component for my albums page
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
const MusicGrid = ({ albums }) => {
return (
<>
<section className="music_display">
{albums.map((album) => (
<article key={album.title}>
{/* <Link href={album.link}> */}
<Link href={`/${album.slug}`}>
<a data-test="musicGridLink">
<Image src={album.coverURL} width="245" height="245" />
<span>{album.title}</span>
</a>
</Link>
</article>
))}
</section>
</>
);
};
export default MusicGrid;
And here is what's generated to the DOM. It's a huge sample, but you can find a few interesting points in there:
The key pieces being the srcset
used for different sized images generated for free. These are made with externally sourced images, interestingly enough, but they're generated by the component to handle rendering specifically to the page!
<article>
<a data-test="musicGridLink" href="/forest"
><span
style="
box-sizing: border-box;
display: inline-block;
overflow: hidden;
width: initial;
height: initial;
background: none;
opacity: 1;
border: 0;
margin: 0;
padding: 0;
position: relative;
max-width: 100%;
"
><span
style="
box-sizing: border-box;
display: block;
width: initial;
height: initial;
background: none;
opacity: 1;
border: 0;
margin: 0;
padding: 0;
max-width: 100%;
"
><img
style="
display: block;
max-width: 100%;
width: initial;
height: initial;
background: none;
opacity: 1;
border: 0;
margin: 0;
padding: 0;
"
alt=""
aria-hidden="true"
src="data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20version=%271.1%27%20width=%27245%27%20height=%27245%27/%3e" /></span
><img
src="/_next/image?url=https%3A%2F%2Fres.cloudinary.com%2Fcpadilla%2Fimage%2Fupload%2Ft_optimize%2Fchrisdpadilla%2Falbums%2Fforest.jpg&w=640&q=75"
decoding="async"
data-nimg="intrinsic"
style="
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
box-sizing: border-box;
padding: 0;
border: none;
margin: auto;
display: block;
width: 0;
height: 0;
min-width: 100%;
max-width: 100%;
min-height: 100%;
max-height: 100%;
"
srcset="
/_next/image?url=https%3A%2F%2Fres.cloudinary.com%2Fcpadilla%2Fimage%2Fupload%2Ft_optimize%2Fchrisdpadilla%2Falbums%2Fforest.jpg&w=256&q=75 1x,
/_next/image?url=https%3A%2F%2Fres.cloudinary.com%2Fcpadilla%2Fimage%2Fupload%2Ft_optimize%2Fchrisdpadilla%2Falbums%2Fforest.jpg&w=640&q=75 2x
" /><noscript
><img
srcset="
/_next/image?url=https%3A%2F%2Fres.cloudinary.com%2Fcpadilla%2Fimage%2Fupload%2Ft_optimize%2Fchrisdpadilla%2Falbums%2Fforest.jpg&w=256&q=75 1x,
/_next/image?url=https%3A%2F%2Fres.cloudinary.com%2Fcpadilla%2Fimage%2Fupload%2Ft_optimize%2Fchrisdpadilla%2Falbums%2Fforest.jpg&w=640&q=75 2x
"
src="/_next/image?url=https%3A%2F%2Fres.cloudinary.com%2Fcpadilla%2Fimage%2Fupload%2Ft_optimize%2Fchrisdpadilla%2Falbums%2Fforest.jpg&w=640&q=75"
decoding="async"
data-nimg="intrinsic"
style="
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
box-sizing: border-box;
padding: 0;
border: none;
margin: auto;
display: block;
width: 0;
height: 0;
min-width: 100%;
max-width: 100%;
min-height: 100%;
max-height: 100%;
"
loading="lazy" /></noscript></span
><span>Forest</span></a
>
</article>
Swing Low
Sangin'! With Lucy!