Skip to content

Cloudflare Pages

Cloudflare Pages has two kinds of project, and they connect differently.

Your project How to tell Use
Connected to Git (Cloudflare builds it) Settings → Builds shows a repository and Deploy hooks A deploy hook, below
Direct upload (uploaded by wrangler pages deploy, often from CI) No Deploy hooks menu GitHub Actions, or read when visited (below)
  1. Settings → Variables and secrets → Add, for Production:
    • JOOGOOS_FEED_URL as Text.
    • JOOGOOS_FEED_KEY as Secret.
  2. Read the feed during the build (same code as Netlify).
  3. Settings → Builds → Deploy hooks → Add deploy hook: name Joogoos, branch main. Copy the address (https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/…).
  4. The owner pastes it in Joogoos: Business → SEO → Where they are published → Build hook → Save.
  5. Retry the latest deployment once: Pages reads variables and secrets only from the next deployment.

A Pages Function can serve the blog itself, reading the feed when a page is visited and keeping it five minutes. There is no build hook and no rebuild: a new article appears within five minutes. This is how joogoos.com’s own blog works.

  1. Add JOOGOOS_FEED_KEY as a Secret (and JOOGOOS_FEED_URL as Text) under Settings → Variables and secrets.
  2. Add functions/blog/[[path]].js (below), and deploy once.
  3. Open /blog. After the first read, the card in Joogoos turns green.
functions/blog/[[path]].js
export async function onRequestGet({ request, env }) {
const res = await fetch(env.JOOGOOS_FEED_URL, {
headers: { authorization: `Bearer ${env.JOOGOOS_FEED_KEY}` },
cf: { cacheTtl: 300, cacheEverything: true },
});
const feed = res.ok ? await res.json() : { entries: [], redirects: [], gone: [] };
const path = new URL(request.url).pathname.replace(/\/+$/, '');
const moved = feed.redirects.find((r) => r.from === path);
if (moved) return Response.redirect(new URL(moved.to, request.url), 301);
if (path === '/blog') return new Response(renderList(feed.entries), { headers: { 'content-type': 'text/html' } });
const entry = feed.entries.find((e) => e.path === path);
if (entry) return new Response(renderArticle(entry), { headers: { 'content-type': 'text/html' } });
return new Response('Not found', { status: feed.gone.some((g) => g.path === path) ? 410 : 404 });
}

renderList and renderArticle are yours: escape every field, and render entry.body with a Markdown renderer. A function’s responses do not get the headers in _headers, so set your security headers on them too.

Keep the last good copy. Put every good feed into the Cache API under a fixed key and serve it whenever a read fails, so the blog never says “no articles” while Joogoos is unreachable or a key is being replaced:

const LAST_GOOD = 'https://your-site.example/__joogoos-feed-last-good';
async function feedOf(env) {
const cache = caches.default;
const old = async () => (await cache.match(LAST_GOOD))?.json() ?? null;
try {
const res = await fetch(env.JOOGOOS_FEED_URL, {
headers: { authorization: `Bearer ${env.JOOGOOS_FEED_KEY}` },
cf: { cacheTtl: 300, cacheEverything: true },
});
if (!res.ok) return old();
const feed = await res.json();
await cache.put(LAST_GOOD, new Response(JSON.stringify(feed), { headers: { 'cache-control': 'max-age=2592000' } }));
return feed;
} catch { return old(); }
}

And serve /blog/sitemap.xml from the feed’s entries (each url and modifiedAt), listed in robots.txt.