fix: og text

This commit is contained in:
chronark 2023-11-06 09:39:11 +01:00
parent 8f14ec4fbe
commit 68228c44fc
No known key found for this signature in database
19 changed files with 264 additions and 265 deletions

View File

@ -5,73 +5,72 @@ import { Metadata } from "next";
import { Analytics } from "./components/analytics"; import { Analytics } from "./components/analytics";
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
default: "chronark.com", default: "chronark.com",
template: "%s | chronark.com", template: "%s | chronark.com",
}, },
description: "Software engineer at upstash.com and founder of planetfall.io", description: "Co-founder of unkey.dev and founder of planetfall.io",
openGraph: { openGraph: {
title: "chronark.com", title: "chronark.com",
description: description:
"Software engineer at upstash.com and founder of planetfall.io", "Co-founder of unkey.dev and founder of planetfall.io",
url: "https://chronark.com", url: "https://chronark.com",
siteName: "chronark.com", siteName: "chronark.com",
images: [ images: [
{ {
url: "https://chronark.com/og.png", url: "https://chronark.com/og.png",
width: 1920, width: 1920,
height: 1080, height: 1080,
}, },
], ],
locale: "en-US", locale: "en-US",
type: "website", type: "website",
}, },
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
googleBot: { googleBot: {
index: true, index: true,
follow: true, follow: true,
"max-video-preview": -1, "max-video-preview": -1,
"max-image-preview": "large", "max-image-preview": "large",
"max-snippet": -1, "max-snippet": -1,
}, },
}, },
twitter: { twitter: {
title: "Chronark", title: "Chronark",
card: "summary_large_image", card: "summary_large_image",
}, },
icons: { icons: {
shortcut: "/favicon.png", shortcut: "/favicon.png",
}, },
}; };
const inter = Inter({ const inter = Inter({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-inter", variable: "--font-inter",
}); });
const calSans = LocalFont({ const calSans = LocalFont({
src: "../public/fonts/CalSans-SemiBold.ttf", src: "../public/fonts/CalSans-SemiBold.ttf",
variable: "--font-calsans", variable: "--font-calsans",
}); });
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en" className={[inter.variable, calSans.variable].join(" ")}> <html lang="en" className={[inter.variable, calSans.variable].join(" ")}>
<head> <head>
<Analytics /> <Analytics />
</head> </head>
<body <body
className={`bg-black ${ className={`bg-black ${process.env.NODE_ENV === "development" ? "debug-screens" : undefined
process.env.NODE_ENV === "development" ? "debug-screens" : undefined }`}
}`} >
> {children}
{children} </body>
</body> </html>
</html> );
);
} }

View File

@ -9,40 +9,40 @@ import { Redis } from "@upstash/redis";
export const revalidate = 60; export const revalidate = 60;
type Props = { type Props = {
params: { params: {
slug: string; slug: string;
}; };
}; };
const redis = Redis.fromEnv(); const redis = Redis.fromEnv();
export async function generateStaticParams(): Promise<Props["params"][]> { export async function generateStaticParams(): Promise<Props["params"][]> {
return allProjects return allProjects
.filter((p) => p.published) .filter((p) => p.published)
.map((p) => ({ .map((p) => ({
slug: p.slug, slug: p.slug,
})); }));
} }
export default async function PostPage({ params }: Props) { export default async function PostPage({ params }: Props) {
const slug = params?.slug; const slug = params?.slug;
const project = allProjects.find((project) => project.slug === slug); const project = allProjects.find((project) => project.slug === slug);
if (!project) { if (!project) {
notFound(); notFound();
} }
const views = const views =
(await redis.get<number>(["pageviews", "projects", slug].join(":"))) ?? 0; (await redis.get<number>(["pageviews", "projects", slug].join(":"))) ?? 0;
return ( return (
<div className="bg-zinc-50 min-h-screen"> <div className="bg-zinc-50 min-h-screen">
<Header project={project} views={views} /> <Header project={project} views={views} />
<ReportView slug={project.slug} /> <ReportView slug={project.slug} />
<article className="px-4 py-12 mx-auto prose prose-zinc prose-quoteless"> <article className="px-4 py-12 mx-auto prose prose-zinc prose-quoteless">
<Mdx code={project.body.code} /> <Mdx code={project.body.code} />
</article> </article>
</div> </div>
); );
} }

View File

