I switched all my projects to Monorepos this year, and I use Cloudlfare Pages intensively for hosting static websites. There is one small problem with Cloudlfare’s Github integration: you can only connect one project per repository. In a monorepo where I provide pages like a landing page, documentation and an app, this is a problem.
It’s good that you can also upload the assets directly. The problem with that: you lose some nice benefits:
- Stable preview URL
- PR comment with the links
And those are already quite nice benefits ;) So I started to rebuild the benefits myself. With the help of Wrangler and the Cloudflare API it is not difficult to achieve everything.
To get a stable URL I originally assumed that I would just get an updated stable URL with the help of the branch name.
npm i -g wrangler
cd ${{ env.ROOT_DIRECTORY }}
CF_PUBLISH_OUTPUT=$(wrangler pages deploy ${{ env.DIST_DIRECTORY }} --project-name=${{ env.CLOUDFLARE_PAGES_PROJECT_NAME }} --branch="${{ steps.extract_branch.outputs.branch }}" --commit-dirty=true --commit-hash=${{ steps.meta.outputs.sha_short }} | grep complete)
echo "cf_deployments=$CF_PUBLISH_OUTPUT" >> "$GITHUB_OUTPUT"
Unfortunately, after a few test runs, I found that this is not the case. I didn’t deal with it further at this point, but tried to take an alternative approach:
- Search all deployments to a branch on every run.
- Delete all deployments
- Upload new assets
For reading and deleting deployments I wrote a small TypesScript program that I run in the CI pipeline.
Read out all previous branch deployments:
public async getDeployments(options?: { branch?: string }) {
const { branch } = options || {}
const { accountId, projectName, apiToken } = this.config
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/deployments`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
},
},
).then((res) => res.json())
let deployments = response.result
if (branch) {
deployments = deployments.filter(
(deployment) =>
deployment.deployment_trigger?.metadata?.branch === branch,
)
}
return deployments
}
Deleting a deployment:
public async deleteDeployment(id: string) {
const { accountId, projectName, apiToken } = this.config
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/deployments/${id}?force=true`,
{
method: 'DELETE',
headers: {
Authorization: `Bearer ${apiToken}`,
},
},
).then((res) => res.json())
}
The approach has another advantage: deployments that are no longer current are always cleaned up, since I am no longer interested in them anyway.

Adam Urban is fullstack engineer, loves serverless and generative art, and is building side projects in his free time. Latest projects are flethy.com, ethme.at and diypunks.xyz.