diff --git a/src/app/(category-sidebar)/products/[category]/opengraph-image.tsx b/src/app/(category-sidebar)/products/[category]/opengraph-image.tsx
new file mode 100644
index 0000000..fe90204
--- /dev/null
+++ b/src/app/(category-sidebar)/products/[category]/opengraph-image.tsx
@@ -0,0 +1,117 @@
+import { db } from "@/db";
+import { ImageResponse } from "next/og";
+import { notFound } from "next/navigation";
+
+// Route segment config
+export const runtime = "edge";
+
+// Image metadata
+export const alt = "About the category";
+export const size = {
+ width: 1200,
+ height: 630,
+};
+
+export const contentType = "image/png";
+
+// Image generation
+export default async function Image(props: {
+ params: Promise<{
+ category: string;
+ }>;
+}) {
+ const { category: categoryParam } = await props.params;
+ const urlDecodedCategory = decodeURIComponent(categoryParam);
+
+ const category = await db.query.categories.findFirst({
+ where: (categories, { eq }) => eq(categories.slug, urlDecodedCategory),
+ with: {
+ subcollections: true,
+ },
+ orderBy: (categories, { asc }) => asc(categories.name),
+ });
+
+ if (!category) {
+ return notFound();
+ }
+
+ const examples = category.subcollections
+ .slice(0, 2)
+ .map((s) => s.name)
+ .join(", ");
+
+ const description = `Choose from our selection of ${category.name}, including ${examples + (category.subcollections.length > 1 ? "," : "")} and more. In stock and ready to ship.`;
+
+ // TODO: Change design to add subcategory images that blur out
+ return new ImageResponse(
+ (
+
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
+
+ {category.name}
+
+
+
+ ),
+ {
+ width: 1200,
+ height: 630,
+ },
+ );
+}
diff --git a/src/app/(category-sidebar)/products/[category]/page.tsx b/src/app/(category-sidebar)/products/[category]/page.tsx
index f71d240..6f7b307 100644
--- a/src/app/(category-sidebar)/products/[category]/page.tsx
+++ b/src/app/(category-sidebar)/products/[category]/page.tsx
@@ -6,10 +6,45 @@ import {
subcollection,
} from "@/db/schema";
import { count, eq } from "drizzle-orm";
+import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ category: string }>;
+}): Promise {
+ const { category: categoryParam } = await params;
+ const urlDecoded = decodeURIComponent(categoryParam);
+ const category = await db.query.categories.findFirst({
+ where: (categories, { eq }) => eq(categories.slug, urlDecoded),
+ with: {
+ subcollections: true,
+ },
+ orderBy: (categories, { asc }) => asc(categories.name),
+ });
+
+ if (!category) {
+ return notFound();
+ }
+
+ const examples = category.subcollections
+ .slice(0, 2)
+ .map((s) => s.name)
+ .join(", ")
+ .toLowerCase();
+
+ return {
+ title: `${category.name} | NextMaster`,
+ openGraph: {
+ title: `${category.name} | NextMaster`,
+ description: `Choose from our selection of ${category.name.toLowerCase()}, including ${examples + (category.subcollections.length > 1 ? "," : "")} and more. In stock and ready to ship.`,
+ },
+ };
+}
+
export default async function Page(props: {
params: Promise<{
category: string;