Removed pages can't be deleted, but served with a 404 instead. One has to use
The below example uses entry id's to build the paths, e.g. example.com/post/90803230238
. For non-id path's, e.g. with a custom field example.com/post/how-to-make-icecream
, please read the note at the end of this answer.
A pre-requisite for this is that getStaticPaths
and getStaticProps
are configured accordingly.
// /pages/blog/[...post].jsx
function Post({ postData }) {
return (
// ...
);
}
// Only runs once during build
export async function getStaticPaths() {
const res = await fetch("https://.../posts");
const posts = await res.json();
// Prerender paths during build
const paths = posts.map((post) => ({
params: { post: post.id },
}));
return {
paths,
fallback: "blocking" // must be "blocking"
};
}
// Runs during build and every revalidation
export async function getStaticProps(context) {
const res = await fetch(`https://.../posts/${context.params.post}`);
const postData = await res.json();
// Check if the page exists in the CMS using the API response
if(!postData){
// The page doesn't exist in the CMS, return notFound to trigger a 404
return{
notFound: true,
// Pick one of the following
revalidate: 30, // <- ISR, interval in seconds between revalidations
revalidate: false // On-demand
}
}
// Return page props
return {
props: {
postData,
},
// Pick one of the following
revalidate: 30, // <- ISR, interval in seconds between revalidations
revalidate: false // On-demand
};
}
The main takeaway from the above code is:
- You can pre-render paths in
getStaticPaths
which only runs during the initial build
getStaticPaths
must use "blocking"
as the fallback
value
getStaticProps
which runs during the initial build and every revalidation, should ask with an API request each time for the page status (Published, Unpublished, ...)
- If the page is active, return the page props
- If the page is inactive/deleted, return a notFound:true
, falling back to the ./pages/404.jsx
page
On-demand revalidation needs an additional file, the API endpoint where the webhook can send notifications to.
// /pages/api/revalidate-post.js
export async function handler(req, res) {
try {
const reqBody = JSON.parse(req.body);
await res.revalidate(`/post/${reqBody.sys.id}`); // revalidate post using entry id
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send("Error revalidating");
}
}
The res.revalidate(``/${reqBody.path}``)
invokes a fresh evaluation of the page using the logic from getStaticProps
.
Now, if somebody would delete the page in the CMS, and trigger the above revalidation webhook handler for the deleted page path, then the path serves a 404 page instead of the original page.
The page itself, however, is not deleted from the disk.
Revalidating using non-id, custom field values for paths
In Contentful, during an "Unpublish" webhook notification, only the unpublished entry ID and a type: "DeletedEntry"
are present in the webhook response body. All other field values, e.g. the path
required to trigger the revalidation, are not available. Here, we have to make an additional request to fetch the inactive's post field values.
Contentful provides the Content Preview API, which allows you to fetch data even from inactive entries.
Heres the pseudocode for the webhook API endpoint, when using custom path values other than the ID:
export async function handler(req, res) {
try {
const reqBody = JSON.parse(req.body);
let pageToRevalidate = "";
if (reqBody.sys.type === "DeletedEntry") {
/**
* "Unpublished" (type = DeletedEntry) status entries in
* Contentful are not available on the normal API endpoints. Here, we
* have to make an additional API request to the Preview API service,
* asking for data for the entry with id reqBody.sys.id.
*/
const unpublishedRes = await fetch(
`https://previewAPI.../posts/${context.params.post}`
);
const unpublishedData = await unpublishedRes.json();
pageToRevalidate = `/${unpublishedData.fields.slug}`;
} else {
pageToRevalidate = `/${reqBody.fields.slug}`;
}
await res.revalidate(pageToRevalidate);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send("Error revalidating");
}
}