From 615885edb8a8e64d4e6178e0714488af6ee76785 Mon Sep 17 00:00:00 2001 From: Levi Zitting Date: Sun, 4 Oct 2026 06:26:58 -0500 Subject: [PATCH 1/2] feat: add CMS page routes --- src/lib/components/ChildPageShell.svelte | 23 ++ src/lib/components/GenericPage.svelte | 46 +++ src/lib/components/OveItEmbed.svelte | 88 ++++++ src/lib/components/SpeakerDetailPage.svelte | 128 ++++++++ src/lib/head.ts | 8 +- src/lib/oveit.ts | 32 ++ src/lib/pageBlocks.ts | 13 + src/lib/pageHead.ts | 61 ++++ src/lib/server/getPageDataForBlocks.ts | 41 +++ src/routes/+layout.svelte | 16 +- src/routes/[conference]/+page.server.ts | 45 +++ src/routes/[conference]/+page.svelte | 21 ++ .../[conference]/[...slug]/+page.server.ts | 77 +++++ .../[conference]/[...slug]/+page.svelte | 36 +++ .../[conference]/register/+page.server.ts | 20 ++ src/routes/[conference]/register/+page.svelte | 21 ++ .../register/success/+page.server.ts | 19 ++ .../register/success/+page.svelte | 38 +++ tests/route-smoke.mjs | 273 ++++++++++++++++-- tests/unit/oveit.test.ts | 29 ++ 20 files changed, 998 insertions(+), 37 deletions(-) create mode 100644 src/lib/components/ChildPageShell.svelte create mode 100644 src/lib/components/GenericPage.svelte create mode 100644 src/lib/components/OveItEmbed.svelte create mode 100644 src/lib/components/SpeakerDetailPage.svelte create mode 100644 src/lib/oveit.ts create mode 100644 src/lib/pageBlocks.ts create mode 100644 src/lib/pageHead.ts create mode 100644 src/lib/server/getPageDataForBlocks.ts create mode 100644 src/routes/[conference]/+page.server.ts create mode 100644 src/routes/[conference]/+page.svelte create mode 100644 src/routes/[conference]/[...slug]/+page.server.ts create mode 100644 src/routes/[conference]/[...slug]/+page.svelte create mode 100644 src/routes/[conference]/register/+page.server.ts create mode 100644 src/routes/[conference]/register/+page.svelte create mode 100644 src/routes/[conference]/register/success/+page.server.ts create mode 100644 src/routes/[conference]/register/success/+page.svelte create mode 100644 tests/unit/oveit.test.ts diff --git a/src/lib/components/ChildPageShell.svelte b/src/lib/components/ChildPageShell.svelte new file mode 100644 index 0000000..ea60a0b --- /dev/null +++ b/src/lib/components/ChildPageShell.svelte @@ -0,0 +1,23 @@ + + + +{@render children()} +{#if sponsors} + +{/if} diff --git a/src/lib/components/GenericPage.svelte b/src/lib/components/GenericPage.svelte new file mode 100644 index 0000000..01e6f06 --- /dev/null +++ b/src/lib/components/GenericPage.svelte @@ -0,0 +1,46 @@ + + +{#each blocks as block (block.id)} + {#if block.contentType === 'introAndEmailSignupBlock'} + + {:else if block.contentType === 'scheduleBlock'} + + {:else if block.contentType === 'locationBlock'} + + {:else if block.contentType === 'sponsorsBlock'} + + {:else if block.contentType === 'richText'} + + {:else if block.contentType === 'textWithButtons'} + + {/if} +{/each} diff --git a/src/lib/components/OveItEmbed.svelte b/src/lib/components/OveItEmbed.svelte new file mode 100644 index 0000000..5fdfdcb --- /dev/null +++ b/src/lib/components/OveItEmbed.svelte @@ -0,0 +1,88 @@ + + +{#if iframeSrc} + +{:else} + Loading... +{/if} diff --git a/src/lib/components/SpeakerDetailPage.svelte b/src/lib/components/SpeakerDetailPage.svelte new file mode 100644 index 0000000..0a1e078 --- /dev/null +++ b/src/lib/components/SpeakerDetailPage.svelte @@ -0,0 +1,128 @@ + + +
+
+
+
+ {#if image?.url} + + {/if} +
+
+

+ {speaker.name}{jobTitle + ? `: ${jobTitle}` + : ''} +

+ {#if bio?.markup} + + {/if} +
+ {#each links as { title, url, icon } (title)} + + + + {/each} +
+
+
+
+
+{#if sessions.length > 0} +
+
+

Sessions

+ {#each sessions as session (session.id)} + + {/each} +
+
+{/if} diff --git a/src/lib/head.ts b/src/lib/head.ts index 41eecee..009db8b 100644 --- a/src/lib/head.ts +++ b/src/lib/head.ts @@ -26,12 +26,12 @@ export type SharedHead = { description?: string; openGraph: { title: string; - image: Required; + image: SharedHeadImage & { url: string }; }; twitter: { card: 'summary_large_image'; title: string; - image: Required; + image: SharedHeadImage & { url: string }; }; icon: { href: string; @@ -83,7 +83,9 @@ export function buildSharedHead({ siteUrl, metadata?.openGraph?.image?.url ?? DEFAULT_OG_IMAGE_PATH, ), - type: metadata?.openGraph?.image?.type ?? 'image/jpeg', + type: + metadata?.openGraph?.image?.type ?? + (metadata?.openGraph?.image ? undefined : 'image/jpeg'), width: metadata?.openGraph?.image?.width ?? 1200, height: metadata?.openGraph?.image?.height ?? 630, }; diff --git a/src/lib/oveit.ts b/src/lib/oveit.ts new file mode 100644 index 0000000..3f4b6ea --- /dev/null +++ b/src/lib/oveit.ts @@ -0,0 +1,32 @@ +import { parseUrl } from '#lib/util.ts'; + +export interface OveItIframeState { + embedId?: string; + iframeUrl: URL; +} + +export function buildOveItIframeState( + embedUrl: string, + locationHash = '', +): OveItIframeState | undefined { + const iframeUrl = parseUrl(embedUrl); + + if (!iframeUrl) { + return; + } + + const embedId = iframeUrl.searchParams.get('id') ?? undefined; + + if (embedId && locationHash) { + const hash = locationHash.startsWith('#') + ? locationHash.substring(1) + : locationHash; + const parts = hash.split('/'); + + if (parts.length >= 2 && parts[0] === embedId && parts[1]) { + iframeUrl.searchParams.set('next', parts[1]); + } + } + + return { embedId, iframeUrl }; +} diff --git a/src/lib/pageBlocks.ts b/src/lib/pageBlocks.ts new file mode 100644 index 0000000..6d6aab7 --- /dev/null +++ b/src/lib/pageBlocks.ts @@ -0,0 +1,13 @@ +import type { ContentBlock } from '#lib/types.ts'; + +export function getPageBlocks(page: { + properties?: { + blocks?: { items?: { content?: ContentBlock }[] } | null; + } | null; +}): ContentBlock[] { + return ( + page.properties?.blocks?.items?.flatMap((item) => + item.content ? [item.content] : [], + ) ?? [] + ); +} diff --git a/src/lib/pageHead.ts b/src/lib/pageHead.ts new file mode 100644 index 0000000..2454b9d --- /dev/null +++ b/src/lib/pageHead.ts @@ -0,0 +1,61 @@ +import { buildSharedHead, type SharedHead } from '#lib/head.ts'; +import { imageUrl } from '#lib/imageUrl.ts'; +import type { Page, Speaker } from '#lib/types.ts'; + +const TITLE_TEMPLATE_PREFIX = 'Method Conference - '; +const OG_IMAGE_WIDTH = 1200; +const OG_IMAGE_HEIGHT = 630; + +export function buildGenericPageHead({ + page, + siteUrl, +}: { + page: Page; + siteUrl: string; +}): SharedHead { + const { + title, + metaDescription, + openGraphImage: openGraphImages, + } = page.properties ?? {}; + const [openGraphImage] = openGraphImages ?? []; + + return buildSharedHead({ + siteUrl, + metadata: { + title: title ? applyTitleTemplate(title) : undefined, + description: metaDescription ?? undefined, + openGraph: openGraphImage?.url + ? { + image: { + url: imageUrl(openGraphImage.url, { + width: OG_IMAGE_WIDTH, + height: OG_IMAGE_HEIGHT, + }), + width: OG_IMAGE_WIDTH, + height: OG_IMAGE_HEIGHT, + }, + } + : undefined, + }, + }); +} + +export function buildSpeakerHead({ + speaker, + siteUrl, +}: { + speaker: Speaker; + siteUrl: string; +}): SharedHead { + return buildSharedHead({ + siteUrl, + metadata: { + title: speaker.name ? applyTitleTemplate(speaker.name) : undefined, + }, + }); +} + +function applyTitleTemplate(title: string): string { + return `${TITLE_TEMPLATE_PREFIX}${title}`; +} diff --git a/src/lib/server/getPageDataForBlocks.ts b/src/lib/server/getPageDataForBlocks.ts new file mode 100644 index 0000000..7127ca8 --- /dev/null +++ b/src/lib/server/getPageDataForBlocks.ts @@ -0,0 +1,41 @@ +import { getSchedule } from '#lib/server/getSchedule.ts'; +import { getSponsors } from '#lib/server/getSponsors.ts'; +import type { + ContentBlock, + ParsedConference, + Schedule, + Sponsors, +} from '#lib/types.ts'; + +export interface AdditionalPageData { + schedule?: Schedule; + sponsors?: Sponsors; +} + +export async function getPageDataForBlocks( + conference: ParsedConference, + blocks: ContentBlock[], +): Promise { + const pageData: AdditionalPageData = {}; + const tasks: Promise[] = []; + + if (blocks.find((block) => block.contentType === 'scheduleBlock')) { + tasks.push( + getSchedule(conference.id).then((schedule) => { + pageData.schedule = schedule; + }), + ); + } + + if (blocks.find((block) => block.contentType === 'sponsorsBlock')) { + tasks.push( + getSponsors(conference.id).then((sponsors) => { + pageData.sponsors = sponsors; + }), + ); + } + + await Promise.all(tasks); + + return pageData; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a057e06..7f14575 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -34,7 +34,11 @@ plausible.init()<` + }), ); const shouldEmitLayoutRobots = $derived( - !data.searchIndexingEnabled && page.status < 400, + page.status < 400 && + (!data.searchIndexingEnabled || page.data.robotsNoIndex), + ); + const layoutRobotsContent = $derived( + data.searchIndexingEnabled ? 'noindex' : 'noindex,nofollow', ); @@ -48,7 +52,9 @@ plausible.init()<` + {/if} - + {#if head.openGraph.image.type} + + {/if} {/if} - + {#if head.twitter.image.type} + + {/if} {#if shouldEmitLayoutRobots} - + {/if} {#if !dev} diff --git a/src/routes/[conference]/+page.server.ts b/src/routes/[conference]/+page.server.ts new file mode 100644 index 0000000..77a1da0 --- /dev/null +++ b/src/routes/[conference]/+page.server.ts @@ -0,0 +1,45 @@ +import { error } from '@sveltejs/kit'; +import { buildGenericPageHead } from '#lib/pageHead.ts'; +import { getPageBlocks } from '#lib/pageBlocks.ts'; +import { getConference } from '#lib/server/getConference.ts'; +import { + getPageDataForBlocks, + type AdditionalPageData, +} from '#lib/server/getPageDataForBlocks.ts'; +import { getSiteUrl } from '#lib/server/config.ts'; +import { getItemByPathOrDefault } from '#lib/server/umbraco/getItemByPath.ts'; +import type { Page } from '#lib/types.ts'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params }) => { + const conference = await getConference(params.conference); + + if (!conference) { + throw error(404); + } + + const page = await getItemByPathOrDefault(`${params.conference}/home`, { + expand: 'properties[$all]', + }); + + if (page?.contentType !== 'home') { + throw error(404); + } + + const blocks = getPageBlocks(page); + const pageData: AdditionalPageData = await getPageDataForBlocks( + conference, + blocks, + ); + + return { + conferenceSlug: params.conference, + conference, + page: page as Page, + ...pageData, + sharedHead: buildGenericPageHead({ + page: page as Page, + siteUrl: getSiteUrl().toString(), + }), + }; +}; diff --git a/src/routes/[conference]/+page.svelte b/src/routes/[conference]/+page.svelte new file mode 100644 index 0000000..4fa2ed1 --- /dev/null +++ b/src/routes/[conference]/+page.svelte @@ -0,0 +1,21 @@ + + + + diff --git a/src/routes/[conference]/[...slug]/+page.server.ts b/src/routes/[conference]/[...slug]/+page.server.ts new file mode 100644 index 0000000..59bf308 --- /dev/null +++ b/src/routes/[conference]/[...slug]/+page.server.ts @@ -0,0 +1,77 @@ +import { error } from '@sveltejs/kit'; +import { buildGenericPageHead, buildSpeakerHead } from '#lib/pageHead.ts'; +import { getPageBlocks } from '#lib/pageBlocks.ts'; +import { getConference } from '#lib/server/getConference.ts'; +import { + getPageDataForBlocks, + type AdditionalPageData, +} from '#lib/server/getPageDataForBlocks.ts'; +import { getSessionsForSpeaker } from '#lib/server/getSessionsForSpeaker.ts'; +import { getSponsors } from '#lib/server/getSponsors.ts'; +import { getSiteUrl } from '#lib/server/config.ts'; +import { getItemByPathOrDefault } from '#lib/server/umbraco/getItemByPath.ts'; +import type { Page, Speaker } from '#lib/types.ts'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params }) => { + const conference = await getConference(params.conference); + + if (!conference) { + throw error(404); + } + + const item = await getItemByPathOrDefault( + `${params.conference}/${params.slug}`, + { expand: 'properties[$all]' }, + ); + + if (!item) { + throw error(404); + } + + const siteUrl = getSiteUrl().toString(); + + if (item.contentType === 'speaker') { + const [layoutSponsors, sessions] = await Promise.all([ + getSponsors(conference.id), + getSessionsForSpeaker(conference.id, item.id), + ]); + + return { + conferenceSlug: params.conference, + conference, + item: item as Speaker, + layoutSponsors, + sessions, + sharedHead: buildSpeakerHead({ + speaker: item as Speaker, + siteUrl, + }), + }; + } + + if (item.contentType === 'page') { + const blocks = getPageBlocks(item); + const [layoutSponsors, pageData]: [ + Awaited>, + AdditionalPageData, + ] = await Promise.all([ + getSponsors(conference.id), + getPageDataForBlocks(conference, blocks), + ]); + + return { + conferenceSlug: params.conference, + conference, + item: item as Page, + layoutSponsors, + ...pageData, + sharedHead: buildGenericPageHead({ + page: item as Page, + siteUrl, + }), + }; + } + + throw error(404); +}; diff --git a/src/routes/[conference]/[...slug]/+page.svelte b/src/routes/[conference]/[...slug]/+page.svelte new file mode 100644 index 0000000..69bc9e4 --- /dev/null +++ b/src/routes/[conference]/[...slug]/+page.svelte @@ -0,0 +1,36 @@ + + + + {#if data.item.contentType === 'speaker'} + + {:else} + + {/if} + diff --git a/src/routes/[conference]/register/+page.server.ts b/src/routes/[conference]/register/+page.server.ts new file mode 100644 index 0000000..7776d47 --- /dev/null +++ b/src/routes/[conference]/register/+page.server.ts @@ -0,0 +1,20 @@ +import { error } from '@sveltejs/kit'; +import { getConference } from '#lib/server/getConference.ts'; +import { getSponsors } from '#lib/server/getSponsors.ts'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params }) => { + const conference = await getConference(params.conference); + const registerUrl = conference?.properties.registerUrl; + + if (!conference || !registerUrl) { + throw error(404); + } + + return { + conferenceSlug: params.conference, + conference, + registerUrl, + layoutSponsors: await getSponsors(conference.id), + }; +}; diff --git a/src/routes/[conference]/register/+page.svelte b/src/routes/[conference]/register/+page.svelte new file mode 100644 index 0000000..50af4b1 --- /dev/null +++ b/src/routes/[conference]/register/+page.svelte @@ -0,0 +1,21 @@ + + + +
+
+ +
+
+
diff --git a/src/routes/[conference]/register/success/+page.server.ts b/src/routes/[conference]/register/success/+page.server.ts new file mode 100644 index 0000000..76639b0 --- /dev/null +++ b/src/routes/[conference]/register/success/+page.server.ts @@ -0,0 +1,19 @@ +import { error } from '@sveltejs/kit'; +import { getConference } from '#lib/server/getConference.ts'; +import { getSponsors } from '#lib/server/getSponsors.ts'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params }) => { + const conference = await getConference(params.conference); + + if (!conference) { + throw error(404); + } + + return { + conferenceSlug: params.conference, + conference, + layoutSponsors: await getSponsors(conference.id), + robotsNoIndex: true, + }; +}; diff --git a/src/routes/[conference]/register/success/+page.svelte b/src/routes/[conference]/register/success/+page.svelte new file mode 100644 index 0000000..4dc3766 --- /dev/null +++ b/src/routes/[conference]/register/success/+page.svelte @@ -0,0 +1,38 @@ + + + +
+
+

+ Thank You for Registering +

+

+ We’ve sent a confirmation email to your inbox. +

+

+ If you don't receive that email or have any other issues please + email info@methodconf.com +

+
+ + Back Home + +
+
+
diff --git a/tests/route-smoke.mjs b/tests/route-smoke.mjs index ca4eb8c..b7204d8 100644 --- a/tests/route-smoke.mjs +++ b/tests/route-smoke.mjs @@ -12,14 +12,41 @@ const APPLE_APP_STORE_LINK = 'https://apps.apple.com/us/app/method-conf/id1498359521'; const NEWSLETTER_LIST_ID = 'fake-newsletter-list'; -const conference = (slug, date, callForSpeakersUrl) => ({ - contentType: 'conference', - name: slug, - route: { path: `/${slug}/` }, - properties: { +const contentBase = (id, contentType, name, path, properties = {}) => ({ + id, + contentType, + name, + createDate: '2024-01-01T00:00:00Z', + updateDate: '2024-01-01T00:00:00Z', + route: { path, startItem: { id: 'root', path: '/' } }, + properties, +}); + +const conference = (slug, date, callForSpeakersUrl, registerUrl) => + contentBase(slug, 'conference', slug, `/${slug}/`, { date, callForSpeakersUrl, - }, + registerUrl, + }); + +const block = (id, contentType, properties = {}) => ({ + id, + contentType, + properties, +}); + +const blocks = (...items) => ({ + items: items.map((content) => ({ content })), +}); + +const image = (url) => ({ + id: `${url}-id`, + name: url, + mediaType: 'Image', + url, + width: 1200, + height: 630, + properties: null, }); const imageFixture = await sharp({ @@ -35,15 +62,115 @@ const imageFixture = await sharp({ const largeImagePlaceholderBytes = 16 * 1024 * 1024; -const state = { - conferences: [ - conference('2023', '2023-09-01T09:00:00Z'), - conference( - '2024', - '2024-09-01T09:00:00Z', - 'https://sessionize.example/method-2024/', +const contentItems = [ + contentBase('home-2024', 'home', 'Home', '/2024/home/', { + title: 'Home', + metaDescription: 'Conference home description', + openGraphImage: [image('/media/home-og.jpg')], + blocks: blocks( + block('intro-block', 'introAndEmailSignupBlock'), + block('schedule-block', 'scheduleBlock'), + block('location-block', 'locationBlock'), + block('sponsors-block', 'sponsorsBlock'), ), - ], + }), + contentBase( + 'conduct-page', + 'page', + 'Code of Conduct', + '/2024/code-of-conduct/', + { + title: 'Code of Conduct', + metaDescription: 'Code page description', + openGraphImage: [image('/media/code-og.jpg')], + blocks: blocks( + block('rich-text-block', 'richText', { + text: { + markup: '

Be kind to each other.

', + blocks: [], + }, + }), + ), + }, + ), + contentBase( + 'speaker-ada', + 'speaker', + 'Ada Lovelace', + '/2024/speakers/ada/', + { + jobTitle: 'Computer scientist', + profileImage: [image('/media/ada.jpg')], + bio: { markup: '

Ada wrote about computing.

', blocks: [] }, + websiteUrl: 'https://example.test/ada', + }, + ), + contentBase('sessions-root', 'sessions', 'Sessions', '/2024/sessions/'), + contentBase( + 'session-1', + 'session', + 'Opening keynote', + '/2024/sessions/opening/', + { + start: '2024-09-01T09:00:00Z', + end: '2024-09-01T10:00:00Z', + speakers: [ + contentBase( + 'speaker-ada', + 'speaker', + 'Ada Lovelace', + '/2024/speakers/ada/', + { + jobTitle: 'Computer scientist', + profileImage: [image('/media/ada.jpg')], + }, + ), + ], + description: { markup: '

Opening remarks.

', blocks: [] }, + }, + ), + contentBase('sponsors-root', 'sponsors', 'Sponsors', '/2024/sponsors/', { + tiers: { + items: [ + { + content: block('sponsor-tier', 'sponsorTier', { + title: 'Community', + sponsors: { + items: [ + { + content: block('sponsor-acme', 'sponsor', { + title: 'Acme', + url: 'https://example.test', + logo: [image('/media/acme.svg')], + }), + }, + ], + }, + }), + }, + ], + }, + }), + contentBase( + 'unsupported', + 'sessions', + 'Sessions leaf', + '/2024/not-a-page/', + ), +]; + +const defaultConferences = () => [ + conference('2023', '2023-09-01T09:00:00Z'), + conference( + '2024', + '2024-09-01T09:00:00Z', + 'https://sessionize.example/method-2024/', + 'https://tickets.example/embed?id=5b12c452dc', + ), +]; + +const state = { + conferences: defaultConferences(), mediaRequests: [], newsletterRequests: [], cmsRequests: 0, @@ -76,9 +203,36 @@ async function startUpstream() { if (url.pathname === '/umbraco/delivery/api/v2/content') { state.cmsRequests += 1; + const filters = url.searchParams.getAll('filter'); + const fetch = url.searchParams.get('fetch') ?? ''; + let items = state.conferences; + + if (fetch.startsWith('descendants:2024')) { + items = contentItems; + } + + if (fetch.startsWith('descendants:sessions-root')) { + items = contentItems.filter((item) => + ['session', 'track'].includes(item.contentType), + ); + } + + for (const filter of filters) { + if (filter.startsWith('contentType:')) { + const contentType = filter.replace('contentType:', ''); + items = items.filter( + (item) => item.contentType === contentType, + ); + } + + if (filter === 'speaker:speaker-ada') { + items = items.filter((item) => item.id === 'session-1'); + } + } + writeJson(response, 200, { - total: state.conferences.length, - items: state.conferences, + total: items.length, + items, }); return; } @@ -101,7 +255,15 @@ async function startUpstream() { return; } - const item = state.conferences.find((item) => item.name === slug); + const byPath = new Map([ + ...state.conferences.map((item) => [item.name, item]), + ...contentItems.map((item) => [ + item.route.path.replace(/^\//, '').replace(/\/$/, ''), + item, + ]), + ['2024/home', contentItems[0]], + ]); + const item = byPath.get(slug.replace(/\/$/, '')); if (!item) { writeJson(response, 404, { message: 'not found' }); @@ -112,6 +274,13 @@ async function startUpstream() { return; } + if (url.pathname === '/api/v1/conference/2024/schedule') { + writeJson(response, 200, { + scheduleGrid: [['session-1']], + }); + return; + } + if (url.pathname === '/api/public/subscription') { let body = ''; request.setEncoding('utf8'); @@ -479,18 +648,56 @@ async function runRouteChecks(origin) { (await request(origin, '/2024/sessions/not-a-page/')).status, 404, ); - assert.equal((await request(origin, '/sitemap.xml')).status, 404); + assert.equal((await request(origin, '/2024/not-a-page/')).status, 404); + await expectRedirect(origin, '/sitemap.xml', 308, './sitemap.xml/'); + assert.equal((await request(origin, '/sitemap.xml/')).status, 404); + + const home = await expectHead(origin, '/2024/', 200); + assert.match(home.html, /Invest In Yourself and Hone Your Craft/); + assert.match(home.html, /Stay in-the-know about event updates/); + assert.match(home.html, /Schedule/); + assert.match(home.html, /Location Info/); + assert.match(home.html, /Sponsors/); + assert.match(home.head, /Method Conference - Home<\/title>/); + assert.match( + home.head, + /<meta name="description" content="Conference home description"/, + ); + assert.match( + home.head, + /<meta property="og:image" content="https:\/\/www\.example\.test\/cms-media\/media\/home-og\.jpg\?width=1200&height=630"/, + ); + assert.equal(countHeadTags(home.head, /property="og:image:type"/g), 0); + + const generic = await expectHead(origin, '/2024/code-of-conduct/', 200); + assert.match(generic.html, /Be kind to each other\./); + assert.match(generic.html, /href="\/2024\/register\/"/); + assert.match( + generic.head, + /<title>Method Conference - Code of Conduct<\/title>/, + ); + + const speaker = await expectHead(origin, '/2024/speakers/ada/', 200); + assert.match(speaker.html, /Ada Lovelace/); + assert.match(speaker.html, /Opening keynote/); + assert.match(speaker.html, /Ada Lovelace Website/); + assert.match( + speaker.head, + /<title>Method Conference - Ada Lovelace<\/title>/, + ); + + const register = await expectHead(origin, '/2024/register/', 200); + assert.match(register.html, /Loading\.\.\./); + assert.doesNotMatch(register.html, /<iframe/); + + const success = await expectHead(origin, '/2024/register/success/', 200); + assert.match(success.html, /Thank You for Registering/); + assert.match(success.html, /href="\/2024\/"/); + assert.match(success.head, /<meta name="robots" content="noindex"/); state.conferences = []; assert.equal((await request(origin, '/')).status, 404); - state.conferences = [ - conference('2023', '2023-09-01T09:00:00Z'), - conference( - '2024', - '2024-09-01T09:00:00Z', - 'https://sessionize.example/method-2024/', - ), - ]; + state.conferences = defaultConferences(); } async function runNewsletterChecks(origin) { @@ -591,8 +798,8 @@ async function runHeadChecks(origin, { searchIndexingEnabled }) { ); } - const notFound = await expectHead(origin, '/2024/nope/', 404); - assertDefaultSharedHead(notFound.head, '/2024/nope/'); + const notFound = await expectHead(origin, '/missing/', 404); + assertDefaultSharedHead(notFound.head, '/missing/'); assert.equal(countHeadTags(notFound.head, /name="robots"/g), 1); assert.match(notFound.head, /<meta name="robots" content="noindex"/); assert.equal(countHeadTags(notFound.head, /property="og:image"/g), 1); @@ -602,7 +809,7 @@ async function runHeadChecks(origin, { searchIndexingEnabled }) { assert.ok(footerIndex > shellIndex, '404 footer should render below shell'); const notFoundCss = await stylesheetText( origin, - '/2024/nope/', + '/missing/', notFound.head, ); assert.match(notFoundCss, /height:\s*100vh/); @@ -626,6 +833,7 @@ async function runRobotChecks(upstreamUrl) { response.headers.get('content-type') ?? '', /^text\/plain/, ); + assert.equal(state.cmsRequests, before, 'robots should not hit CMS'); await runHeadChecks(enabledApp.origin, { searchIndexingEnabled: true, }); @@ -638,19 +846,24 @@ async function runRobotChecks(upstreamUrl) { searchIndexingEnabled: false, }); try { + const beforeDisabledRobots = state.cmsRequests; await expectText( disabledApp.origin, '/robots.txt', 200, 'User-Agent: *\nDisallow: /\n\n', ); + assert.equal( + state.cmsRequests, + beforeDisabledRobots, + 'robots should not hit CMS', + ); await runHeadChecks(disabledApp.origin, { searchIndexingEnabled: false, }); } finally { await disabledApp.close(); } - assert.equal(state.cmsRequests, before, 'robots should not hit CMS'); } async function runImageOptimizerChecks(origin) { diff --git a/tests/unit/oveit.test.ts b/tests/unit/oveit.test.ts new file mode 100644 index 0000000..07f2b1b --- /dev/null +++ b/tests/unit/oveit.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { buildOveItIframeState } from '#lib/oveit.ts'; + +describe('OveIt embed URL handling', () => { + it('passes matching return hash path as next without changing the parent hash', () => { + const state = buildOveItIframeState( + 'https://tickets.example/embed?id=5b12c452dc&theme=method', + '#5b12c452dc/order/return', + ); + + expect(state?.embedId).toBe('5b12c452dc'); + expect(state?.iframeUrl.searchParams.get('next')).toBe('order'); + expect(state?.iframeUrl.toString()).toBe( + 'https://tickets.example/embed?id=5b12c452dc&theme=method&next=order', + ); + }); + + it('ignores hashes for a different embed and invalid embed URLs', () => { + const state = buildOveItIframeState( + 'https://tickets.example/embed?id=current', + '#other/order/return', + ); + + expect(state?.iframeUrl.searchParams.get('next')).toBeNull(); + expect( + buildOveItIframeState('not a url', '#current/order'), + ).toBeUndefined(); + }); +}); From 3f7720216acd48dca823418a1db5552c6b525963 Mon Sep 17 00:00:00 2001 From: Levi Zitting <me@levizitting.com> Date: Sun, 4 Oct 2026 07:02:26 -0500 Subject: [PATCH 2/2] fix: refresh registration embed when its URL changes --- src/lib/components/OveItEmbed.svelte | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/lib/components/OveItEmbed.svelte b/src/lib/components/OveItEmbed.svelte index 5fdfdcb..ed87df0 100644 --- a/src/lib/components/OveItEmbed.svelte +++ b/src/lib/components/OveItEmbed.svelte @@ -34,19 +34,16 @@ iframe.style.height = `${Number.isNaN(parsedHeight) ? 800 : parsedHeight}px`; } - onMount(() => { + $effect(() => { const iframeState = buildOveItIframeState( embedUrl, window.location.hash, ); + embedId = iframeState?.embedId; + iframeSrc = iframeState?.iframeUrl.toString(); + }); - if (!iframeState) { - return; - } - - embedId = iframeState.embedId; - iframeSrc = iframeState.iframeUrl.toString(); - + onMount(() => { function onWindowMessage(event: MessageEvent) { if (event.source !== iframe?.contentWindow) { return;