feat: build home page from home.json sections

- Hero with callsign/locator tags and CTAs on khaki grid
- Feature sections for ARD (YM1KTC), DMR Türkiye, QRV73!, QRZ73!
- Telegram groups and resource site card grids
- Latest posts block with empty state (wired to blog in stage 4)
- Section wrapper, decorative numerals, external Button variant

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 00:55:33 +03:00
parent d8a79ccbe3
commit f2d943c31b
10 changed files with 349 additions and 15 deletions
+62 -1
View File
@@ -5,8 +5,10 @@
import { z } from 'astro/zod';
import siteJson from '../data/site.json';
import socialJson from '../data/social.json';
import homeJson from '../data/home.json';
const link = z.object({ label: z.string().min(1), href: z.string().min(1) });
const icon = z.string().startsWith('tabler:');
const siteSchema = z.object({
name: z.string(),
@@ -47,11 +49,68 @@ const socialSchema = z.array(
label: z.string(),
// Empty string = account not set → hidden in the UI.
href: z.union([z.url(), z.literal('')]),
icon: z.string().startsWith('tabler:'),
icon,
handle: z.string().optional(),
}),
);
const homeSchema = z.object({
seo: z.object({ description: z.string().max(160) }),
hero: z.object({
numeral: z.string(),
tags: z.array(z.object({ icon, label: z.string() })),
asideTitle: z.string(),
asideBody: z.string(),
asideLink: link,
eyebrow: z.string(),
title: z.string(),
lead: z.string(),
ctas: z.array(
link.extend({ variant: z.enum(['primary', 'secondary', 'link']), icon: icon.optional() }),
),
}),
sections: z.array(
z.discriminatedUnion('type', [
z.object({
type: z.literal('feature'),
id: z.string(),
eyebrow: z.string(),
title: z.string(),
body: z.string(),
callsign: z.string().optional(),
callsignLabel: z.string().optional(),
icon,
link: link.extend({ href: z.url() }),
}),
z.object({
type: z.literal('cards'),
id: z.string(),
eyebrow: z.string(),
title: z.string(),
items: z
.array(
z.object({
title: z.string(),
description: z.string().optional(),
href: z.url(),
icon,
}),
)
.min(1),
}),
z.object({
type: z.literal('latestPosts'),
id: z.string(),
eyebrow: z.string(),
title: z.string(),
count: z.number().int().positive(),
allLink: link,
empty: z.string(),
}),
]),
),
});
function parse<T>(name: string, schema: z.ZodType<T>, data: unknown): T {
const result = schema.safeParse(data);
if (!result.success) {
@@ -61,7 +120,9 @@ function parse<T>(name: string, schema: z.ZodType<T>, data: unknown): T {
}
export const site = parse('site.json', siteSchema, siteJson);
export const home = parse('home.json', homeSchema, homeJson);
export const social = parse('social.json', socialSchema, socialJson).filter((s) => s.href);
export type Site = z.infer<typeof siteSchema>;
export type HomeSection = (typeof home.sections)[number];
export type SocialLink = (typeof social)[number];