mirror of
https://github.com/AderKonstantin/aderktech-chronark.com-.git
synced 2025-06-08 13:48:42 +03:00
fix: og text
This commit is contained in:
parent
8f14ec4fbe
commit
68228c44fc
117
app/layout.tsx
117
app/layout.tsx
@ -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>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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">→</span>
|
Read more <span aria-hidden="true">→</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user