Skip to content

fix: proxy Notion images to avoid expired S3 URLs#72

Merged
nicolasrouanne merged 1 commit into
mainfrom
fix/notion-image-proxy
May 12, 2026
Merged

fix: proxy Notion images to avoid expired S3 URLs#72
nicolasrouanne merged 1 commit into
mainfrom
fix/notion-image-proxy

Conversation

@nicolasrouanne
Copy link
Copy Markdown
Member

Summary

  • Nouvelle route app/api/notion-image/route.ts qui résout une URL S3 fraîche via notion.blocks.retrieve({ block_id }) et stream le binaire avec Cache-Control: public, s-maxage=86400, stale-while-revalidate=604800.
  • NotionRenderer pointe les images Notion-hosted (type file) sur cette route au lieu de l'URL S3 signée. Les images external (Unsplash, etc.) restent directes.
  • lib/notion.ts : notion client exporté pour réutilisation ; databaseId rendu lazy pour ne plus crasher au load du module dans la route.

Pourquoi

Les URLs S3 signées de Notion expirent au bout d'1h (X-Amz-Expires=3600). Après expiration, S3 répond 403 + Content-Type: application/xml → Chrome bloque (ORB/CORB). Avec cacheLife("max") sur fetchArticleById, l'URL morte est gelée et servie à tous les visiteurs (cf. https://qraft.tech/blog/350218ed-56d7-814c-88a2-def1a2800fd2). Aucun CDN ne peut intervenir parce que le domaine S3 est externe.

En proxifiant via qraft.tech, l'URL devient stable → Cloudflare + Vercel cachent l'image à l'edge ; un seul appel Notion API par image par jour au pire.

Closes #71

Test plan

  • Vérifier que les images du blog s'affichent après merge sur l'article cassé
  • Vérifier qu'une image external (si présente dans un article) charge toujours
  • cf-cache-status au 2ème hit de /api/notion-image?blockId=… (sera DYNAMIC tant qu'aucune Cache Rule Cloudflare n'est ajoutée — pas bloquant, le cache Vercel s'occupe déjà du gros)
  • ?blockId=foo → 400, ?blockId=<UUID inexistant> → 404
  • (Optionnel) Ajouter la Cache Rule Cloudflare pour /api/notion-image*

🤖 Generated with Claude Code

…3 URLs

Notion serves block images via pre-signed S3 URLs with X-Amz-Expires=3600.
After one hour the URL returns 403 + Content-Type: application/xml, which
Chrome blocks via ORB (net::ERR_BLOCKED_BY_ORB). Since fetchArticleById
uses "use cache" + cacheLife("max"), the dead URL is frozen and served to
every visitor. The S3 host is external so neither Cloudflare nor Vercel
can cache it either.

The new route resolves a fresh signed URL via notion.blocks.retrieve at
request time and streams the image with Cache-Control: public, s-maxage,
keeping the URL stable on qraft.tech so both CDNs cache it at the edge.

Closes #71

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
website Ready Ready Preview May 12, 2026 4:22pm

Request Review

Copy link
Copy Markdown
Member Author

@nicolasrouanne nicolasrouanne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nicolasrouanne nicolasrouanne merged commit 71a16d6 into main May 12, 2026
2 checks passed
@nicolasrouanne nicolasrouanne deleted the fix/notion-image-proxy branch May 12, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blog: les images Notion expirent après 1h (URLs S3 signées)

1 participant