Files
iptvnator/apps/website/src/content.config.ts
T
4grayandClaude Fable 5.1 812ec69fd4 feat(website): turn blog tags into topic hubs
Tags were decorative: a plain span on every card and post header, with no
page, filter or search behind them, and a vocabulary of 32 slugs across 16
posts where 22 slugs appeared once. They now form a closed vocabulary of ten
topics (src/lib/blog-tags.ts) that the content collection schema enforces, so
an unknown slug fails the build instead of minting a new tag.

Every used tag gets a hub at /blog/tag/<tag>/ with CollectionPage and
BreadcrumbList structured data; the blog index and the hubs show a "Topics"
rail with post counts; and every chip links to its hub. The cards become
<article> elements with the title link stretched over the card, because chip
links cannot nest inside a card that is one big <a>.

Posts are retagged to the new vocabulary. A structural test covers the rail,
each hub, the chip targets, the sitemap and the absence of nested anchors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 21:51:26 +02:00

24 lines
922 B
TypeScript

import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
import { BLOG_TAG_SLUGS } from './lib/blog-tags';
const blog = defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
author: z.string().default('4gray'),
featured: z.boolean().default(false),
heroImage: z.string().optional(),
/** Closed vocabulary: each slug is a hub page at `/blog/tag/<slug>/` (see `lib/blog-tags.ts`). */
tags: z.array(z.enum(BLOG_TAG_SLUGS)).default([]),
draft: z.boolean().default(false),
/** Rendered as an accordion after the post and emitted as FAQPage JSON-LD. */
faq: z.array(z.object({ q: z.string(), a: z.string() })).optional(),
}),
});
export const collections = { blog };