feat: add blog with archives, pagination, search, sharing and RSS
- Content collection (one folder per post) with zod schema - /blog, category and tag archives with /sayfa/<n>/ pagination - Post page: breadcrumbs, date badges, share bar, TOC, DIY notice, code copy, license line, prev/next, BlogPosting + BreadcrumbList JSON-LD - Sidebar: Pagefind search, categories, featured fallback, tag cloud - Sätteri HAST plugins for figure/figcaption and external links - Dual-theme Shiki, themed prose, RSS feed, latest posts on home - Three sample posts marked as examples Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ import { z } from 'astro/zod';
|
||||
import siteJson from '../data/site.json';
|
||||
import socialJson from '../data/social.json';
|
||||
import homeJson from '../data/home.json';
|
||||
import categoriesJson from '../data/categories.json';
|
||||
import blogJson from '../data/blog.json';
|
||||
|
||||
const link = z.object({ label: z.string().min(1), href: z.string().min(1) });
|
||||
const icon = z.string().startsWith('tabler:');
|
||||
@@ -111,6 +113,73 @@ const homeSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
const categoriesSchema = z
|
||||
.array(
|
||||
z.object({
|
||||
slug: z.string().regex(/^[a-z0-9-]+$/),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
icon,
|
||||
}),
|
||||
)
|
||||
.min(1);
|
||||
|
||||
/** Object of required string keys; keeps the key names in the inferred type. */
|
||||
const strings = <const K extends string>(keys: readonly K[]) =>
|
||||
z.object(Object.fromEntries(keys.map((k) => [k, z.string()])) as Record<K, z.ZodString>);
|
||||
|
||||
const blogSchema = z.object({
|
||||
seo: z.object({ title: z.string(), description: z.string().max(160) }),
|
||||
list: strings([
|
||||
'eyebrow',
|
||||
'title',
|
||||
'categoryEyebrow',
|
||||
'tagEyebrow',
|
||||
'tagTitle',
|
||||
'categoryDescription',
|
||||
'tagDescription',
|
||||
'pageTitle',
|
||||
'empty',
|
||||
]),
|
||||
card: strings(['readMore', 'readingTime']),
|
||||
dates: strings(['created', 'updated']),
|
||||
pager: strings(['label', 'first', 'prev', 'next', 'last', 'page']),
|
||||
sidebar: strings([
|
||||
'label',
|
||||
'search',
|
||||
'searchLabel',
|
||||
'searchPlaceholder',
|
||||
'searchNoResults',
|
||||
'searchResults',
|
||||
'searchUnavailable',
|
||||
'categories',
|
||||
'featured',
|
||||
'tags',
|
||||
]),
|
||||
post: strings([
|
||||
'breadcrumbLabel',
|
||||
'toc',
|
||||
'shareLabel',
|
||||
'copyLink',
|
||||
'copied',
|
||||
'nativeShare',
|
||||
'copyCode',
|
||||
'codeCopied',
|
||||
'category',
|
||||
'tags',
|
||||
'adjacentLabel',
|
||||
'prev',
|
||||
'next',
|
||||
'license',
|
||||
'licenseName',
|
||||
'licenseHref',
|
||||
]),
|
||||
diy: z.object({ title: z.string(), body: z.string(), link }),
|
||||
share: z.array(
|
||||
z.object({ id: z.string(), label: z.string(), icon, url: z.string().includes('{url}') }),
|
||||
),
|
||||
});
|
||||
|
||||
function parse<T>(name: string, schema: z.ZodType<T>, data: unknown): T {
|
||||
const result = schema.safeParse(data);
|
||||
if (!result.success) {
|
||||
@@ -121,8 +190,16 @@ 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 categories = parse('categories.json', categoriesSchema, categoriesJson);
|
||||
export const blog = parse('blog.json', blogSchema, blogJson);
|
||||
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 Category = (typeof categories)[number];
|
||||
export type SocialLink = (typeof social)[number];
|
||||
|
||||
/** Fills `{key}` placeholders in UI strings from the JSON files. */
|
||||
export function fmt(template: string, values: Record<string, string | number>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (m, k: string) => (k in values ? String(values[k]) : m));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user