- Install @sanity/client + @portabletext/to-html - studio/ — Sanity v3 project with post schema (title, slug, date, category, summary, cover image with hotspot, Portable Text body). Run with `cd studio && npm install && sanity dev` after setting SANITY_STUDIO_PROJECT_ID. - src/lib/sanity.ts — typed client, GROQ queries, Portable Text → HTML conversion - src/lib/news.ts — adds getArticles() / getArticle() async functions that prefer Sanity when SANITY_PROJECT_ID is set; fall back to local markdown otherwise - events/index.astro, events/[slug].astro, events/page/[page].astro — switch to async getArticles()/getArticle(); [slug].astro drops getStaticPaths() in favour of SSR request-time fetch with redirect-on-not-found - .env.example — documents SANITY_PROJECT_ID / SANITY_DATASET / SANITY_API_TOKEN Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
import { format } from "date-fns";
|
|
import { marked } from "marked";
|
|
import { isSanityConfigured, getSanityPosts, getSanityPost } from "./sanity";
|
|
|
|
export type NewsArticle = {
|
|
title: string;
|
|
date: string;
|
|
summary: string;
|
|
image?: string;
|
|
slug: string;
|
|
category: string;
|
|
body: string;
|
|
html: string;
|
|
};
|
|
|
|
type Frontmatter = Partial<
|
|
Pick<NewsArticle, "title" | "date" | "summary" | "image" | "slug" | "category">
|
|
>;
|
|
|
|
const newsModules = import.meta.glob("/src/content/news/*.md", {
|
|
eager: true,
|
|
as: "raw",
|
|
});
|
|
|
|
const parseFrontmatter = (raw: string): { frontmatter: Frontmatter; body: string } => {
|
|
const frontmatterMatch = raw.match(/^---\s*([\s\S]*?)\s*---\s*([\s\S]*)$/);
|
|
if (!frontmatterMatch) {
|
|
return { frontmatter: {}, body: raw.trim() };
|
|
}
|
|
|
|
const [, frontmatterBlock, body] = frontmatterMatch;
|
|
const frontmatter: Frontmatter = {};
|
|
|
|
frontmatterBlock.split("\n").forEach((line) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) return;
|
|
const [key, ...rest] = trimmed.split(":");
|
|
if (!key || rest.length === 0) return;
|
|
const value = rest.join(":").trim().replace(/^["']|["']$/g, "");
|
|
if (key in frontmatter) return;
|
|
(frontmatter as Record<string, string>)[key.trim()] = value;
|
|
});
|
|
|
|
return { frontmatter, body: body.trim() };
|
|
};
|
|
|
|
export const newsArticles: NewsArticle[] = Object.entries(newsModules)
|
|
.map(([path, raw]) => {
|
|
const { frontmatter, body } = parseFrontmatter(raw as string);
|
|
const slug = frontmatter.slug || path.split("/").pop()?.replace(/\.md$/, "") || "";
|
|
|
|
return {
|
|
title: frontmatter.title || slug,
|
|
date: frontmatter.date || "",
|
|
summary: frontmatter.summary || "",
|
|
image: frontmatter.image,
|
|
slug,
|
|
category: frontmatter.category || "News",
|
|
body,
|
|
html: marked.parse(body) as string,
|
|
};
|
|
})
|
|
.sort((a, b) => {
|
|
const aDate = Date.parse(a.date);
|
|
const bDate = Date.parse(b.date);
|
|
if (Number.isNaN(aDate) || Number.isNaN(bDate)) {
|
|
return b.title.localeCompare(a.title);
|
|
}
|
|
return bDate - aDate;
|
|
});
|
|
|
|
export const getNewsArticle = (slug: string): NewsArticle | undefined =>
|
|
newsArticles.find((article) => article.slug === slug);
|
|
|
|
export const formatNewsDate = (date: string): string => {
|
|
const parsed = Date.parse(date);
|
|
if (Number.isNaN(parsed)) return date;
|
|
return format(new Date(parsed), "MMM yyyy");
|
|
};
|
|
|
|
// ── Async data access (prefers Sanity when configured) ────────────────────────
|
|
|
|
/**
|
|
* Returns all articles. Uses Sanity when SANITY_PROJECT_ID is set,
|
|
* otherwise falls back to the local markdown files.
|
|
*/
|
|
export async function getArticles(): Promise<NewsArticle[]> {
|
|
if (isSanityConfigured()) {
|
|
return getSanityPosts();
|
|
}
|
|
return newsArticles;
|
|
}
|
|
|
|
/**
|
|
* Returns a single article by slug. Uses Sanity when SANITY_PROJECT_ID is set,
|
|
* otherwise falls back to the local markdown files.
|
|
*/
|
|
export async function getArticle(slug: string): Promise<NewsArticle | undefined> {
|
|
if (isSanityConfigured()) {
|
|
return getSanityPost(slug);
|
|
}
|
|
return getNewsArticle(slug);
|
|
}
|