I have the same issue. I use a webhook to revalidate pages in next.js. Each page has an UID which is used to construct the page URL. Because I couldn't figure out the UID on unpublish, I had to resort to storing page data in a database. I query the database by document ID and it returns the UID. This way I can figure out which UID/URL to revalidate on unpublish. (there are some free database providers, I used https://www.cockroachlabs.com)
Some suggestions that would solve this problem on Prismics side:
- Allow to send (all) document data or document UID to the webhook
- Allow to delay unpublish for x seconds after the webhook has been called
If anyone is interested in a quick and dirty example solution for the problem I have (use at your own risk):
Summary
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Client } from 'pg';
import { linkResolver, createClient } from '~/prismicio';
/**
* TODO: THIS IS A HACK:
* ? remove postgres logics when prismic allows
* ? to figure out links/data/uid of unpublished pages
*/
const storeDocumentData = async (
updatedData: { uid: string; id: string }[],
) => {
const pgClient = new Client(process.env.REVALIDATE_DATABASE);
await pgClient.connect();
await pgClient.query(`
UPDATE storedDocuments SET data='${JSON.stringify(updatedData)}'
WHERE ID = (SELECT ID FROM storedDocuments ORDER BY ID LIMIT 1);
`);
await pgClient.end();
};
const getStoredDocumentData = async () => {
const pgClient = new Client(process.env.REVALIDATE_DATABASE);
await pgClient.connect();
const storedDataString = (
await pgClient.query(
`SELECT data
FROM storedDocuments
ORDER BY ID LIMIT 1;
`,
)
).rows[0].data;
await pgClient.end();
return JSON.parse(storedDataString) as {
uid: string;
id: string;
}[];
};
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Check for secret to confirm this is a valid request
if (req.body.secret !== process.env.REVALIDATE_SECRET) {
return res.status(401).json({ message: 'Unauthorized' });
}
try {
const client = createClient();
const storedDocumentData = await getStoredDocumentData();
const documents = await client.getAllByIDs(req.body.documents);
const revalidatePromises = req.body.documents.flatMap(
async (id: string) => {
const foundUpdatedDocumentIndex = documents.findIndex(
(doc) => doc.id === id,
);
const foundUpdatedDocument = documents[foundUpdatedDocumentIndex];
const storedDocumentIndex = storedDocumentData.findIndex(
(doc) => doc.id === id,
);
const storedDocument = storedDocumentData[storedDocumentIndex];
if (
storedDocument?.uid &&
foundUpdatedDocument?.uid &&
storedDocument.uid !== foundUpdatedDocument.uid
) {
// uid has changed (uid is used as URL)
const updatedData = [...storedDocumentData];
storeDocumentData(updatedData);
updatedData[storedDocumentIndex].uid = foundUpdatedDocument.uid;
return [
res.revalidate(
linkResolver({ type: 'page', uid: storedDocument.uid }),
),
res.revalidate(
linkResolver({ type: 'page', uid: foundUpdatedDocument.uid }),
),
];
}
if (foundUpdatedDocument?.uid) {
if (!storedDocument) {
// new document
storeDocumentData([
...storedDocumentData,
{
id: foundUpdatedDocument.id,
uid: foundUpdatedDocument.uid,
},
]);
}
return res.revalidate(
linkResolver({ type: 'page', uid: foundUpdatedDocument.uid }),
);
}
if (storedDocument) {
// found no updated document so the page is unpublished
const updatedData = [...storedDocumentData];
delete updatedData[storedDocumentIndex];
storeDocumentData(updatedData);
return res.revalidate(
linkResolver({ type: 'page', uid: storedDocument.uid }),
);
}
throw Error('Revalidate bug');
},
);
await Promise.all(revalidatePromises);
return res.status(200).send('OK');
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
return res.status(500).send('Error revalidating');
}
};
export default handler;
// revalidate.js
const { createClient } = require('@prismicio/client');
const { Client } = require('pg');
const smConfig = require('./sm.json');
const fetch = (...args) =>
import('node-fetch').then(({ default: fetch }) => fetch(...args));
global.fetch = fetch;
const cleanPageData = (pageData) => ({
uid: pageData.uid,
id: pageData.id,
});
const insertInitialPageData = async () => {
if (!process.env.REVALIDATE_DATABASE) return;
const client = createClient(smConfig.apiEndpoint);
const pgClient = new Client(process.env.REVALIDATE_DATABASE);
await pgClient.connect();
await pgClient.query(`
DROP TABLE storedDocuments;
`);
await pgClient.query(`
CREATE TABLE storedDocuments (
id serial PRIMARY KEY,
data TEXT
);
`);
const documents = await client
.getAllByType('page')
.then((docs) => docs.map(cleanPageData));
await pgClient.query(`
INSERT INTO storedDocuments(data)
VALUES ('${JSON.stringify(documents || [])}');
`);
await pgClient.end();
};
insertInitialPageData();
// update your build command e.g.
next build && node ./revalidate.js
Don't forget to return { notFound: true }
in your dynamic next pages
interface Params extends ParsedUrlQuery {
uid: string[];
}
export const getStaticProps: GetStaticProps<{}, Params> = async ({ previewData, params }) => {
const client = createClient({ previewData });
const asyncPage = client
.getByUID('page', params?.uid?.[0] || '')
.catch((err: Error) => {
if (err.message.includes('No documents were returned')) {
return null;
}
throw Error(err.message);
});
const asyncGlobalData = fetchGlobalData(client);
const [page, globalData] = await Promise.all([asyncPage, asyncGlobalData]);
return page
? {
props: { page, globalData },
}
: { notFound: true };
};
export const getStaticPaths: GetStaticPaths = async () => {
const client = createClient();
const documents = await client.getAllByType('page');
return {
paths: documents.map((doc) => prismicH.asLink(doc, linkResolver)),
fallback: 'blocking',
};
};