feat: add announcements feature with dynamic routing and markdown support
This commit is contained in:
66
app/announcements/[slug]/page.tsx
Normal file
66
app/announcements/[slug]/page.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { getAnnouncementBySlug, getAnnouncementSlugs } from "@/lib/announcements";
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const slugs = await getAnnouncementSlugs();
|
||||||
|
return slugs.map((slug) => ({ slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const announcement = await getAnnouncementBySlug(slug);
|
||||||
|
return {
|
||||||
|
title: `${announcement.title} | お知らせ`,
|
||||||
|
description: announcement.summary || `${announcement.title} のお知らせです。`,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
title: "お知らせ",
|
||||||
|
description: "お知らせページ",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AnnouncementDetailPage({ params }: PageProps) {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
let announcement;
|
||||||
|
try {
|
||||||
|
announcement = await getAnnouncementBySlug(slug);
|
||||||
|
} catch {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="mx-auto flex w-full max-w-4xl flex-1 flex-col gap-6 px-4 py-8 sm:px-8 sm:py-12">
|
||||||
|
<header className="rounded-3xl border bg-card/70 p-6 shadow-sm backdrop-blur sm:p-8">
|
||||||
|
<Badge variant="outline" className="mb-3">
|
||||||
|
お知らせ詳細
|
||||||
|
</Badge>
|
||||||
|
<p className="text-xs text-muted-foreground">{announcement.date}</p>
|
||||||
|
<h1 className="mt-2 text-2xl font-semibold tracking-tight sm:text-4xl">{announcement.title}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="rounded-3xl border bg-card p-6 shadow-sm sm:p-8">
|
||||||
|
<div
|
||||||
|
className="markdown-body space-y-4 text-sm leading-7 sm:text-base [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-2 [&_blockquote]:pl-3 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_h1]:text-2xl [&_h1]:font-semibold [&_h2]:text-xl [&_h2]:font-semibold [&_h3]:text-lg [&_h3]:font-semibold [&_li]:ml-5 [&_ol]:list-decimal [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-3 [&_ul]:list-disc"
|
||||||
|
dangerouslySetInnerHTML={{ __html: announcement.contentHtml }}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Link href="/announcements" className="text-sm text-primary underline underline-offset-4">
|
||||||
|
お知らせ一覧へ戻る
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
app/announcements/page.tsx
Normal file
49
app/announcements/page.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { getAllAnnouncements } from "@/lib/announcements";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "お知らせ | Takasumi-Neodyマイクラサーバプロジェクト",
|
||||||
|
description: "Takasumi-Neodyマイクラサーバプロジェクトのお知らせ一覧です。",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function AnnouncementsPage() {
|
||||||
|
const announcements = await getAllAnnouncements();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-8 sm:px-8 sm:py-12">
|
||||||
|
<header className="rounded-3xl border bg-card/70 p-6 shadow-sm backdrop-blur sm:p-8">
|
||||||
|
<Badge variant="outline" className="mb-3">
|
||||||
|
お知らせ
|
||||||
|
</Badge>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">更新情報・告知</h1>
|
||||||
|
<p className="mt-3 text-sm leading-7 text-muted-foreground sm:text-base">
|
||||||
|
このページは Markdown で作成したお知らせを表示します。
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="grid gap-4">
|
||||||
|
{announcements.map((item) => (
|
||||||
|
<Card key={item.slug} className="border-foreground/10">
|
||||||
|
<CardHeader>
|
||||||
|
<p className="text-xs text-muted-foreground">{item.date}</p>
|
||||||
|
<CardTitle className="text-xl">
|
||||||
|
<Link
|
||||||
|
href={`/announcements/${item.slug}`}
|
||||||
|
className="underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Link>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">{item.summary}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
app/page.tsx
39
app/page.tsx
@@ -1,17 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ExternalLink, Pickaxe, Sprout, Wifi } from "lucide-react";
|
import { ExternalLink, Pickaxe, Sprout, Wifi } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -48,14 +41,22 @@ export default function Home() {
|
|||||||
サーバー追加画面で下記アドレスを入力してください。Java版 と
|
サーバー追加画面で下記アドレスを入力してください。Java版 と
|
||||||
統合版のどちらにも対応しています。
|
統合版のどちらにも対応しています。
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Link href="/announcements" className={cn(buttonVariants({ variant: "outline" }))}>
|
||||||
|
お知らせを見る
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid gap-4 md:grid-cols-2">
|
<section className="grid gap-4 md:grid-cols-2">
|
||||||
{servers.map((server) => {
|
{servers.map((server) => {
|
||||||
const Icon = server.icon;
|
const Icon = server.icon;
|
||||||
return (
|
return (
|
||||||
<Card key={server.name} className="border-foreground/10">
|
<article
|
||||||
<CardHeader>
|
key={server.name}
|
||||||
|
className="rounded-2xl border border-foreground/10 bg-card p-5 shadow-xs sm:p-6"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
<div className="mb-2 flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
<div className="mb-2 flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||||
<Badge variant={server.badgeVariant}>{server.name}</Badge>
|
<Badge variant={server.badgeVariant}>{server.name}</Badge>
|
||||||
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
@@ -63,19 +64,19 @@ export default function Home() {
|
|||||||
Java版 / 統合版 対応
|
Java版 / 統合版 対応
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="flex items-center gap-2 text-xl">
|
<h3 className="flex items-center gap-2 text-xl font-semibold">
|
||||||
<Icon className="size-5" />
|
<Icon className="size-5" />
|
||||||
{server.name}
|
{server.name}
|
||||||
</CardTitle>
|
</h3>
|
||||||
<CardDescription>{server.description}</CardDescription>
|
<p className="mt-1 text-sm text-muted-foreground">{server.description}</p>
|
||||||
</CardHeader>
|
</header>
|
||||||
<CardContent>
|
<div className="mt-5">
|
||||||
<div className="rounded-xl border bg-background px-4 py-3">
|
<div className="rounded-xl border bg-background px-4 py-3">
|
||||||
<p className="text-xs text-muted-foreground">サーバアドレス</p>
|
<p className="text-xs text-muted-foreground">サーバアドレス</p>
|
||||||
<p className="mt-1 break-all font-mono text-sm">{server.address}</p>
|
<p className="mt-1 break-all font-mono text-sm">{server.address}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
<CardFooter className="gap-2">
|
<div className="mt-5 flex gap-2">
|
||||||
<a
|
<a
|
||||||
className={cn(buttonVariants(), "w-full sm:w-auto")}
|
className={cn(buttonVariants(), "w-full sm:w-auto")}
|
||||||
href={`minecraft://?addExternalServer=${server.name}|${server.address}`}
|
href={`minecraft://?addExternalServer=${server.name}|${server.address}`}
|
||||||
@@ -83,8 +84,8 @@ export default function Home() {
|
|||||||
Minecraft で開く
|
Minecraft で開く
|
||||||
<ExternalLink className="size-4" />
|
<ExternalLink className="size-4" />
|
||||||
</a>
|
</a>
|
||||||
</CardFooter>
|
</div>
|
||||||
</Card>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
30
bun.lock
30
bun.lock
@@ -8,7 +8,9 @@
|
|||||||
"@base-ui/react": "^1.3.0",
|
"@base-ui/react": "^1.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
|
"marked": "^17.0.5",
|
||||||
"next": "16.2.1",
|
"next": "16.2.1",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
@@ -391,7 +393,7 @@
|
|||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||||
|
|
||||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
|
|
||||||
@@ -619,6 +621,8 @@
|
|||||||
|
|
||||||
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
|
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
|
||||||
|
|
||||||
|
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
@@ -699,6 +703,8 @@
|
|||||||
|
|
||||||
"graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="],
|
"graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="],
|
||||||
|
|
||||||
|
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
||||||
|
|
||||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
@@ -765,6 +771,8 @@
|
|||||||
|
|
||||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||||
|
|
||||||
|
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||||
|
|
||||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||||
@@ -835,7 +843,7 @@
|
|||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||||
|
|
||||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
@@ -857,6 +865,8 @@
|
|||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
|
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
||||||
@@ -905,6 +915,8 @@
|
|||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"marked": ["marked@17.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||||
@@ -1091,6 +1103,8 @@
|
|||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||||
@@ -1129,6 +1143,8 @@
|
|||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||||
|
|
||||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
@@ -1159,6 +1175,8 @@
|
|||||||
|
|
||||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||||
|
|
||||||
|
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||||
|
|
||||||
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
@@ -1297,6 +1315,8 @@
|
|||||||
|
|
||||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
"@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
@@ -1333,6 +1353,8 @@
|
|||||||
|
|
||||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"cosmiconfig/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
|
|
||||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
@@ -1393,6 +1415,8 @@
|
|||||||
|
|
||||||
"@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
"@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
@@ -1407,6 +1431,8 @@
|
|||||||
|
|
||||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"cosmiconfig/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
"eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||||
|
|
||||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|||||||
17
content/announcements/2026-03-29-project-start.md
Normal file
17
content/announcements/2026-03-29-project-start.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: "お知らせページを公開しました"
|
||||||
|
date: "2026-03-29"
|
||||||
|
summary: "Takasumi-Neodyマイクラサーバプロジェクトのお知らせページを公開しました。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# お知らせページ公開
|
||||||
|
|
||||||
|
Takasumi-Neodyマイクラサーバプロジェクトの **お知らせページ** を公開しました。
|
||||||
|
|
||||||
|
今後の更新情報やイベント告知は、このページで案内します。
|
||||||
|
|
||||||
|
- サーバーのメンテナンス情報
|
||||||
|
- 新機能の追加情報
|
||||||
|
- イベント開催のお知らせ
|
||||||
|
|
||||||
|
最新情報はこのページを確認してください。
|
||||||
93
lib/announcements.ts
Normal file
93
lib/announcements.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { markdownToHtml } from "@/lib/markdown";
|
||||||
|
|
||||||
|
const ANNOUNCEMENTS_DIR = path.join(process.cwd(), "content", "announcements");
|
||||||
|
|
||||||
|
type AnnouncementFrontmatter = {
|
||||||
|
title?: string;
|
||||||
|
date?: string;
|
||||||
|
summary?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnnouncementListItem = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
summary: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnnouncementDetail = AnnouncementListItem & {
|
||||||
|
contentHtml: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseFrontmatter(markdown: string): {
|
||||||
|
frontmatter: AnnouncementFrontmatter;
|
||||||
|
body: string;
|
||||||
|
} {
|
||||||
|
if (!markdown.startsWith("---\n")) {
|
||||||
|
return { frontmatter: {}, body: markdown };
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = markdown.indexOf("\n---\n", 4);
|
||||||
|
if (end === -1) {
|
||||||
|
return { frontmatter: {}, body: markdown };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawFrontmatter = markdown.slice(4, end);
|
||||||
|
const body = markdown.slice(end + 5);
|
||||||
|
const frontmatter: AnnouncementFrontmatter = {};
|
||||||
|
|
||||||
|
for (const line of rawFrontmatter.split("\n")) {
|
||||||
|
const delimiterIndex = line.indexOf(":");
|
||||||
|
if (delimiterIndex <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = line.slice(0, delimiterIndex).trim();
|
||||||
|
const value = line.slice(delimiterIndex + 1).trim().replace(/^"|"$/g, "");
|
||||||
|
|
||||||
|
if (key === "title" || key === "date" || key === "summary") {
|
||||||
|
frontmatter[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { frontmatter, body: body.trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAnnouncementSlugs(): Promise<string[]> {
|
||||||
|
const entries = await fs.readdir(ANNOUNCEMENTS_DIR, { withFileTypes: true });
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
||||||
|
.map((entry) => entry.name.replace(/\.md$/, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAnnouncementBySlug(slug: string): Promise<AnnouncementDetail> {
|
||||||
|
const filePath = path.join(ANNOUNCEMENTS_DIR, `${slug}.md`);
|
||||||
|
const markdown = await fs.readFile(filePath, "utf-8");
|
||||||
|
const { frontmatter, body } = parseFrontmatter(markdown);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
title: frontmatter.title ?? slug,
|
||||||
|
date: frontmatter.date ?? "日付未設定",
|
||||||
|
summary: frontmatter.summary ?? "",
|
||||||
|
contentHtml: markdownToHtml(body),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllAnnouncements(): Promise<AnnouncementListItem[]> {
|
||||||
|
const slugs = await getAnnouncementSlugs();
|
||||||
|
const items = await Promise.all(slugs.map((slug) => getAnnouncementBySlug(slug)));
|
||||||
|
|
||||||
|
return items
|
||||||
|
.map((item) => ({
|
||||||
|
slug: item.slug,
|
||||||
|
title: item.title,
|
||||||
|
date: item.date,
|
||||||
|
summary: item.summary,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
|
}
|
||||||
144
lib/markdown.ts
Normal file
144
lib/markdown.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
type ParseState = {
|
||||||
|
inCodeBlock: boolean;
|
||||||
|
codeLang: string;
|
||||||
|
paragraphBuffer: string[];
|
||||||
|
listType: "ul" | "ol" | null;
|
||||||
|
listBuffer: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeHtml(value: string): string {
|
||||||
|
return value
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyInlineMarkdown(value: string): string {
|
||||||
|
const escaped = escapeHtml(value);
|
||||||
|
|
||||||
|
return escaped
|
||||||
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||||
|
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||||
|
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
||||||
|
.replace(/~~([^~]+)~~/g, "<del>$1</del>")
|
||||||
|
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushParagraph(state: ParseState, html: string[]): void {
|
||||||
|
if (state.paragraphBuffer.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.push(`<p>${applyInlineMarkdown(state.paragraphBuffer.join(" "))}</p>`);
|
||||||
|
state.paragraphBuffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushList(state: ParseState, html: string[]): void {
|
||||||
|
if (!state.listType || state.listBuffer.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.push(`<${state.listType}>${state.listBuffer.join("")}</${state.listType}>`);
|
||||||
|
state.listType = null;
|
||||||
|
state.listBuffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markdownToHtml(markdown: string): string {
|
||||||
|
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
||||||
|
const html: string[] = [];
|
||||||
|
const state: ParseState = {
|
||||||
|
inCodeBlock: false,
|
||||||
|
codeLang: "",
|
||||||
|
paragraphBuffer: [],
|
||||||
|
listType: null,
|
||||||
|
listBuffer: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const rawLine of lines) {
|
||||||
|
const line = rawLine.trimEnd();
|
||||||
|
|
||||||
|
if (line.startsWith("```")) {
|
||||||
|
flushParagraph(state, html);
|
||||||
|
flushList(state, html);
|
||||||
|
|
||||||
|
if (!state.inCodeBlock) {
|
||||||
|
state.inCodeBlock = true;
|
||||||
|
state.codeLang = line.slice(3).trim();
|
||||||
|
const className = state.codeLang ? ` class="language-${escapeHtml(state.codeLang)}"` : "";
|
||||||
|
html.push(`<pre><code${className}>`);
|
||||||
|
} else {
|
||||||
|
state.inCodeBlock = false;
|
||||||
|
state.codeLang = "";
|
||||||
|
html.push("</code></pre>");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.inCodeBlock) {
|
||||||
|
html.push(`${escapeHtml(rawLine)}\n`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.length === 0) {
|
||||||
|
flushParagraph(state, html);
|
||||||
|
flushList(state, html);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||||
|
if (headingMatch) {
|
||||||
|
flushParagraph(state, html);
|
||||||
|
flushList(state, html);
|
||||||
|
const level = headingMatch[1].length;
|
||||||
|
html.push(`<h${level}>${applyInlineMarkdown(headingMatch[2])}</h${level}>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^---$/.test(line)) {
|
||||||
|
flushParagraph(state, html);
|
||||||
|
flushList(state, html);
|
||||||
|
html.push("<hr />");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockquoteMatch = line.match(/^>\s?(.*)$/);
|
||||||
|
if (blockquoteMatch) {
|
||||||
|
flushParagraph(state, html);
|
||||||
|
flushList(state, html);
|
||||||
|
html.push(`<blockquote><p>${applyInlineMarkdown(blockquoteMatch[1])}</p></blockquote>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ulMatch = line.match(/^[-*+]\s+(.+)$/);
|
||||||
|
if (ulMatch) {
|
||||||
|
flushParagraph(state, html);
|
||||||
|
if (state.listType !== "ul") {
|
||||||
|
flushList(state, html);
|
||||||
|
state.listType = "ul";
|
||||||
|
}
|
||||||
|
state.listBuffer.push(`<li>${applyInlineMarkdown(ulMatch[1])}</li>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const olMatch = line.match(/^\d+\.\s+(.+)$/);
|
||||||
|
if (olMatch) {
|
||||||
|
flushParagraph(state, html);
|
||||||
|
if (state.listType !== "ol") {
|
||||||
|
flushList(state, html);
|
||||||
|
state.listType = "ol";
|
||||||
|
}
|
||||||
|
state.listBuffer.push(`<li>${applyInlineMarkdown(olMatch[1])}</li>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushList(state, html);
|
||||||
|
state.paragraphBuffer.push(line.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
flushParagraph(state, html);
|
||||||
|
flushList(state, html);
|
||||||
|
|
||||||
|
return html.join("\n");
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
"@base-ui/react": "^1.3.0",
|
"@base-ui/react": "^1.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
|
"marked": "^17.0.5",
|
||||||
"next": "16.2.1",
|
"next": "16.2.1",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user