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

@ -1,2 +1,2 @@
UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN= UPSTASH_REDIS_REST_TOKEN=

View File

@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<a href="https://chronark.com"><h1 align="center">chronark.com</h1></a> <a href="https://chronark.com"><h1 align="center">chronark.com</h1></a>
My personal website, built with [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/), [Upstash](https://upstash.com?ref=chronark.com), [Contentlayer](https://www.contentlayer.dev/) and deployed to [Vercel](https://vercel.com/). My personal website, built with [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/), [Upstash](https://upstash.com?ref=chronark.com), [Contentlayer](https://www.contentlayer.dev/) and deployed to [Vercel](https://vercel.com/).
</div> </div>
@ -30,4 +30,4 @@ pnpm dev
## Cloning / Forking ## Cloning / Forking
Please remove all of my personal information (projects, images, etc.) before deploying your own version of this site. Please remove all of my personal information (projects, images, etc.) before deploying your own version of this site.

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

@ -113,4 +113,4 @@ $ curl -s https://envshare.dev/api/v1/secret/HdPbXgpvUvNk43oxSdK97u
"remainingReads": 1 "remainingReads": 1
} }
} }
``` ```

View File

@ -4,7 +4,7 @@ description: QStash is a fully managed serverless queue and messaging service de
date: "2022-07-18" date: "2022-07-18"
url: https://upstash.com/qstash url: https://upstash.com/qstash
published: true published: true
--- ---
QStash is an HTTP based messaging and scheduling solution for the serverless and edge runtimes. QStash is an HTTP based messaging and scheduling solution for the serverless and edge runtimes.
@ -33,4 +33,4 @@ If you use QStash in the above example, you simply send a request to QStash from
With QStash, you can add delays to the requests. Send an email 3 days after the shipment to remind the customer to add a review. You can also schedule tasks. You can send the requests with a CRON expression, so it will be run repetitively. With QStash, you can add delays to the requests. Send an email 3 days after the shipment to remind the customer to add a review. You can also schedule tasks. You can send the requests with a CRON expression, so it will be run repetitively.
To learn more about QStash, visit [upstash.com/qstash](upstash.com/qstash). To learn more about QStash, visit [upstash.com/qstash](upstash.com/qstash).

View File

@ -5,4 +5,4 @@ url: https://console.upstash.com/ratelimit
repository: upstash/auth-analytics repository: upstash/auth-analytics
--- ---
TODO: TODO:

View File

@ -4,7 +4,7 @@ description: A CLI to provision and manage Upstash resources, including Redis an
date: "2022-05-16" date: "2022-05-16"
repository: upstash/cli repository: upstash/cli
published: true published: true
--- ---
@ -38,7 +38,7 @@ for windows, linux and mac (both intel and m1).
```bash ```bash
> upstash > upstash
Usage: upstash Usage: upstash
Version: development Version: development
Description: Description:
@ -47,15 +47,15 @@ for windows, linux and mac (both intel and m1).
Options: Options:
-h, --help - Show this help. -h, --help - Show this help.
-V, --version - Show the version number for this program. -V, --version - Show the version number for this program.
-c, --config <string> - Path to .upstash.json file -c, --config <string> - Path to .upstash.json file
Commands: Commands:
auth - Login and logout auth - Login and logout
redis - Manage redis database instances redis - Manage redis database instances
kafka - Manage kafka clusters and topics kafka - Manage kafka clusters and topics
team - Manage your teams and their members team - Manage your teams and their members
Environment variables: Environment variables:

View File

@ -5,7 +5,7 @@ date: "2023-02-13"
url: https://console.upstash.com/ratelimit url: https://console.upstash.com/ratelimit
repository: upstash/core-analytics repository: upstash/core-analytics
published: true published: true
--- ---

View File

