feat: add design system, theme toggle, SEO head and base layout
- Huemint Magazine tokens for light/dark themes with AA-safe accent variants - FOUC-free theme init + toggle (localStorage, follows OS until chosen) - Seo component (canonical, OG, Twitter, WebSite/Person JSON-LD) - Header with popover mobile menu, 4-column footer with social links - Container, GridLines, SectionHeading, Button, Badge, Card, ExternalLink - site.json / social.json validated with zod - dev-only /styleguide preview Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Typed, validated access to src/data/*.json.
|
||||
* A schema mismatch throws at import time, so `astro build` fails loudly.
|
||||
*/
|
||||
import { z } from 'astro/zod';
|
||||
import siteJson from '../data/site.json';
|
||||
import socialJson from '../data/social.json';
|
||||
|
||||
const link = z.object({ label: z.string().min(1), href: z.string().min(1) });
|
||||
|
||||
const siteSchema = z.object({
|
||||
name: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string().max(160),
|
||||
url: z.url(),
|
||||
lang: z.string(),
|
||||
locale: z.string(),
|
||||
titleSeparator: z.string(),
|
||||
author: z.object({ name: z.string(), callsign: z.string(), locator: z.string() }),
|
||||
postsPerPage: z.number().int().positive(),
|
||||
nav: z.array(link).min(1),
|
||||
footer: z.object({
|
||||
about: z.string(),
|
||||
quickLinksTitle: z.string(),
|
||||
legalTitle: z.string(),
|
||||
socialTitle: z.string(),
|
||||
rss: link,
|
||||
legal: z.array(link),
|
||||
license: z.string(),
|
||||
signoff: z.string(),
|
||||
}),
|
||||
ui: z.object({
|
||||
skipToContent: z.string(),
|
||||
mainNav: z.string(),
|
||||
footerNav: z.string(),
|
||||
menuOpen: z.string(),
|
||||
menuClose: z.string(),
|
||||
themeToggle: z.string(),
|
||||
newTab: z.string(),
|
||||
home: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const socialSchema = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
// Empty string = account not set → hidden in the UI.
|
||||
href: z.union([z.url(), z.literal('')]),
|
||||
icon: z.string().startsWith('tabler:'),
|
||||
handle: z.string().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
function parse<T>(name: string, schema: z.ZodType<T>, data: unknown): T {
|
||||
const result = schema.safeParse(data);
|
||||
if (!result.success) {
|
||||
throw new Error(`src/data/${name} is invalid:\n${z.prettifyError(result.error)}`);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export const site = parse('site.json', siteSchema, siteJson);
|
||||
export const social = parse('social.json', socialSchema, socialJson).filter((s) => s.href);
|
||||
|
||||
export type Site = z.infer<typeof siteSchema>;
|
||||
export type SocialLink = (typeof social)[number];
|
||||
Reference in New Issue
Block a user