Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { createHmac, timingSafeEqual } from "node:crypto";
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";

export const runtime = "nodejs";
const tagsByEvent: Record<string, (pageId: string) => string[]> = {
"page.content_updated": (id) => (id ? [`blog-article-${id}`] : []),
"page.properties_updated": (id) =>
id ? ["blog-list", `blog-article-${id}`] : ["blog-list"],
"page.created": () => ["blog-list"],
"page.deleted": (id) =>
id ? ["blog-list", `blog-article-${id}`] : ["blog-list"],
};

export async function POST(request: NextRequest) {
const body = await request.text();
Expand All @@ -14,11 +21,6 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}

if (payload.verification_token) {
console.log("Notion verification_token:", payload.verification_token);
return NextResponse.json({ ok: true });
}

const secret = process.env.NOTION_WEBHOOK_SECRET;
if (!secret) {
return NextResponse.json(
Expand All @@ -45,6 +47,18 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}

revalidateTag("blog-articles", "max");
return NextResponse.json({ revalidated: true });
const type = typeof payload.type === "string" ? payload.type : "";
const entity = payload.entity as Record<string, unknown> | undefined;
const pageId = typeof entity?.id === "string" ? entity.id : "";

console.log(`[revalidate] type=${type} pageId=${pageId}`);

const getTags = tagsByEvent[type];
const revalidated = getTags ? getTags(pageId) : [];

for (const tag of revalidated) {
revalidateTag(tag, "max");
}

return NextResponse.json({ revalidated });
}
2 changes: 1 addition & 1 deletion app/blog/[slug]/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function ArticleNotFound() {
<div className="flex flex-col items-center gap-4 pt-16 pb-24 text-center">
<h2 className="font-serif text-2xl font-bold">Article introuvable</h2>
<p className="text-zinc-500">
Cet article n&apos;existe pas ou a ete supprime.
Cet article n&apos;existe pas ou a été supprimé.
</p>
<Link
href="/blog"
Expand Down
43 changes: 29 additions & 14 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,59 @@
import { cache } from "react";
import { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";

import FluidContainer from "../../../components/FluidContainer";
import { articles, getArticleBySlug } from "../data";
import NotionRenderer from "../../components/NotionRenderer";
import {
fetchArticles,
fetchArticleById,
extractPlainText,
} from "../../../lib/notion";

const getArticle = cache(fetchArticleById);

interface ArticlePageProps {
params: Promise<{ slug: string }>;
}

export function generateStaticParams() {
return articles.map((article) => ({ slug: article.slug }));
export async function generateStaticParams() {
try {
const articles = await fetchArticles();
return articles.map((article) => ({ slug: article.id }));
} catch {
return [];
}
}

export async function generateMetadata({
params,
}: ArticlePageProps): Promise<Metadata> {
const { slug } = await params;
const article = getArticleBySlug(slug);
const article = await getArticle(slug);

if (!article) return {};

let description = article.title;
for (const block of article.blocks) {
if (block.type === "paragraph" && block.paragraph.rich_text.length > 0) {
description = extractPlainText(block.paragraph.rich_text).slice(0, 160);
break;
}
}

return {
title: article.title,
description: article.content.split("\n\n")[0],
description,
};
}

export default async function ArticlePage({ params }: ArticlePageProps) {
const { slug } = await params;
const article = getArticleBySlug(slug);
const article = await getArticle(slug);

if (!article) notFound();

const paragraphs = article.content.split("\n\n");

return (
<FluidContainer>
<div className="pt-8 pb-24">
Expand All @@ -49,12 +68,8 @@ export default async function ArticlePage({ params }: ArticlePageProps) {
{article.title}
</h1>
<p className="pt-4 text-sm text-zinc-400">{article.date}</p>
<div className="space-y-6 pt-8">
{paragraphs.map((paragraph, index) => (
<p key={index} className="text-base text-zinc-700">
{paragraph}
</p>
))}
<div className="pt-8">
<NotionRenderer blocks={article.blocks} />
</div>
</article>
</div>
Expand Down
130 changes: 0 additions & 130 deletions app/blog/data.ts

This file was deleted.

47 changes: 36 additions & 11 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
import { Suspense } from "react";
import CoverBackground from "../components/CoverBackground";
import Menu from "../components/Menu.client";
import FluidContainer from "../../components/FluidContainer";
import BlogCard from "../components/BlogCard";
import { articles } from "./data";
import Pagination from "../components/Pagination";
import { fetchArticles } from "../../lib/notion";

export default function BlogPage() {
const ARTICLES_PER_PAGE = 10;

async function ArticleList({ page }: { page: Promise<string | undefined> }) {
const [pageParam, articles] = await Promise.all([page, fetchArticles()]);

const totalPages = Math.max(
1,
Math.ceil(articles.length / ARTICLES_PER_PAGE)
);
const currentPage = Math.min(Math.max(1, Number(pageParam) || 1), totalPages);
const paginatedArticles = articles.slice(
(currentPage - 1) * ARTICLES_PER_PAGE,
currentPage * ARTICLES_PER_PAGE
);

return (
<>
{paginatedArticles.map((article) => (
<BlogCard key={article.id} article={article} />
))}
<Pagination currentPage={currentPage} totalPages={totalPages} />
</>
);
}

interface BlogPageProps {
searchParams: Promise<{ page?: string }>;
}

export default function BlogPage({ searchParams }: BlogPageProps) {
return (
<main>
<div className="relative">
Expand All @@ -24,15 +55,9 @@ export default function BlogPage() {

<FluidContainer>
<div className="-mt-8 pb-24">
{articles.map((article) => (
<BlogCard
key={article.slug}
slug={article.slug}
title={article.title}
date={article.date}
preview={article.content.split("\n\n")[0]}
/>
))}
<Suspense>
<ArticleList page={searchParams.then((sp) => sp.page)} />
</Suspense>
</div>
</FluidContainer>
</main>
Expand Down
32 changes: 15 additions & 17 deletions app/components/BlogCard.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
import clsx from "clsx";
import Link from "next/link";
import type { ArticleMeta } from "../../lib/notion";

interface BlogCardProps {
slug: string;
title: string;
date: string;
preview?: string;
article: ArticleMeta;
compact?: boolean;
}

export default function BlogCard({
slug,
title,
date,
preview,
compact,
}: BlogCardProps) {
export default function BlogCard({ article, compact }: BlogCardProps) {
const { id, title, date, author } = article;
return (
<Link
href={`/blog/${slug}`}
href={`/blog/${id}`}
className={clsx(
"group flex items-center justify-between border-b border-black/10 transition-colors hover:border-black/30",
compact ? "py-4" : "py-6"
compact ? "py-3" : "py-5"
)}
>
<div className="min-w-0 flex-1">
<h3 className="font-serif text-lg font-bold transition-transform group-hover:translate-x-1">
{title}
</h3>
{preview && (
<p className="line-clamp-2 pt-1 text-sm text-zinc-500">{preview}</p>
)}
<p className="pt-1 text-sm text-zinc-400">{date}</p>
<p className="pt-1 text-sm text-zinc-400">
{date}
{author && (
<>
<span className="mx-1.5">&middot;</span>
{author}
</>
)}
</p>
</div>
<span className="ml-4 text-zinc-400 transition-transform group-hover:translate-x-1">
&rarr;
Expand Down
Loading