From e600d0a663148a0a08ea7c0d3140dee88ab90a24 Mon Sep 17 00:00:00 2001 From: Jonas Braathen Date: Tue, 7 Jul 2026 02:32:21 +0200 Subject: [PATCH] cache queries and revalidate with incremental static regeneration --- README.md | 15 +++ dnscms/dnscms/apps.py | 3 + dnscms/dnscms/revalidation.py | 97 ++++++++++++++ dnscms/dnscms/settings/base.py | 4 + dnscms/tests/test_revalidation.py | 127 +++++++++++++++++++ web/package.json | 1 + web/src/app/[...url]/page.tsx | 2 - web/src/app/aktuelt/[slug]/page.tsx | 26 +--- web/src/app/api/revalidate/route.ts | 28 ++++ web/src/app/api/v1/events/route.ts | 7 +- web/src/app/arrangementer/[slug]/page.tsx | 26 +--- web/src/app/client.ts | 7 +- web/src/app/preview/render/page.tsx | 7 +- web/src/app/sok/page.tsx | 3 +- web/src/components/events/EventIndexView.tsx | 7 +- web/src/components/home/HomePageView.tsx | 7 +- web/src/gql/gql.ts | 12 -- web/src/gql/graphql.ts | 44 ------- web/src/lib/openinghours.ts | 9 +- web/src/lib/revalidation.test.ts | 24 ++++ web/src/lib/revalidation.ts | 38 ++++++ web/tsconfig.json | 1 + 22 files changed, 381 insertions(+), 114 deletions(-) create mode 100644 dnscms/dnscms/revalidation.py create mode 100644 dnscms/tests/test_revalidation.py create mode 100644 web/src/app/api/revalidate/route.ts create mode 100644 web/src/lib/revalidation.test.ts create mode 100644 web/src/lib/revalidation.ts diff --git a/README.md b/README.md index ffd9ae4..64c9e11 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,18 @@ npm run build prek install # registers the git hook prek run --all-files # run on everything ``` + +## Caching and revalidation + +The frontend caches all pages and GraphQL responses (ISR) under a single `cms` cache tag. +Wagtail purges it via a webhook on any publish/unpublish/move/delete (see `dnscms/dnscms/revalidation.py`). +Date-sensitive queries (`futureEvents`, opening hours) additionally expire at Oslo midnight. +`REVALIDATE_WEBHOOK_SECRET` must be set to the same value on both sides +(`web/.env.local` and `dnscms/dnscms/settings/local.py` or env); unset disables the webhook. + +```bash +# manual purge +curl -X POST -H "X-Revalidate-Secret: $SECRET" https:///api/revalidate +``` + +Note: ISR is off under `npm run dev`; use `npm run build && npm run start` to test caching. diff --git a/dnscms/dnscms/apps.py b/dnscms/dnscms/apps.py index f665e7b..9b4493b 100644 --- a/dnscms/dnscms/apps.py +++ b/dnscms/dnscms/apps.py @@ -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() diff --git a/dnscms/dnscms/revalidation.py b/dnscms/dnscms/revalidation.py new file mode 100644 index 0000000..2d98d60 --- /dev/null +++ b/dnscms/dnscms/revalidation.py @@ -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}" + ) diff --git a/dnscms/dnscms/settings/base.py b/dnscms/dnscms/settings/base.py index f5ce506..7c9074c 100644 --- a/dnscms/dnscms/settings/base.py +++ b/dnscms/dnscms/settings/base.py @@ -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, diff --git a/dnscms/tests/test_revalidation.py b/dnscms/tests/test_revalidation.py new file mode 100644 index 0000000..2ebef76 --- /dev/null +++ b/dnscms/tests/test_revalidation.py @@ -0,0 +1,127 @@ +import logging +import urllib.error + +import pytest + +from dnscms import revalidation +from events.models import EventCategory +from tests.conftest import GenericPageFactory + +WEBHOOK_URL = "https://frontend.example.com/api/revalidate" + + +class InlineThread: + def __init__(self, target=None, args=(), kwargs=None, daemon=None): + self._target = target + self._args = args + self._kwargs = kwargs or {} + + def start(self): + self._target(*self._args, **self._kwargs) + + +@pytest.fixture +def webhook_calls(monkeypatch, settings): + settings.REVALIDATE_WEBHOOK_SECRET = "s3cret" + settings.FRONTEND_BASE_URL = "https://frontend.example.com" + calls = [] + monkeypatch.setattr(revalidation.threading, "Thread", InlineThread) + monkeypatch.setattr( + revalidation, "_send_webhook", lambda url, secret, reason: calls.append((url, secret)) + ) + return calls + + +def test_page_publish_triggers_webhook( + home_page, webhook_calls, django_capture_on_commit_callbacks +): + page = GenericPageFactory(parent=home_page) + with django_capture_on_commit_callbacks(execute=True): + page.save_revision().publish() + + assert webhook_calls == [(WEBHOOK_URL, "s3cret")] + + +def test_page_unpublish_triggers_webhook( + home_page, webhook_calls, django_capture_on_commit_callbacks +): + page = GenericPageFactory(parent=home_page) + page.save_revision().publish() + webhook_calls.clear() + + with django_capture_on_commit_callbacks(execute=True): + page.unpublish() + + assert webhook_calls == [(WEBHOOK_URL, "s3cret")] + + +def test_page_delete_triggers_webhook( + home_page, webhook_calls, django_capture_on_commit_callbacks +): + # live=False isolates the post_delete path: deleting a live page would + # also fire page_unpublished (a harmless duplicate purge in production) + page = GenericPageFactory(parent=home_page, live=False) + webhook_calls.clear() + + with django_capture_on_commit_callbacks(execute=True): + page.delete() + + assert webhook_calls == [(WEBHOOK_URL, "s3cret")] + + +@pytest.mark.django_db +def test_snippet_save_triggers_webhook(webhook_calls, django_capture_on_commit_callbacks): + with django_capture_on_commit_callbacks(execute=True): + EventCategory.objects.create(name="Konsert", slug="konsert") + + assert webhook_calls == [(WEBHOOK_URL, "s3cret")] + + +def test_unset_secret_skips_and_warns_once(settings, monkeypatch, caplog): + settings.REVALIDATE_WEBHOOK_SECRET = "" + monkeypatch.setattr(revalidation, "_warned_unconfigured", False) + + with caplog.at_level(logging.WARNING, logger="dnscms.revalidation"): + revalidation.trigger_frontend_revalidation("test") + revalidation.trigger_frontend_revalidation("test") + + warnings = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warnings) == 1 + + +def test_send_webhook_failure_is_swallowed(monkeypatch, caplog): + def raise_urlerror(request, timeout): + raise urllib.error.URLError("connection refused") + + monkeypatch.setattr(revalidation.urllib.request, "urlopen", raise_urlerror) + + with caplog.at_level(logging.ERROR, logger="dnscms.revalidation"): + revalidation._send_webhook(WEBHOOK_URL, "s3cret", "test") + + assert any("frontend revalidation failed" in r.message for r in caplog.records) + + +def test_send_webhook_request_shape(monkeypatch): + seen = {} + + class FakeResponse: + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + def fake_urlopen(request, timeout): + seen["request"] = request + seen["timeout"] = timeout + return FakeResponse() + + monkeypatch.setattr(revalidation.urllib.request, "urlopen", fake_urlopen) + revalidation._send_webhook(WEBHOOK_URL, "s3cret", "test") + + request = seen["request"] + assert request.get_method() == "POST" + assert request.get_header("X-revalidate-secret") == "s3cret" + assert seen["timeout"] == revalidation.WEBHOOK_TIMEOUT_SECONDS diff --git a/web/package.json b/web/package.json index 5f527b1..6cac3bc 100644 --- a/web/package.json +++ b/web/package.json @@ -7,6 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", + "test": "node --test src/lib/*.test.ts", "codegen": "graphql-codegen", "perf:build": "next build", "perf:serve": "next start -p 3100", diff --git a/web/src/app/[...url]/page.tsx b/web/src/app/[...url]/page.tsx index de379d3..b753d05 100644 --- a/web/src/app/[...url]/page.tsx +++ b/web/src/app/[...url]/page.tsx @@ -8,8 +8,6 @@ import { } from "@/components/general/GenericPageView"; import { getSeoMetadata } from "@/lib/seo"; -export const dynamicParams = false; - function getWagtailUrlPath(url: string[]): string { // for the page /foo/bar we need to look for `/home/foo/bar/` return `/home/${url.join("/")}/`; diff --git a/web/src/app/aktuelt/[slug]/page.tsx b/web/src/app/aktuelt/[slug]/page.tsx index 1a5c745..d20ab83 100644 --- a/web/src/app/aktuelt/[slug]/page.tsx +++ b/web/src/app/aktuelt/[slug]/page.tsx @@ -1,36 +1,14 @@ import { Metadata, ResolvingMetadata } from "next"; import { notFound } from "next/navigation"; -import { getClient } from "@/app/client"; import { NewsPageView, loadNewsPageProps, } from "@/components/news/NewsPageView"; -import { graphql } from "@/gql"; import { getSeoMetadata } from "@/lib/seo"; export async function generateStaticParams() { - const allNewsSlugsQuery = graphql(` - query allNewsSlugs { - pages(contentType: "news.NewsPage") { - id - slug - } - } - `); - const { data, error } = await getClient().query(allNewsSlugsQuery, {}); - - if (error) { - throw new Error(error.message); - } - if (!data?.pages) { - throw new Error( - "Failed to generate static params for subpages of /aktuelt" - ); - } - - return data.pages.map((page: any) => ({ - slug: page.slug, - })); + // Prerender nothing at build time; render and cache on first request + return []; } type Params = Promise<{ slug: string }>; diff --git a/web/src/app/api/revalidate/route.ts b/web/src/app/api/revalidate/route.ts new file mode 100644 index 0000000..1c5f52d --- /dev/null +++ b/web/src/app/api/revalidate/route.ts @@ -0,0 +1,28 @@ +import { timingSafeEqual } from "node:crypto"; +import { revalidateTag } from "next/cache"; +import { NextRequest, NextResponse } from "next/server"; + +import { CMS_CACHE_TAG } from "@/lib/revalidation"; + +function secretMatches(provided: string, expected: string): boolean { + const a = Buffer.from(provided); + const b = Buffer.from(expected); + return a.length === b.length && timingSafeEqual(a, b); +} + +export async function POST(req: NextRequest) { + const secret = process.env.REVALIDATE_WEBHOOK_SECRET; + if (!secret) { + return NextResponse.json( + { error: "revalidation is not configured" }, + { status: 503 } + ); + } + const provided = req.headers.get("x-revalidate-secret") ?? ""; + if (!secretMatches(provided, secret)) { + return NextResponse.json({ error: "invalid secret" }, { status: 401 }); + } + // expire: 0 purges immediately; the next request renders fresh + revalidateTag(CMS_CACHE_TAG, { expire: 0 }); + return NextResponse.json({ revalidated: true, tag: CMS_CACHE_TAG }); +} diff --git a/web/src/app/api/v1/events/route.ts b/web/src/app/api/v1/events/route.ts index df8acbf..e90be01 100644 --- a/web/src/app/api/v1/events/route.ts +++ b/web/src/app/api/v1/events/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getClient } from "@/app/client"; +import { cachedUntilOsloMidnight } from "@/lib/revalidation"; import { eventsOverviewQuery, getSingularEvents, @@ -45,7 +46,11 @@ export async function GET(req: NextRequest) { ); } - const { data, error } = await getClient().query(eventsOverviewQuery, {}); + const { data, error } = await getClient().query( + eventsOverviewQuery, + {}, + cachedUntilOsloMidnight() + ); if (error) { throw new Error(error.message); } diff --git a/web/src/app/arrangementer/[slug]/page.tsx b/web/src/app/arrangementer/[slug]/page.tsx index 0ed16dd..69c607b 100644 --- a/web/src/app/arrangementer/[slug]/page.tsx +++ b/web/src/app/arrangementer/[slug]/page.tsx @@ -1,36 +1,14 @@ import { Metadata, ResolvingMetadata } from "next"; import { notFound } from "next/navigation"; -import { getClient } from "@/app/client"; import { EventPageView, loadEventPageProps, } from "@/components/events/EventPageView"; -import { graphql } from "@/gql"; import { getSeoMetadata } from "@/lib/seo"; export async function generateStaticParams() { - const allEventSlugsQuery = graphql(` - query allEventSlugs { - pages(contentType: "events.EventPage") { - id - slug - } - } - `); - const { data, error } = await getClient().query(allEventSlugsQuery, {}); - - if (error) { - throw new Error(error.message); - } - if (!data?.pages) { - throw new Error( - "Failed to generate static params for subpages of /arrangementer" - ); - } - - return data.pages.map((page: any) => ({ - slug: page.slug, - })); + // Prerender nothing at build time; render and cache on first request + return []; } type Params = Promise<{ slug: string }>; diff --git a/web/src/app/client.ts b/web/src/app/client.ts index 596ae7f..7ff55ba 100644 --- a/web/src/app/client.ts +++ b/web/src/app/client.ts @@ -3,6 +3,8 @@ import "server-only"; import { cacheExchange, createClient, fetchExchange } from "@urql/core"; import { registerUrql } from "@urql/next/rsc"; +import { CMS_CACHE_TAG } from "@/lib/revalidation"; + const wagtailBaseUrl = process.env.WAGTAIL_BASE_URL; if (!wagtailBaseUrl) { throw new Error("WAGTAIL_BASE_URL is not set"); @@ -13,8 +15,9 @@ const makeClient = () => { return createClient({ url: graphqlEndpoint, exchanges: [cacheExchange, fetchExchange], - // requestPolicy: "network-only", - fetchOptions: { next: { revalidate: 0 } }, + // Cache all queries in the Data Cache until revalidateTag(CMS_CACHE_TAG); + // override per operation with the contexts in @/lib/revalidation + fetchOptions: { cache: "force-cache", next: { tags: [CMS_CACHE_TAG] } }, }); }; diff --git a/web/src/app/preview/render/page.tsx b/web/src/app/preview/render/page.tsx index afb1735..8d451b4 100644 --- a/web/src/app/preview/render/page.tsx +++ b/web/src/app/preview/render/page.tsx @@ -1,4 +1,5 @@ import { getClient } from "@/app/client"; +import { uncached } from "@/lib/revalidation"; import { PreviewBanner } from "@/components/general/PreviewBanner"; import { AssociationIndexView, @@ -149,7 +150,11 @@ export default async function PreviewRender() { return ; } - const { data, error } = await getClient().query(previewPageQuery, { token }); + const { data, error } = await getClient().query( + previewPageQuery, + { token }, + uncached + ); if (error) { throw new Error(error.message); } diff --git a/web/src/app/sok/page.tsx b/web/src/app/sok/page.tsx index 2405a09..a2bad1e 100644 --- a/web/src/app/sok/page.tsx +++ b/web/src/app/sok/page.tsx @@ -1,4 +1,5 @@ import { getClient } from "@/app/client"; +import { uncached } from "@/lib/revalidation"; import { type SearchResult, SearchResults, @@ -65,7 +66,7 @@ export default async function Page({ } `); - const { data } = await getClient().query(searchQuery, { query }); + const { data } = await getClient().query(searchQuery, { query }, uncached); const all = (data?.results ?? []) as SearchResult[]; totalCount = all.length; results = all.slice(0, RESULT_LIMIT); diff --git a/web/src/components/events/EventIndexView.tsx b/web/src/components/events/EventIndexView.tsx index 59e5715..5f3cc48 100644 --- a/web/src/components/events/EventIndexView.tsx +++ b/web/src/components/events/EventIndexView.tsx @@ -1,5 +1,6 @@ import { VenueFragment } from "@/gql/graphql"; import { getClient } from "@/app/client"; +import { cachedUntilOsloMidnight } from "@/lib/revalidation"; import { EventContainer } from "@/components/events/EventContainer"; import { PageHeader } from "@/components/general/PageHeader"; import { @@ -17,7 +18,11 @@ export type EventIndexViewProps = { }; export async function loadEventIndexProps(): Promise { - const { data, error } = await getClient().query(eventsOverviewQuery, {}); + const { data, error } = await getClient().query( + eventsOverviewQuery, + {}, + cachedUntilOsloMidnight() + ); if (error) throw new Error(error.message); if ( !data?.index || diff --git a/web/src/components/home/HomePageView.tsx b/web/src/components/home/HomePageView.tsx index 0fcffbd..7d82fd4 100644 --- a/web/src/components/home/HomePageView.tsx +++ b/web/src/components/home/HomePageView.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { graphql } from "@/gql"; import { HomeFragment } from "@/gql/graphql"; import { getClient } from "@/app/client"; +import { cachedUntilOsloMidnight } from "@/lib/revalidation"; import { EventListItemFragment } from "@/lib/event"; import { NewsListItemFragment } from "@/lib/news"; import { FeaturedEvents } from "@/components/events/FeaturedEvents"; @@ -55,7 +56,11 @@ export type HomePageViewProps = { export async function loadHomePageProps(overrides?: { homeOverride?: HomeFragment; }): Promise { - const { data, error } = await getClient().query(homeQuery, {}); + const { data, error } = await getClient().query( + homeQuery, + {}, + cachedUntilOsloMidnight() + ); if (error) throw new Error(error.message); const home = overrides?.homeOverride ?? (data?.home as HomeFragment | undefined); if (!home) throw new Error("Failed to load /"); diff --git a/web/src/gql/gql.ts b/web/src/gql/gql.ts index d327350..c750dbb 100644 --- a/web/src/gql/gql.ts +++ b/web/src/gql/gql.ts @@ -15,8 +15,6 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ */ type Documents = { "\n query allGenericSlugs {\n pages(contentType: \"generic.GenericPage\") {\n id\n urlPath\n }\n }\n ": typeof types.AllGenericSlugsDocument, - "\n query allNewsSlugs {\n pages(contentType: \"news.NewsPage\") {\n id\n slug\n }\n }\n ": typeof types.AllNewsSlugsDocument, - "\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n ": typeof types.AllEventSlugsDocument, "\n query allAssociationSlugs {\n pages(contentType: \"associations.AssociationPage\") {\n id\n slug\n }\n }\n ": typeof types.AllAssociationSlugsDocument, "\n query allVenueSlugs {\n pages(contentType: \"venues.VenuePage\", limit: 100) {\n id\n slug\n }\n }\n ": typeof types.AllVenueSlugsDocument, "\n query previewPage($token: String!) {\n page: page(token: $token) {\n __typename\n ... on GenericPage {\n ...Generic\n }\n ... on StudioPage {\n ...Studio\n }\n ... on SponsorsPage {\n ...SponsorsPage\n }\n ... on HomePage {\n ...Home\n }\n ... on EventPage {\n ...Event\n }\n ... on NewsPage {\n ...News\n }\n ... on AssociationPage {\n ...Association\n }\n ... on VenuePage {\n ...Venue\n }\n ... on NewsIndex {\n ...NewsIndex\n }\n ... on AssociationIndex {\n ...AssociationIndex\n }\n ... on VenueIndex {\n ...VenueIndex\n }\n ... on VenueRentalIndex {\n ...VenueRentalIndex\n }\n ... on ContactIndex {\n ...ContactIndex\n }\n }\n }\n": typeof types.PreviewPageDocument, @@ -85,8 +83,6 @@ type Documents = { }; const documents: Documents = { "\n query allGenericSlugs {\n pages(contentType: \"generic.GenericPage\") {\n id\n urlPath\n }\n }\n ": types.AllGenericSlugsDocument, - "\n query allNewsSlugs {\n pages(contentType: \"news.NewsPage\") {\n id\n slug\n }\n }\n ": types.AllNewsSlugsDocument, - "\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n ": types.AllEventSlugsDocument, "\n query allAssociationSlugs {\n pages(contentType: \"associations.AssociationPage\") {\n id\n slug\n }\n }\n ": types.AllAssociationSlugsDocument, "\n query allVenueSlugs {\n pages(contentType: \"venues.VenuePage\", limit: 100) {\n id\n slug\n }\n }\n ": types.AllVenueSlugsDocument, "\n query previewPage($token: String!) {\n page: page(token: $token) {\n __typename\n ... on GenericPage {\n ...Generic\n }\n ... on StudioPage {\n ...Studio\n }\n ... on SponsorsPage {\n ...SponsorsPage\n }\n ... on HomePage {\n ...Home\n }\n ... on EventPage {\n ...Event\n }\n ... on NewsPage {\n ...News\n }\n ... on AssociationPage {\n ...Association\n }\n ... on VenuePage {\n ...Venue\n }\n ... on NewsIndex {\n ...NewsIndex\n }\n ... on AssociationIndex {\n ...AssociationIndex\n }\n ... on VenueIndex {\n ...VenueIndex\n }\n ... on VenueRentalIndex {\n ...VenueRentalIndex\n }\n ... on ContactIndex {\n ...ContactIndex\n }\n }\n }\n": types.PreviewPageDocument, @@ -172,14 +168,6 @@ export function graphql(source: string): unknown; * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query allGenericSlugs {\n pages(contentType: \"generic.GenericPage\") {\n id\n urlPath\n }\n }\n "): (typeof documents)["\n query allGenericSlugs {\n pages(contentType: \"generic.GenericPage\") {\n id\n urlPath\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query allNewsSlugs {\n pages(contentType: \"news.NewsPage\") {\n id\n slug\n }\n }\n "): (typeof documents)["\n query allNewsSlugs {\n pages(contentType: \"news.NewsPage\") {\n id\n slug\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n "): (typeof documents)["\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n "]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/web/src/gql/graphql.ts b/web/src/gql/graphql.ts index 9167218..3adf94c 100644 --- a/web/src/gql/graphql.ts +++ b/web/src/gql/graphql.ts @@ -25,48 +25,6 @@ export type AllGenericSlugsQuery = { pages: Array< | { id: string | null, urlPath: string } > }; -export type AllNewsSlugsQueryVariables = Exact<{ [key: string]: never; }>; - - -export type AllNewsSlugsQuery = { pages: Array< - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - > }; - -export type AllEventSlugsQueryVariables = Exact<{ [key: string]: never; }>; - - -export type AllEventSlugsQuery = { pages: Array< - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - | { id: string | null, slug: string } - > }; - export type AllAssociationSlugsQueryVariables = Exact<{ [key: string]: never; }>; @@ -2134,8 +2092,6 @@ export const OpeningHoursRangeBlockFragmentDoc = {"kind":"Document","definitions export const OpeningHoursWeekBlockFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OpeningHoursWeekBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursWeekBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tuesday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"wednesday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"thursday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"friday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"saturday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"sunday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OpeningHoursRangeBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timeFrom"}},{"kind":"Field","name":{"kind":"Name","value":"timeTo"}},{"kind":"Field","name":{"kind":"Name","value":"custom"}}]}}]} as unknown as DocumentNode; export const OpeningHoursSetFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OpeningHoursSet"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursSet"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"effectiveFrom"}},{"kind":"Field","name":{"kind":"Name","value":"effectiveTo"}},{"kind":"Field","name":{"kind":"Name","value":"announcement"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"function"}},{"kind":"Field","name":{"kind":"Name","value":"week"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursWeekBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursWeekBlock"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OpeningHoursRangeBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timeFrom"}},{"kind":"Field","name":{"kind":"Name","value":"timeTo"}},{"kind":"Field","name":{"kind":"Name","value":"custom"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OpeningHoursWeekBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursWeekBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tuesday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"wednesday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"thursday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"friday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"saturday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"sunday"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OpeningHoursRangeBlock"}}]}}]}}]}}]} as unknown as DocumentNode; export const AllGenericSlugsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allGenericSlugs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"contentType"},"value":{"kind":"StringValue","value":"generic.GenericPage","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"urlPath"}}]}}]}}]} as unknown as DocumentNode; -export const AllNewsSlugsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allNewsSlugs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"contentType"},"value":{"kind":"StringValue","value":"news.NewsPage","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]} as unknown as DocumentNode; -export const AllEventSlugsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allEventSlugs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"contentType"},"value":{"kind":"StringValue","value":"events.EventPage","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]} as unknown as DocumentNode; export const AllAssociationSlugsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allAssociationSlugs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"contentType"},"value":{"kind":"StringValue","value":"associations.AssociationPage","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]} as unknown as DocumentNode; export const AllVenueSlugsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allVenueSlugs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"contentType"},"value":{"kind":"StringValue","value":"venues.VenuePage","block":false}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"100"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]} as unknown as DocumentNode; export const PreviewPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"previewPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"page"},"name":{"kind":"Name","value":"page"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"GenericPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Generic"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StudioPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Studio"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SponsorsPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SponsorsPage"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HomePage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Home"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EventPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Event"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NewsPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"News"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AssociationPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Association"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VenuePage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Venue"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NewsIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NewsIndex"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AssociationIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AssociationIndex"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VenueIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"VenueIndex"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VenueRentalIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"VenueRentalIndex"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ContactIndex"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RichTextBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RichTextBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rawValue"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Image"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CustomImage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}},{"kind":"Field","name":{"kind":"Name","value":"alt"}},{"kind":"Field","name":{"kind":"Name","value":"attribution"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ImageWithTextBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ImageWithTextBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"imageFormat"}},{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ImageSliderItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ImageSliderItemBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ImageSliderBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ImageSliderBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ImageSliderItemBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ImageSliderItem"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HorizontalRuleBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HorizontalRuleBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FeaturedBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FeaturedBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","alias":{"kind":"Name","value":"featuredBlockText"},"name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"linkText"}},{"kind":"Field","name":{"kind":"Name","value":"imagePosition"}},{"kind":"Field","name":{"kind":"Name","value":"backgroundColor"}},{"kind":"Field","name":{"kind":"Name","value":"featuredPage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contentType"}},{"kind":"Field","name":{"kind":"Name","value":"pageType"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EventPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"featuredImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NewsPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"featuredImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuredImageOverride"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ContactEntity"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactEntity"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"contactType"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}},{"kind":"Field","name":{"kind":"Name","value":"image"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ContactListBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactListBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"blockType"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactEntityBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contactEntity"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ContactEntity"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"EmbedBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EmbedBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"embed"}},{"kind":"Field","name":{"kind":"Name","value":"rawEmbed"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FactBoxBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FactBoxBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backgroundColor"}},{"kind":"Field","alias":{"kind":"Name","value":"factBoxBody"},"name":{"kind":"Name","value":"body"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScheduleItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ScheduleItemBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"time"}},{"kind":"Field","alias":{"kind":"Name","value":"scheduleItemTitle"},"name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScheduleBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ScheduleBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"scheduleTitle"},"name":{"kind":"Name","value":"title"}},{"kind":"Field","alias":{"kind":"Name","value":"scheduleItems"},"name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ScheduleItemBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScheduleItem"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LeafBlocks"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamFieldInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"blockType"}},{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RichTextBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RichTextBlock"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ImageWithTextBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ImageWithTextBlock"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ImageSliderBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ImageSliderBlock"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HorizontalRuleBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"HorizontalRuleBlock"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FeaturedBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FeaturedBlock"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactListBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ContactListBlock"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EmbedBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EmbedBlock"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FactBoxBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FactBoxBlock"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ScheduleBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScheduleBlock"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AccordionBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AccordionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"heading"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"blockType"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageSectionBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageSectionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"backgroundColor"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"blockType"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OneLevelOfBlocks"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamFieldInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LeafBlocks"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AccordionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AccordionBlock"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LeafBlocks"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageSectionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageSectionBlock"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LeafBlocks"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ContactSectionBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactSectionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"blocks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"blockType"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ContactSubsectionBlock"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactSubsectionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"blocks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"blockType"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Blocks"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamFieldInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OneLevelOfBlocks"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AccordionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AccordionBlock"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OneLevelOfBlocks"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageSectionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageSectionBlock"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OneLevelOfBlocks"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactSectionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ContactSectionBlock"}},{"kind":"Field","name":{"kind":"Name","value":"blocks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OneLevelOfBlocks"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactSubsectionBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ContactSubsectionBlock"}},{"kind":"Field","name":{"kind":"Name","value":"blocks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OneLevelOfBlocks"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Sponsor"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SponsorBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"website"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"EventCategory"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EventCategory"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"pig"}},{"kind":"Field","name":{"kind":"Name","value":"showInFilters"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"EventOrganizer"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EventOrganizer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"externalUrl"}},{"kind":"Field","name":{"kind":"Name","value":"association"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AssociationPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Generic"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"GenericPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"urlPath"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"pig"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Studio"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StudioPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"pig"}},{"kind":"Field","name":{"kind":"Name","value":"logo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}},{"kind":"Field","name":{"kind":"Name","value":"alt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SponsorsPage"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SponsorsPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sponsors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SponsorBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Sponsor"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Home"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HomePage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"featuredEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EventPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subtitle"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OneLevelOfBlocks"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuredImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pig"}},{"kind":"Field","name":{"kind":"Name","value":"facebookUrl"}},{"kind":"Field","name":{"kind":"Name","value":"ticketUrl"}},{"kind":"Field","name":{"kind":"Name","value":"free"}},{"kind":"Field","name":{"kind":"Name","value":"priceRegular"}},{"kind":"Field","name":{"kind":"Name","value":"priceMember"}},{"kind":"Field","name":{"kind":"Name","value":"priceStudent"}},{"kind":"Field","name":{"kind":"Name","value":"categories"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EventCategory"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EventCategory"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"occurrences"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5000"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EventOccurrence"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"end"}},{"kind":"Field","name":{"kind":"Name","value":"venue"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"preposition"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"venueCustom"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"organizers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EventOrganizer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EventOrganizer"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"News"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NewsPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"firstPublishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"excerpt"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"featuredImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Association"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AssociationPage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"excerpt"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}},{"kind":"Field","name":{"kind":"Name","value":"logo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}}]}},{"kind":"Field","name":{"kind":"Name","value":"associationType"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Venue"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VenuePage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ImageSliderBlock"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ImageSliderBlock"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuredImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Image"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showAsBookable"}},{"kind":"Field","name":{"kind":"Name","value":"showInOverview"}},{"kind":"Field","name":{"kind":"Name","value":"floor"}},{"kind":"Field","name":{"kind":"Name","value":"preposition"}},{"kind":"Field","name":{"kind":"Name","value":"usedFor"}},{"kind":"Field","name":{"kind":"Name","value":"techSpecsUrl"}},{"kind":"Field","name":{"kind":"Name","value":"capabilityAudio"}},{"kind":"Field","name":{"kind":"Name","value":"capabilityAudioVideo"}},{"kind":"Field","name":{"kind":"Name","value":"capabilityBar"}},{"kind":"Field","name":{"kind":"Name","value":"capabilityLighting"}},{"kind":"Field","name":{"kind":"Name","value":"capacityLegal"}},{"kind":"Field","name":{"kind":"Name","value":"capacityStanding"}},{"kind":"Field","name":{"kind":"Name","value":"capacitySitting"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NewsIndex"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NewsIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AssociationIndex"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AssociationIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"VenueIndex"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VenueIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"VenueRentalIndex"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VenueRentalIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ContactIndex"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContactIndex"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"seoTitle"}},{"kind":"Field","name":{"kind":"Name","value":"searchDescription"}},{"kind":"Field","name":{"kind":"Name","value":"lead"}},{"kind":"Field","name":{"kind":"Name","value":"body"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Blocks"}}]}}]}}]} as unknown as DocumentNode; diff --git a/web/src/lib/openinghours.ts b/web/src/lib/openinghours.ts index 6b880de..3651f35 100644 --- a/web/src/lib/openinghours.ts +++ b/web/src/lib/openinghours.ts @@ -8,6 +8,7 @@ import { } from "date-fns"; import { getClient } from "@/app/client"; +import { cachedUntilOsloMidnight } from "@/lib/revalidation"; import { graphql, unmaskFragment } from "@/gql"; import type { OpeningHoursRangeBlockFragment as OpeningHoursRangeBlock, @@ -50,7 +51,13 @@ const openingHoursQuery = graphql(` `); export async function fetchOpeningHoursSets() { - const { data, error } = await getClient().query(openingHoursQuery, {}); + const { data, error } = await getClient().query( + openingHoursQuery, + {}, + // The footer renders today's hours on every page, so this TTL doubles as + // the sitewide midnight refresh for futureEvents rollover + cachedUntilOsloMidnight() + ); const sets = (data?.openingHoursSets ?? []) as OpeningHoursSet[]; return sets; } diff --git a/web/src/lib/revalidation.test.ts b/web/src/lib/revalidation.test.ts new file mode 100644 index 0000000..46b8366 --- /dev/null +++ b/web/src/lib/revalidation.test.ts @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { secondsUntilOsloMidnight } from "./revalidation.ts"; + +test("normal day: 12:00 CEST is 12h from midnight", () => { + const now = new Date("2026-07-07T10:00:00Z"); + assert.equal(secondsUntilOsloMidnight(now), 12 * 3600); +}); + +test("clamps to 60s just before midnight", () => { + const now = new Date("2026-07-07T21:59:30Z"); + assert.equal(secondsUntilOsloMidnight(now), 60); +}); + +test("DST fall-back day is 25h (2026-10-25)", () => { + const now = new Date("2026-10-24T22:00:00Z"); + assert.equal(secondsUntilOsloMidnight(now), 25 * 3600); +}); + +test("DST spring-forward day is 23h (2026-03-29)", () => { + const now = new Date("2026-03-28T23:00:00Z"); + assert.equal(secondsUntilOsloMidnight(now), 23 * 3600); +}); diff --git a/web/src/lib/revalidation.ts b/web/src/lib/revalidation.ts new file mode 100644 index 0000000..34e7b24 --- /dev/null +++ b/web/src/lib/revalidation.ts @@ -0,0 +1,38 @@ +import { addDays, startOfDay } from "date-fns"; +import { fromZonedTime, toZonedTime } from "date-fns-tz"; +import type { OperationContext } from "@urql/core"; + +const timeZone = "Europe/Oslo"; + +// Global cache tag: any CMS content change purges everything +export const CMS_CACHE_TAG = "cms"; + +const MIN_REVALIDATE_SECONDS = 60; + +// Clamped so a request just before midnight never yields revalidate ~0 +export function secondsUntilOsloMidnight(now: Date = new Date()): number { + const osloNow = toZonedTime(now, timeZone); + const nextOsloMidnight = startOfDay(addDays(osloNow, 1)); + const instant = fromZonedTime(nextOsloMidnight, timeZone); + return Math.max( + Math.ceil((instant.getTime() - now.getTime()) / 1000), + MIN_REVALIDATE_SECONDS + ); +} + +// Per-operation contexts replace (not merge with) the client-default +// fetchOptions, so both contexts below are self-contained + +// For per-user (search) and token-scoped (preview) queries +export const uncached: Partial = { + fetchOptions: { cache: "no-store" }, +}; + +// For queries whose results depend on "today" (futureEvents, opening hours) +export function cachedUntilOsloMidnight(): Partial { + return { + fetchOptions: { + next: { revalidate: secondsUntilOsloMidnight(), tags: [CMS_CACHE_TAG] }, + }, + }; +} diff --git a/web/tsconfig.json b/web/tsconfig.json index 877b650..ed5608f 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -13,6 +13,7 @@ "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, + "allowImportingTsExtensions": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true,