feat(post): add h-entry microformat + tag-name keywords fallback
Deploy / verify (push) Failing after 44s
Deploy / deploy (push) Has been skipped

Microformats:
- Wrap the post <article> in h-entry with p-name on the headline,
  p-summary on the subheadline (or sr-only fallback from excerpt),
  e-content on the rendered body, dt-published on the created date
  (hidden time element) and an invisible p-author h-card. Tag names
  are mirrored as hidden p-category entries so crawlers see them
  even though the visual Tags component already links them.
- itemscope/itemtype maps to schema.org BlogPosting (or Event when
  `isEvent`), aligning the invisible microdata with the existing
  JSON-LD.

Keywords:
- When post.seoKeywords is empty, fall back to the comma-joined tag
  names so <meta name="keywords"> still ships something meaningful.
This commit is contained in:
Peter Meier
2026-04-24 11:04:14 +02:00
parent 36288befe1
commit 5b82d37be4
2 changed files with 43 additions and 6 deletions
+11 -1
View File
@@ -232,7 +232,17 @@ export const load: PageServerLoad = async ({ params, locals, url, setHeaders })
seoDescription: postDescription,
socialImage: postSocialImageAbs ?? postImageUrl ?? undefined,
robots: (post as { seoMetaRobots?: string }).seoMetaRobots ?? undefined,
keywords: (post as { seoKeywords?: string }).seoKeywords ?? undefined,
keywords: (() => {
const explicit = ((post as { seoKeywords?: string }).seoKeywords ?? '').trim();
if (explicit) return explicit;
// Fallback: join tag names so published posts without explicit keywords
// still ship a meaningful <meta name="keywords">.
const tags = (post as { postTag?: Array<{ name?: string }> }).postTag ?? [];
const names = tags
.map((t) => (typeof t?.name === 'string' ? t.name.trim() : ''))
.filter(Boolean);
return names.length > 0 ? names.join(', ') : undefined;
})(),
jsonLd,
breadcrumbItems: [
{ href: '/', label: 'Start' },
+32 -5
View File
@@ -72,21 +72,36 @@
</div>
</div>
<article class="mt-8">
<article
class="mt-8 h-entry"
itemscope
itemtype={data.isEvent ? 'https://schema.org/Event' : 'https://schema.org/BlogPosting'}
>
<a class="u-url" href="#" aria-hidden="true" hidden></a>
{#if data.post._created || data.post.created}
<time
class="dt-published"
datetime={String(data.post.created ?? data.post._created ?? '')}
hidden
>{data.post.created ?? data.post._created}</time>
{/if}
{#if data.contentHeadlineHtml}
<div class="content markdown max-w-none prose prose-zinc">
<div class="content markdown max-w-none prose prose-zinc p-name">
{@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>
<h1 class="p-name">{data.post.headline}</h1>
{/if}
{#if data.post.subheadline}
<p class="text-zinc-600 lead">{data.post.subheadline}</p>
<p class="text-zinc-600 lead p-summary">{data.post.subheadline}</p>
{/if}
</header>
{/if}
{#if !data.post.subheadline && data.post.excerpt}
<p class="p-summary sr-only">{data.post.excerpt}</p>
{/if}
{#if data.isEvent && (data.eventDateLabel || data.eventLocationText)}
<div class="mb-6">
<EventBadges
@@ -97,13 +112,25 @@
</div>
{/if}
{#if data.contentBodyHtml}
<div class="content markdown max-w-none prose prose-zinc">
<div class="content markdown max-w-none prose prose-zinc e-content">
{@html data.contentBodyHtml}
</div>
{/if}
{#if data.hasRowContent}
<ContentRows layout={data.post} translations={data.translations} />
{/if}
{#if (data.post.postTag ?? []).length > 0}
<div hidden>
{#each data.post.postTag ?? [] as tag}
{#if tag && typeof tag === 'object' && 'name' in tag && tag.name}
<span class="p-category">{tag.name}</span>
{/if}
{/each}
</div>
{/if}
<div class="p-author h-card" hidden>
<span class="p-name">{data.post.author ?? 'Bürgerinitiative Vachdorf'}</span>
</div>
</article>
{#if data.isEvent && (data.mapEmbedUrl || data.mapLinkUrl)}