@ -5,7 +5,7 @@ date: "2022-12-12"
url: https://upstash.com/blog/edge-flags-beta url: https://upstash.com/blog/edge-flags-beta
repository: upstash/edge-flags repository: upstash/edge-flags
published: true published: true
--- ---
@ -13,7 +13,7 @@ Whether you want to ship without breaking things, run A/B tests or just want to
feature flags are a great way to dynamically change the behaviour of your app without redeploying. We're excited to announce the public feature flags are a great way to dynamically change the behaviour of your app without redeploying. We're excited to announce the public
beta release of our new feature flagging library: [@upstash/edge-flags](https://github.com/upstash/edge-flags). beta release of our new feature flagging library: [@upstash/edge-flags](https://github.com/upstash/edge-flags).
*Edge Flags*, as the name implies, is a feature flag solution built to run at the edge. It is using [Upstash Redis](https://upstash.com/), a globally replicated serverless Redis service, to store configuration and is *Edge Flags*, as the name implies, is a feature flag solution built to run at the edge. It is using [Upstash Redis](https://upstash.com/), a globally replicated serverless Redis service, to store configuration and is
designed to work with [Next.js](https://nextjs.org) and [Vercel](https://vercel.com). We want to support other frameworks in the future, so if you have a suggestion, please let us know! designed to work with [Next.js](https://nextjs.org) and [Vercel](https://vercel.com). We want to support other frameworks in the future, so if you have a suggestion, please let us know!
With the ability to toggle features on and off at the edge, you can quickly respond With the ability to toggle features on and off at the edge, you can quickly respond

View File

@ -5,7 +5,7 @@ date: "2022-01-08"
url: https://upstash.com/kafka url: https://upstash.com/kafka
repository: upstash/upstash-kafka repository: upstash/upstash-kafka
published: true published: true
--- ---

View File

@ -4,7 +4,7 @@ description: Near realtime analytics for your ratelimits. Integrated into the @u
repository: upstash/ratelimit" repository: upstash/ratelimit"
url: https://console.upstash.com/ratelimit url: https://console.upstash.com/ratelimit
published: true published: true
--- ---
TODO: TODO:

View File

@ -5,7 +5,7 @@ date: "2022-06-06"
url: https://upstash.com/blog/upstash-ratelimit url: https://upstash.com/blog/upstash-ratelimit
repository: upstash/ratelimit repository: upstash/ratelimit
published: true published: true
--- ---
In today's digital age, serverless computing has become increasingly popular due to its scalability and cost-efficiency. One of the challenges of serverless computing is to manage resources efficiently, and one critical aspect of this is rate limiting. Rate limiting is a technique that limits the number of requests a client can make to a server over a given period. This technique can prevent abuse, improve performance, and reduce costs. One npm package that helps implement rate limiting for serverless applications is @upstash/ratelimit, built on top of Upstash Redis. In today's digital age, serverless computing has become increasingly popular due to its scalability and cost-efficiency. One of the challenges of serverless computing is to manage resources efficiently, and one critical aspect of this is rate limiting. Rate limiting is a technique that limits the number of requests a client can make to a server over a given period. This technique can prevent abuse, improve performance, and reduce costs. One npm package that helps implement rate limiting for serverless applications is @upstash/ratelimit, built on top of Upstash Redis.
@ -46,4 +46,4 @@ const { success } = await ratelimit.limit("identifier")
In the code above, we initialize Upstash with our Upstash Redis credentials and define our rate limiting rules. We then call the `limit` function, passing the identifier. The function returns a Promise that resolves with `success` and some other useful data. In the code above, we initialize Upstash with our Upstash Redis credentials and define our rate limiting rules. We then call the `limit` function, passing the identifier. The function returns a Promise that resolves with `success` and some other useful data.
`@upstash/ratelimit` is a useful npm package for serverless rate limiting that simplifies the process of implementing rate limiting for serverless applications. The package is built on top of Upstash Redis, which provides a complete solution for serverless applications. With `@upstash/ratelimit`, serverless developers can easily implement rate limiting, which can help prevent abuse, improve performance, and reduce costs. `@upstash/ratelimit` is a useful npm package for serverless rate limiting that simplifies the process of implementing rate limiting for serverless applications. The package is built on top of Upstash Redis, which provides a complete solution for serverless applications. With `@upstash/ratelimit`, serverless developers can easily implement rate limiting, which can help prevent abuse, improve performance, and reduce costs.

View File

@ -5,8 +5,8 @@ date: "2023-02-05"
url: https://upstash.com url: https://upstash.com
repository: upstash/react-ui repository: upstash/react-ui
published: true published: true
--- ---
`@upstash/react-ui` is powering the CLI in your browser on [console.upstash.com](https://console.upstash.com). It allows you to interact with your Upstash Redis database in a simple and intuitive way. `@upstash/react-ui` is powering the CLI in your browser on [console.upstash.com](https://console.upstash.com). It allows you to interact with your Upstash Redis database in a simple and intuitive way.

View File

@ -5,7 +5,7 @@ date: "2022-03-14"
url: https://upstash.com/redis url: https://upstash.com/redis
repository: upstash/upstash-redis repository: upstash/upstash-redis
published: true published: true
--- ---
Upstash is a cloud-based service provider that offers a Redis-compatible service. In addition to that, we have also created an npm package called `@upstash/redis`. This package provides a strongly typed Redis client that uses HTTP instead of TCP to communicate with the database, making it perfect for serverless and edge runtimes. Upstash is a cloud-based service provider that offers a Redis-compatible service. In addition to that, we have also created an npm package called `@upstash/redis`. This package provides a strongly typed Redis client that uses HTTP instead of TCP to communicate with the database, making it perfect for serverless and edge runtimes.
@ -42,4 +42,4 @@ console.log(data)
await redis.lpush('elements', 'magnesium') await redis.lpush('elements', 'magnesium')
data = await redis.lrange('elements', 0, 100 ) data = await redis.lrange('elements', 0, 100 )
console.log(data) console.log(data)
``` ```

View File

@ -5,4 +5,4 @@ description: A library to record and analyse web page traffic and user behaviour
published: true published: true
--- ---
Coming soon Coming soon

View File

@ -52,4 +52,4 @@
"@opentelemetry/semantic-conventions": "1.13.0" "@opentelemetry/semantic-conventions": "1.13.0"
} }
} }
} }

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 });
} }