@ -11,128 +11,128 @@ const redis = Redis.fromEnv();
export const revalidate = 60; export const revalidate = 60;
export default async function ProjectsPage() { export default async function ProjectsPage() {
const views = ( const views = (
await redis.mget<number[]>( await redis.mget<number[]>(
...allProjects.map((p) => ["pageviews", "projects", p.slug].join(":")), ...allProjects.map((p) => ["pageviews", "projects", p.slug].join(":")),
) )
).reduce((acc, v, i) => { ).reduce((acc, v, i) => {
acc[allProjects[i].slug] = v ?? 0; acc[allProjects[i].slug] = v ?? 0;
return acc; return acc;
}, {} as Record<string, number>); }, {} as Record<string, number>);
const featured = allProjects.find((project) => project.slug === "unkey")!; const featured = allProjects.find((project) => project.slug === "unkey")!;
const top2 = allProjects.find((project) => project.slug === "planetfall")!; const top2 = allProjects.find((project) => project.slug === "planetfall")!;
const top3 = allProjects.find((project) => project.slug === "highstorm")!; const top3 = allProjects.find((project) => project.slug === "highstorm")!;
const sorted = allProjects const sorted = allProjects
.filter((p) => p.published) .filter((p) => p.published)
.filter( .filter(
(project) => (project) =>
project.slug !== featured.slug && project.slug !== featured.slug &&
project.slug !== top2.slug && project.slug !== top2.slug &&
project.slug !== top3.slug, project.slug !== top3.slug,
) )
.sort( .sort(
(a, b) => (a, b) =>
new Date(b.date ?? Number.POSITIVE_INFINITY).getTime() - new Date(b.date ?? Number.POSITIVE_INFINITY).getTime() -
new Date(a.date ?? Number.POSITIVE_INFINITY).getTime(), new Date(a.date ?? Number.POSITIVE_INFINITY).getTime(),
); );
return ( return (
<div className="relative pb-16"> <div className="relative pb-16">
<Navigation /> <Navigation />
<div className="px-6 pt-20 mx-auto space-y-8 max-w-7xl lg:px-8 md:space-y-16 md:pt-24 lg:pt-32"> <div className="px-6 pt-20 mx-auto space-y-8 max-w-7xl lg:px-8 md:space-y-16 md:pt-24 lg:pt-32">
<div className="max-w-2xl mx-auto lg:mx-0"> <div className="max-w-2xl mx-auto lg:mx-0">
<h2 className="text-3xl font-bold tracking-tight text-zinc-100 sm:text-4xl"> <h2 className="text-3xl font-bold tracking-tight text-zinc-100 sm:text-4xl">
Projects Projects
</h2> </h2>
<p className="mt-4 text-zinc-400"> <p className="mt-4 text-zinc-400">
Some of the projects are from work and some are on my own time. Some of the projects are from work and some are on my own time.
</p> </p>
</div> </div>
<div className="w-full h-px bg-zinc-800" /> <div className="w-full h-px bg-zinc-800" />
<div className="grid grid-cols-1 gap-8 mx-auto lg:grid-cols-2 "> <div className="grid grid-cols-1 gap-8 mx-auto lg:grid-cols-2 ">
<Card> <Card>
<Link href={`/projects/${featured.slug}`}> <Link href={`/projects/${featured.slug}`}>
<article className="relative w-full h-full p-4 md:p-8"> <article className="relative w-full h-full p-4 md:p-8">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="text-xs text-zinc-100"> <div className="text-xs text-zinc-100">
{featured.date ? ( {featured.date ? (
<time dateTime={new Date(featured.date).toISOString()}> <time dateTime={new Date(featured.date).toISOString()}>
{Intl.DateTimeFormat(undefined, { {Intl.DateTimeFormat(undefined, {
dateStyle: "medium", dateStyle: "medium",
}).format(new Date(featured.date))} }).format(new Date(featured.date))}
</time> </time>
) : ( ) : (
<span>SOON</span> <span>SOON</span>
)} )}
</div> </div>
<span className="flex items-center gap-1 text-xs text-zinc-500"> <span className="flex items-center gap-1 text-xs text-zinc-500">
<Eye className="w-4 h-4" />{" "} <Eye className="w-4 h-4" />{" "}
{Intl.NumberFormat("en-US", { notation: "compact" }).format( {Intl.NumberFormat("en-US", { notation: "compact" }).format(
views[featured.slug] ?? 0, views[featured.slug] ?? 0,
)} )}
</span> </span>
</div> </div>
<h2 <h2
id="featured-post" id="featured-post"
className="mt-4 text-3xl font-bold text-zinc-100 group-hover:text-white sm:text-4xl font-display" className="mt-4 text-3xl font-bold text-zinc-100 group-hover:text-white sm:text-4xl font-display"
> >
{featured.title} {featured.title}
</h2> </h2>
<p className="mt-4 leading-8 duration-150 text-zinc-400 group-hover:text-zinc-300"> <p className="mt-4 leading-8 duration-150 text-zinc-400 group-hover:text-zinc-300">
{featured.description} {featured.description}
</p> </p>
<div className="absolute bottom-4 md:bottom-8"> <div className="absolute bottom-4 md:bottom-8">
<p className="hidden text-zinc-200 hover:text-zinc-50 lg:block"> <p className="hidden text-zinc-200 hover:text-zinc-50 lg:block">
Read more <span aria-hidden="true">&rarr;</span> Read more <span aria-hidden="true">&rarr;</span>
</p> </p>
</div> </div>
</article> </article>
</Link> </Link>
</Card> </Card>
<div className="flex flex-col w-full gap-8 mx-auto border-t border-gray-900/10 lg:mx-0 lg:border-t-0 "> <div className="flex flex-col w-full gap-8 mx-auto border-t border-gray-900/10 lg:mx-0 lg:border-t-0 ">
{[top2, top3].map((project) => ( {[top2, top3].map((project) => (
<Card key={project.slug}> <Card key={project.slug}>
<Article project={project} views={views[project.slug] ?? 0} /> <Article project={project} views={views[project.slug] ?? 0} />
</Card> </Card>
))} ))}
</div> </div>
</div> </div>
<div className="hidden w-full h-px md:block bg-zinc-800" /> <div className="hidden w-full h-px md:block bg-zinc-800" />
<div className="grid grid-cols-1 gap-4 mx-auto lg:mx-0 md:grid-cols-3"> <div className="grid grid-cols-1 gap-4 mx-auto lg:mx-0 md:grid-cols-3">
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{sorted {sorted
.filter((_, i) => i % 3 === 0) .filter((_, i) => i % 3 === 0)
.map((project) => ( .map((project) => (
<Card key={project.slug}> <Card key={project.slug}>
<Article project={project} views={views[project.slug] ?? 0} /> <Article project={project} views={views[project.slug] ?? 0} />
</Card> </Card>
))} ))}
</div> </div>
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{sorted {sorted
.filter((_, i) => i % 3 === 1) .filter((_, i) => i % 3 === 1)
.map((project) => ( .map((project) => (
<Card key={project.slug}> <Card key={project.slug}>
<Article project={project} views={views[project.slug] ?? 0} /> <Article project={project} views={views[project.slug] ?? 0} />
</Card> </Card>
))} ))}
</div> </div>
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{sorted {sorted
.filter((_, i) => i % 3 === 2) .filter((_, i) => i % 3 === 2)
.map((project) => ( .map((project) => (
<Card key={project.slug}> <Card key={project.slug}>
<Article project={project} views={views[project.slug] ?? 0} /> <Article project={project} views={views[project.slug] ?? 0} />
</Card> </Card>
))} ))}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@ -3,45 +3,45 @@ import { NextRequest, NextResponse } from "next/server";
const redis = Redis.fromEnv(); const redis = Redis.fromEnv();
export const config = { export const config = {
runtime: "edge", runtime: "edge",
}; };
export default async function incr(req: NextRequest): Promise<NextResponse> { export default async function incr(req: NextRequest): Promise<NextResponse> {
if (req.method !== "POST") { if (req.method !== "POST") {
return new NextResponse("use POST", { status: 405 }); return new NextResponse("use POST", { status: 405 });
} }
if (req.headers.get("Content-Type") !== "application/json") { if (req.headers.get("Content-Type") !== "application/json") {
return new NextResponse("must be json", { status: 400 }); return new NextResponse("must be json", { status: 400 });
} }
const body = await req.json(); const body = await req.json();
let slug: string | undefined = undefined; let slug: string | undefined = undefined;
if ("slug" in body) { if ("slug" in body) {
slug = body.slug; slug = body.slug;
} }
if (!slug) { if (!slug) {
return new NextResponse("Slug not found", { status: 400 }); return new NextResponse("Slug not found", { status: 400 });
} }
const ip = req.ip; const ip = req.ip;
if (ip) { if (ip) {
// Hash the IP in order to not store it directly in your db. // Hash the IP in order to not store it directly in your db.
const buf = await crypto.subtle.digest( const buf = await crypto.subtle.digest(
"SHA-256", "SHA-256",
new TextEncoder().encode(ip), new TextEncoder().encode(ip),
); );
const hash = Array.from(new Uint8Array(buf)) const hash = Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join(""); .join("");
// deduplicate the ip for each slug // deduplicate the ip for each slug
const isNew = await redis.set(["deduplicate", hash, slug].join(":"), true, { const isNew = await redis.set(["deduplicate", hash, slug].join(":"), true, {
nx: true, nx: true,
ex: 24 * 60 * 60, ex: 24 * 60 * 60,
}); });
if (!isNew) { if (!isNew) {
new NextResponse(null, { status: 202 }); new NextResponse(null, { status: 202 });
} }
} }
await redis.incr(["pageviews", "projects", slug].join(":")); await redis.incr(["pageviews", "projects", slug].join(":"));
return new NextResponse(null, { status: 202 }); return new NextResponse(null, { status: 202 });
} }