36288befe1
The article template only rendered the H1 it could pluck from the markdown body (`contentHeadlineHtml`). Posts whose body opens with H2 (or no heading) silently lost both the schema-level `headline` and `subheadline` — they were used in SEO/JSON-LD/breadcrumb only. Fall back to the structured fields when no inline H1 is present.
126 lines
3.9 KiB
Svelte
126 lines
3.9 KiB
Svelte
<script lang="ts">
|
|
import ContentRows from '$lib/components/ContentRows.svelte';
|
|
import Tag from '$lib/components/Tag.svelte';
|
|
import Tags from '$lib/components/Tags.svelte';
|
|
import EventBadges from '$lib/components/EventBadges.svelte';
|
|
import EventMap from '$lib/components/EventMap.svelte';
|
|
import Accordion from '$lib/components/Accordion.svelte';
|
|
import RustyImage from '$lib/components/RustyImage.svelte';
|
|
import { t, T } from '$lib/translations';
|
|
import type { PageData } from './$types';
|
|
|
|
let { data }: { data: PageData } = $props();
|
|
|
|
function executeScripts(node: HTMLElement) {
|
|
const scripts = node.querySelectorAll("script");
|
|
scripts.forEach((old) => {
|
|
const s = document.createElement("script");
|
|
for (const { name, value } of old.attributes) s.setAttribute(name, value);
|
|
s.text = old.textContent ?? "";
|
|
old.replaceWith(s);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{data.seoTitle}</title>
|
|
{#if data.seoDescription}
|
|
<meta name="description" content={data.seoDescription} />
|
|
{/if}
|
|
{#if data.postImageUrl}
|
|
<meta property="og:image" content={data.postImageUrl} />
|
|
{/if}
|
|
</svelte:head>
|
|
|
|
<div class="md:flex gap-2 items-end">
|
|
{#if data.rawPostImageUrl}
|
|
<div class="overflow-hidden inline-block my-4 border border-white self-start ring-1 ring-black/50 shadow-lg">
|
|
<RustyImage
|
|
src={data.rawPostImageUrl}
|
|
focal={data.postImageFocal}
|
|
width={150}
|
|
height={200}
|
|
fit="cover"
|
|
alt=""
|
|
class="block object-cover"
|
|
loading="eager"
|
|
fetchpriority="high"
|
|
/>
|
|
</div>
|
|
{:else if data.postImageUrl}
|
|
<div class="overflow-hidden inline-block my-4 border border-white self-start ring-1 ring-black/50 shadow-lg">
|
|
<img
|
|
src={data.postImageUrl}
|
|
alt=""
|
|
class="block object-cover"
|
|
width="150"
|
|
height="200"
|
|
style="aspect-ratio: 1.3333"
|
|
loading="eager"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
<div class="mt-2 mb-4! md:mb-1 min-w-0 gap-2">
|
|
{#if data.postDate}
|
|
<div class="flex flex-nowrap gap-2 items-center shrink-0 mb-2">
|
|
<Tag label={data.postDate} variant="date" />
|
|
</div>
|
|
{/if}
|
|
<div class="flex flex-nowrap gap-2 items-center shrink-0 p-1 pb-4 -ml-1 overflow-x-auto overflow-y-hidden">
|
|
<Tags tags={data.post.postTag ?? []} variant="green" tagBase="/posts/tag" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<article class="mt-8">
|
|
{#if data.contentHeadlineHtml}
|
|
<div class="content markdown max-w-none prose prose-zinc">
|
|
{@html data.contentHeadlineHtml}
|
|
</div>
|
|
{:else if data.post.headline || data.post.subheadline}
|
|
<header class="content markdown max-w-none prose prose-zinc mb-4">
|
|
{#if data.post.headline}
|
|
<h1>{data.post.headline}</h1>
|
|
{/if}
|
|
{#if data.post.subheadline}
|
|
<p class="text-zinc-600 lead">{data.post.subheadline}</p>
|
|
{/if}
|
|
</header>
|
|
{/if}
|
|
{#if data.isEvent && (data.eventDateLabel || data.eventLocationText)}
|
|
<div class="mb-6">
|
|
<EventBadges
|
|
eventDate={data.eventDateRaw}
|
|
locationText={data.eventLocationText}
|
|
locationHref={(data.mapEmbedUrl || data.mapLinkUrl) ? '#map-section' : null}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{#if data.contentBodyHtml}
|
|
<div class="content markdown max-w-none prose prose-zinc">
|
|
{@html data.contentBodyHtml}
|
|
</div>
|
|
{/if}
|
|
{#if data.hasRowContent}
|
|
<ContentRows layout={data.post} translations={data.translations} />
|
|
{/if}
|
|
</article>
|
|
|
|
{#if data.isEvent && (data.mapEmbedUrl || data.mapLinkUrl)}
|
|
<EventMap
|
|
embedUrl={data.mapEmbedUrl}
|
|
linkUrl={data.mapLinkUrl}
|
|
locationText={data.eventLocationText}
|
|
label={t(data.translations, T.post_map)}
|
|
translations={data.translations}
|
|
/>
|
|
{/if}
|
|
|
|
{#if data.commentSlot}
|
|
<Accordion label={t(data.translations, T.post_comments)} class="mt-6">
|
|
<div class="p-8 border border-zinc-200 rounded-lg" use:executeScripts>
|
|
{@html data.commentSlot}
|
|
</div>
|
|
</Accordion>
|
|
{/if}
|