cache queries and revalidate with incremental static regeneration

This commit is contained in:
2026-07-07 02:32:21 +02:00
parent d63b14b2e5
commit e600d0a663
22 changed files with 381 additions and 114 deletions
+3
View File
@@ -6,3 +6,6 @@ class DnsCmsConfig(AppConfig):
def ready(self):
from dnscms import signals # noqa: F401
from dnscms.revalidation import register_signal_handlers
register_signal_handlers()
+97
View File
@@ -0,0 +1,97 @@
"""Purge the frontend cache when content changes.
The frontend caches all GraphQL responses under a single "cms" cache tag;
any content change POSTs to its /api/revalidate endpoint for a global purge.
"""
import logging
import threading
import urllib.request
from django.conf import settings
from django.db import transaction
from django.db.models.signals import post_delete, post_save
logger = logging.getLogger(__name__)
WEBHOOK_TIMEOUT_SECONDS = 5
# Non-page models exposed over GraphQL; pages are covered by wagtail signals
REVALIDATING_MODELS = [
"contacts.ContactEntity",
"events.EventCategory",
"events.EventOrganizer",
"images.CustomImage",
"openinghours.OpeningHoursSet",
]
_warned_unconfigured = False
def _send_webhook(url, secret, reason):
request = urllib.request.Request(
url, data=b"", method="POST", headers={"X-Revalidate-Secret": secret}
)
try:
with urllib.request.urlopen(request, timeout=WEBHOOK_TIMEOUT_SECONDS) as response:
logger.info("frontend revalidation ok (%s): HTTP %s", reason, response.status)
except Exception:
logger.exception("frontend revalidation failed (%s)", reason)
def trigger_frontend_revalidation(reason):
global _warned_unconfigured
secret = settings.REVALIDATE_WEBHOOK_SECRET
if not secret:
if not _warned_unconfigured:
logger.warning("REVALIDATE_WEBHOOK_SECRET is not set, skipping frontend revalidation")
_warned_unconfigured = True
return
url = f"{settings.FRONTEND_BASE_URL}/api/revalidate"
# After commit so the frontend can't re-render from pre-commit data,
# in a thread so publishing never blocks on the frontend
transaction.on_commit(
lambda: threading.Thread(
target=_send_webhook, args=(url, secret, reason), daemon=True
).start()
)
def _on_page_published(sender, instance, **kwargs):
trigger_frontend_revalidation(f"page_published: {instance.slug}")
def _on_page_unpublished(sender, instance, **kwargs):
trigger_frontend_revalidation(f"page_unpublished: {instance.slug}")
def _on_page_moved(sender, instance, **kwargs):
trigger_frontend_revalidation(f"post_page_move: {instance.slug}")
def _on_page_deleted(sender, instance, **kwargs):
trigger_frontend_revalidation(f"page_deleted: {instance.slug}")
def _on_model_changed(sender, instance, **kwargs):
trigger_frontend_revalidation(f"{sender._meta.label}: {instance}")
def register_signal_handlers():
from wagtail.models import Page
from wagtail.signals import page_published, page_unpublished, post_page_move
page_published.connect(_on_page_published, dispatch_uid="revalidation.page_published")
page_unpublished.connect(_on_page_unpublished, dispatch_uid="revalidation.page_unpublished")
post_page_move.connect(_on_page_moved, dispatch_uid="revalidation.post_page_move")
# Wagtail sends no signal on page deletion; MTI deletes of any page type
# also emit post_delete for the base Page row
post_delete.connect(_on_page_deleted, sender=Page, dispatch_uid="revalidation.page_deleted")
for label in REVALIDATING_MODELS:
post_save.connect(
_on_model_changed, sender=label, dispatch_uid=f"revalidation.save.{label}"
)
post_delete.connect(
_on_model_changed, sender=label, dispatch_uid=f"revalidation.delete.{label}"
)
+4
View File
@@ -200,6 +200,10 @@ BASE_URL = WAGTAIL_BASE_URL
# redirect "View Live" clicks on the CMS host over to the headless frontend.
FRONTEND_BASE_URL = os.environ.get("FRONTEND_BASE_URL", "http://localhost:3000").rstrip("/")
# Shared secret for the frontend cache purge webhook (see dnscms/revalidation.py).
# Unset means no webhooks are sent.
REVALIDATE_WEBHOOK_SECRET = os.environ.get("REVALIDATE_WEBHOOK_SECRET", "")
WAGTAIL_HEADLESS_PREVIEW = {
"CLIENT_URLS": {"default": f"{FRONTEND_BASE_URL}/api/preview"},
"SERVE_BASE_URL": FRONTEND_BASE_URL,