Published September 07, 2024 (7 months ago) 4 min read
This post continues a series where I discuss how to create a personal blog using Next.js and Sanity. If you haven't set up Sanity in a Next.js project yet, check out my previous guide.
If you've already integrated Sanity, let's dive into how to fetch and display blogs you've created in the embedded Sanity Studio on your website.
Since we're using Next.js' App Router, you only need to create the file src/app/blogs/page.tsx
and export a default component. Here's an example:
tsx
1// src/app/blogs/page.tsx
2
3export default function BlogsPage() {
4 return <div>This is the blogs page</div>;
5}
After creating this file, start your local server and visit http://localhost:3000/blogs to view your new page (assuming your server runs on http://localhost:3000).
Before displaying the blogs, we need to fetch them from Sanity's Content Lake.
Sanity uses a query language called GROQ (Graph-Relational Object Queries) to customize fetch request parameters. You can learn more about GROQ in their documentation.
Let’s create our query. Start by making a new file at src/sanity/lib/queries.ts
and add the following code:
typescript
1import { defineQuery } from "next-sanity";
2
3// gets all posts with slugs
4export const POSTS_QUERY =
5 defineQuery(`*[_type == "post" && defined(slug.current)]{
6 _id,
7 body,
8 slug,
9 title,
10 categories[]->{
11 slug,
12 title
13 },
14 publishedAt,
15 _createdAt,
16 mainImage{
17 asset->{
18 url,
19 metadata{
20 dimensions,
21 lqip
22 }
23 }
24 }
25 } | order(_createdAt desc)`);
26
You can test and refine your queries using the GROQ playground at http://localhost:3000/studio/vision.
To fetch blogs, we'll use the Sanity Client located at src/sanity/lib/client.ts
, which next-sanity
configured during setup. Use the fetch
method from the client, passing the query as an argument. Here’s how to retrieve all blog posts:
tsx
1import { client } from "@/sanity/lib/client";
2import { POSTS_QUERY } from "@/sanity/lib/queries";
3
4export default async function BlogsPage() {
5 const blogs = await client.fetch(POSTS_QUERY);
6
7 return <div>this is the blogs page</div>;
8}
Sanity Client supports all Next.js options for fetch
caching and revalidation. Here’s how to implement caching and revalidation when fetching blogs:
tsx
1import { client } from "@/sanity/lib/client";
2import { POSTS_QUERY } from "@/sanity/lib/queries";
3import { POSTS_QUERYResult } from "../../../sanity.types";
4
5export default async function BlogsPage() {
6 const blogs = await client.fetch<POSTS_QUERYResult>(
7 POSTS_QUERY,
8 {},
9 {
10 next: {
11 // revalidate every 60 seconds
12 revalidate: 60,
13
14 // or supply the tags so we can have on-demand revalidation
15 // by calling `revalidateTag('blogs')`
16 // tags: ["blogs"],
17 },
18 },
19 );
20
21 return <div>this is the blogs page</div>;
22}
Let’s create a helper function to encapsulate the logic for fetching data. In src/sanity/lib/client.ts
, add the following code:
typescript
1import { createClient } from "next-sanity";
2
3import { apiVersion, dataset, projectId } from "../env";
4import { QueryParams } from "sanity";
5
6export const client = createClient({
7 projectId,
8 dataset,
9 apiVersion,
10 useCdn: true,
11});
12
13export async function sanityFetch<T = any>({
14 query,
15 params = {},
16 revalidate = process.env.NODE_ENV === "development" ? 10 : 60 * 5, // default revalidation time in seconds
17 tags = [],
18}:
19 query: string;
20 params?: QueryParams;
21 revalidate?: number | false;
22 tags?: string[];
23}) {
24 return client.fetch<T>(query, params, {
25 next: {
26 revalidate: tags.length ? false : revalidate, // for simple, time-based revalidation
27 tags, // for tag-based revalidation
28 },
29 });
30}
Let’s update our code to retrieve the blogs:
tsx
1import { sanityFetch } from "@/sanity/lib/client";
2import { POSTS_QUERY } from "@/sanity/lib/queries";
3import { POSTS_QUERYResult } from "../../../sanity.types";
4
5export default async function BlogsPage() {
6 const blogs = await sanityFetch({
7 query: POSTS_QUERY
8 });
9
10 return <div>this is the blogs page</div>;
11}
Currently, we lack type inference for the return value of sanityFetch
—it returns a value of type any
, which isn't very helpful. Fortunately, Sanity TypeGen can generate types based on our defined queries and the schema in src/sanity/schemaTypes
. To generate these types:
sanity-typegen.json
file at your project's root and add the following content to configure Sanity TypeGen:json
1{
2 "path": "./src/**/*.{ts,tsx,js,jsx}",
3 "schema": "./src/sanity/extract.json",
4 "generates": "./src/sanity/types.ts"
5}
sh
1npx sanity@latest schema extract
sh
1npx sanity@latest typegen generate
Since you'll need to run these commands whenever your schema or queries update, it's sensible to add them to the scripts section in your package.json
:
json
1"scripts": {
2 ...,
3 "predev": "npm run typegen",
4 "prebuild": "npm run typegen",
5 "typegen": "sanity schema extract --path=src/sanity/extract.json && sanity typegen generate"
6},
Now you can generate types by simply running:
sh
1npm run typegen
This will create a sanity.types.ts
file with the generated types in your project's root directory.
Sanity TypeGen generates a {name-of-query}Result
type for each defined query. For example, the result type for our POSTS_QUERY
will be POSTS_QUERYResult
. We can use this to assert the return type of when retrieving the blogs, like so:
tsx
1import { sanityFetch } from "@/sanity/lib/client";
2import { POSTS_QUERY } from "@/sanity/lib/queries";
3import { POSTS_QUERYResult } from "../../../sanity.types";
4
5export default async function BlogsPage() {
6 const blogs = await sanityFetch<POSTS_QUERYResult>({
7 query: POSTS_QUERY
8 });
9
10 return <div>this is the blogs page</div>;
11}
Now for the exciting part: displaying the blogs we created in Sanity Studio on our website. While the specific implementation is up to you—I won't delve into styles or component creation—I'll highlight some key considerations and features you might want to incorporate as you showcase your blogs.
javascript
1// next.config.mjs
2
3/** @type {import('next').NextConfig} */
4const nextConfig = {
5 images: {
6 remotePatterns: [
7 {
8 protocol: "https",
9 hostname: "cdn.sanity.io",
10 },
11 ],
12 },
13};
14
15export default nextConfig;
urlFor
function located in src/sanity/lib/image.ts
to construct the image URL to be passed into the src prop when using Next.js’ <Image />
component. You can refer to Sanity’s documentation for more information. Here’s an example:tsx
1import Image from "next/image";
2import { urlFor } from "@/sanity/lib/image";
3
4<Image
5 src={urlFor(blog.mainImage).width(100).height(62).url()}
6 alt={blog.title || ""}
7 width={100}
8 height={62}
9 />
lqip
(Low Quality Image Placeholders) attribute as an image placeholder while your image loads.lqip
is included in your post query.typescript
1import { defineQuery } from "next-sanity";
2
3// gets all posts with slugs
4export const POSTS_QUERY =
5 defineQuery(`*[_type == "post" && defined(slug.current)]{
6 _id,
7 body,
8 slug,
9 title,
10 categories[]->{
11 slug,
12 title
13 },
14 publishedAt,
15 _createdAt,
16 mainImage{
17 asset->{
18 url,
19 metadata{
20 dimensions,
21 lqip
22 }
23 }
24 }
25 } | order(_createdAt desc)`);
26
"blur"
as your image placeholder and supply the image’s lqip
in the blurDataURL
prop like so:tsx
1import Image from "next/image";
2import { urlFor } from "@/sanity/lib/image";
3
4<Image
5 src={urlFor(blog.mainImage).width(100).height(62).url()}
6 alt={blog.title || ""}
7 width={100}
8 height={62}
9 placeholder="blur"
10 blurDataURL={blog.mainImage.asset?.metadata?.lqip || ""}
11 />
The dates returned when retrieving the blogs from Sanity Content Lake are in ISO format by default. You can use a library called date-fns to format the dates so they are human-friendly. For instance, you can do something like this:
tsx
1 import { formatDistanceToNow, format } from "date-fns";
2
3 // `blog` here is an item from your POSTS_QUERYResult
4
5 const date = blog.publishedAt || blog._createdAt
6
7 // formats the date into something like: September 19, 2024
8 const formattedDate = format(new Date(date), "MMMM dd, yyyy")
9
10 // formats the date into phrases like: "about a minute ago" or "about 2 months ago"
11 // depending on how long the date is from the current date
12 const timeAgoInWords = `${formatDistanceToNow(new Date(date))} ago`
You will often see on blogs an estimate of how long it might take a user to read the entire post. Fortunately, there is a library for this: reading-time-estimator. Here’s an example of how to use it:
tsx
1import { PortableTextBlock, toPlainText } from "next-sanity";
2import { readingTime } from "reading-time-estimator";
3
4// `blog` here is an item from your POSTS_QUERYResult
5
6const readTime = readingTime(
7 // `toPlainText` converts `blog.body` into...well, plain text.
8 toPlainText(blog.body as unknown as PortableTextBlock),
9 200, // <----- average words per minute
10);
11
12console.log(readTime.text) // "3 minute read"
You can check out the source code for a simple Sanity blog demo I made and take some inspiration from the component that renders each blog item.
Up to this point, we are relying on time-based revalidation for our blogs, meaning when we create or modify documents from Sanity Studio, it will not immediately reflect on our website unless we set the revalidation time to 0.
In the next blog, I will discuss how to set up tag-based revalidation and Sanity webhooks so our website content is immediately updated whenever we modify documents from Sanity Studio.