mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
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>
24 lines
922 B
TypeScript
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 };
|