From 75a3ea3baaf1975dd13968d8ee744e69466ac639 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 25 Jan 2026 00:47:14 +0000
Subject: [PATCH 1/6] Initial plan
From fd79a9903e08ed12770465126b4da4ddfe32d828 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 25 Jan 2026 00:50:03 +0000
Subject: [PATCH 2/6] Initial plan for metadata and OpenGraph refactor
Co-authored-by: oBusk <13413409+oBusk@users.noreply.github.com>
---
next-env.d.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818f..9edff1c7 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
From 89efe72e3d8994e6db372f981092326ae2300428 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 25 Jan 2026 00:57:53 +0000
Subject: [PATCH 3/6] Implement metadata and OpenGraph refactor for SEO
improvements
- Created reusable OpenGraphImage component for consistent OG images
- Enhanced root layout with comprehensive metadata, OpenGraph, and Twitter cards
- Added OpenGraph images for all about pages (/about, /about/api, /about/source-trust)
- Implemented dynamic OpenGraph images for diff pages showing stats (additions, deletions, files)
- Added provenance and trusted publisher badges to diff OpenGraph images
- Updated all page metadata with improved descriptions and OpenGraph data
Co-authored-by: oBusk <13413409+oBusk@users.noreply.github.com>
---
src/app/-/about/api/opengraph-image.tsx | 36 ++
src/app/-/about/api/page.tsx | 15 +-
src/app/-/about/opengraph-image.tsx | 37 ++
src/app/-/about/page.tsx | 14 +
.../-/about/source-trust/opengraph-image.tsx | 108 +-----
src/app/[...parts]/page.tsx | 32 +-
src/app/_components/OpenGraphImage.tsx | 131 +++++++
src/app/api/-/og/route.tsx | 329 ++++++++++++++++++
src/app/layout.tsx | 34 +-
9 files changed, 636 insertions(+), 100 deletions(-)
create mode 100644 src/app/-/about/api/opengraph-image.tsx
create mode 100644 src/app/-/about/opengraph-image.tsx
create mode 100644 src/app/_components/OpenGraphImage.tsx
create mode 100644 src/app/api/-/og/route.tsx
diff --git a/src/app/-/about/api/opengraph-image.tsx b/src/app/-/about/api/opengraph-image.tsx
new file mode 100644
index 00000000..35ce3fde
--- /dev/null
+++ b/src/app/-/about/api/opengraph-image.tsx
@@ -0,0 +1,36 @@
+import {
+ contentType,
+ OpenGraphImage,
+ size,
+} from "^/app/_components/OpenGraphImage";
+
+export const alt = "API – npm-diff.app";
+export { size, contentType };
+
+export default async function Image() {
+ return OpenGraphImage({
+ tag: "Documentation",
+ title: "API",
+ icon: (
+
+ ),
+ });
+}
diff --git a/src/app/-/about/api/page.tsx b/src/app/-/about/api/page.tsx
index e90ee7a3..07a1d3ad 100644
--- a/src/app/-/about/api/page.tsx
+++ b/src/app/-/about/api/page.tsx
@@ -21,7 +21,20 @@ const EXAMPLE_ABSOLUTE_URL = `${DOMAIN}${EXAMPLE_RELATIVE_LINK}` as const;
export const metadata: Metadata = {
title: "API",
- description: "API documentation for npm-diff.app",
+ description:
+ "API documentation for npm-diff.app. Access programmatic diff capabilities for npm packages using the same API that powers the web interface.",
+ openGraph: {
+ title: "npm-diff.app API",
+ description:
+ "API documentation for npm-diff.app. Access programmatic diff capabilities for npm packages.",
+ url: "https://npm-diff.app/-/about/api",
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "npm-diff.app API",
+ description:
+ "API documentation for npm-diff.app. Access programmatic diff capabilities for npm packages.",
+ },
};
const AboutApiPage = async () => {
diff --git a/src/app/-/about/opengraph-image.tsx b/src/app/-/about/opengraph-image.tsx
new file mode 100644
index 00000000..56aaed89
--- /dev/null
+++ b/src/app/-/about/opengraph-image.tsx
@@ -0,0 +1,37 @@
+import {
+ contentType,
+ OpenGraphImage,
+ size,
+} from "^/app/_components/OpenGraphImage";
+
+export const alt = "About – npm-diff.app";
+export { size, contentType };
+
+export default async function Image() {
+ return OpenGraphImage({
+ tag: "About",
+ title: "npm-diff.app",
+ icon: (
+
+ ),
+ });
+}
diff --git a/src/app/-/about/page.tsx b/src/app/-/about/page.tsx
index bbe3a18b..3b7aa727 100644
--- a/src/app/-/about/page.tsx
+++ b/src/app/-/about/page.tsx
@@ -11,6 +11,20 @@ import {
export const metadata: Metadata = {
title: "About",
+ description:
+ "Learn how npm-diff.app works. Compare npm packages using the official libnpmdiff library, visualize differences, and analyze bundle sizes with bundlephobia and packagephobia integrations.",
+ openGraph: {
+ title: "About npm-diff.app",
+ description:
+ "Learn how npm-diff.app compares npm packages using the official libnpmdiff library and visualizes the differences for safer dependency upgrades.",
+ url: "https://npm-diff.app/-/about",
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "About npm-diff.app",
+ description:
+ "Learn how npm-diff.app compares npm packages using the official libnpmdiff library and visualizes the differences for safer dependency upgrades.",
+ },
};
export default function AboutPage() {
diff --git a/src/app/-/about/source-trust/opengraph-image.tsx b/src/app/-/about/source-trust/opengraph-image.tsx
index 6219c115..bb2b785d 100644
--- a/src/app/-/about/source-trust/opengraph-image.tsx
+++ b/src/app/-/about/source-trust/opengraph-image.tsx
@@ -1,86 +1,17 @@
-import { ImageResponse } from "next/og";
-import { readFile } from "node:fs/promises";
-import { join } from "node:path";
+import {
+ contentType,
+ OpenGraphImage,
+ size,
+} from "^/app/_components/OpenGraphImage";
export const alt = "Source & Trust – npm-diff.app";
-export const size = {
- width: 1200,
- height: 630,
-};
-
-export const contentType = "image/png";
+export { size, contentType };
export default async function Image() {
- const logoData = await readFile(join(process.cwd(), "src/app/icon.png"));
- const logoSrc = Uint8Array.from(logoData).buffer;
-
- return new ImageResponse(
-
-

-
-
- Feature
-
-
- Source & Trust
-
-
+ return OpenGraphImage({
+ tag: "Feature",
+ title: "Source & Trust",
+ icon: (
-
- npm-diff.app
-
-
,
- {
- ...size,
- },
- );
+ ),
+ });
}
diff --git a/src/app/[...parts]/page.tsx b/src/app/[...parts]/page.tsx
index e7f113c7..f2928e50 100644
--- a/src/app/[...parts]/page.tsx
+++ b/src/app/[...parts]/page.tsx
@@ -26,13 +26,41 @@ export async function generateMetadata({
params,
}: DiffPageProps): Promise {
const { parts } = await params;
+ const partsStr = Array.isArray(parts) ? parts.join("/") : parts;
const specs = splitParts(decodeParts(parts));
const [a, b] = specs.map((spec) => createSimplePackageSpec(spec));
+ const packageNameA = simplePackageSpecToString(a);
+ const packageNameB = simplePackageSpecToString(b);
+ const title = `Comparing ${packageNameA}...${packageNameB}`;
+ const description = `View the detailed diff between npm packages ${packageNameA} and ${packageNameB}. See file changes, additions, deletions, bundle size comparisons, and trust signals including provenance and trusted publishing information.`;
+
+ // Generate OpenGraph image URL
+ const ogImageUrl = `/api/-/og?parts=${encodeURIComponent(partsStr)}`;
+
return {
- title: `Comparing ${simplePackageSpecToString(a)}...${simplePackageSpecToString(b)}`,
- description: `A diff between the npm packages "${simplePackageSpecToString(a)}" and "${simplePackageSpecToString(b)}"`,
+ title,
+ description,
+ openGraph: {
+ title,
+ description,
+ type: "article",
+ images: [
+ {
+ url: ogImageUrl,
+ width: 1200,
+ height: 630,
+ alt: `Diff between ${packageNameA} and ${packageNameB}`,
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title,
+ description,
+ images: [ogImageUrl],
+ },
};
}
diff --git a/src/app/_components/OpenGraphImage.tsx b/src/app/_components/OpenGraphImage.tsx
new file mode 100644
index 00000000..2affc71e
--- /dev/null
+++ b/src/app/_components/OpenGraphImage.tsx
@@ -0,0 +1,131 @@
+import { ImageResponse } from "next/og";
+import { readFile } from "node:fs/promises";
+import { join } from "node:path";
+import type { ReactNode } from "react";
+
+export const size = {
+ width: 1200,
+ height: 630,
+};
+
+export const contentType = "image/png";
+
+export interface OpenGraphImageProps {
+ /**
+ * Tag to display (e.g., "Feature", "About", "API")
+ */
+ tag: string;
+ /**
+ * Main title text
+ */
+ title: string;
+ /**
+ * Icon to display on the right side (SVG element)
+ */
+ icon: ReactNode;
+ /**
+ * Background color (default: "#0d1117")
+ */
+ backgroundColor?: string;
+}
+
+/**
+ * Reusable OpenGraph image component for npm-diff.app
+ * Creates a consistent OG image with a tag, title, and icon
+ */
+export async function OpenGraphImage({
+ tag,
+ title,
+ icon,
+ backgroundColor = "#0d1117",
+}: OpenGraphImageProps) {
+ const logoData = await readFile(join(process.cwd(), "src/app/icon.png"));
+ const logoSrc = Uint8Array.from(logoData).buffer;
+
+ return new ImageResponse(
+
+ {/* eslint-disable-next-line @next/next/no-img-element -- OpenGraph images require standard img tag */}
+

+
+
+ {tag}
+
+
+ {title}
+
+
+ {icon}
+
+ npm-diff.app
+
+
,
+ {
+ ...size,
+ },
+ );
+}
diff --git a/src/app/api/-/og/route.tsx b/src/app/api/-/og/route.tsx
new file mode 100644
index 00000000..cee19a08
--- /dev/null
+++ b/src/app/api/-/og/route.tsx
@@ -0,0 +1,329 @@
+import { ImageResponse } from "next/og";
+import { type NextRequest } from "next/server";
+import { readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { getSourceInformation } from "^/lib/api/npm/sourceInformation";
+import { createSimplePackageSpec } from "^/lib/createSimplePackageSpec";
+import destination from "^/lib/destination";
+import { gitDiffParse } from "^/lib/gitDiff";
+import npmDiff from "^/lib/npmDiff";
+import countChanges from "^/lib/utils/countChanges";
+import decodeParts from "^/lib/utils/decodeParts";
+import splitParts from "^/lib/utils/splitParts";
+
+function formatNumber(num: number): string {
+ if (num >= 1000) {
+ return (num / 1000).toFixed(1) + "k";
+ }
+ return num.toString();
+}
+
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const parts = searchParams.get("parts");
+
+ if (!parts) {
+ return new Response("Missing parts parameter", { status: 400 });
+ }
+
+ const specsOrVersions = splitParts(decodeParts(parts));
+ const { canonicalSpecs } = await destination(specsOrVersions);
+ const [a, b] = canonicalSpecs.map((spec) => createSimplePackageSpec(spec));
+
+ // Get diff statistics
+ const diff = await npmDiff(canonicalSpecs, {});
+ const files = gitDiffParse(diff);
+ const changes = files.map((file) => countChanges(file.hunks));
+ const additions = changes
+ .map(({ additions }) => additions)
+ .reduce((a, b) => a + b, 0);
+ const deletions = changes
+ .map(({ deletions }) => deletions)
+ .reduce((a, b) => a + b, 0);
+
+ // Get source information for badges
+ const [sourceA, sourceB] = await Promise.all([
+ getSourceInformation({ name: a.name, version: a.version }).catch(
+ () => null,
+ ),
+ getSourceInformation({ name: b.name, version: b.version }).catch(
+ () => null,
+ ),
+ ]);
+
+ const hasProvenance = !!(sourceA || sourceB);
+ const hasTrustedPublisher = !!(
+ sourceA?.hasTrustedPublisher || sourceB?.hasTrustedPublisher
+ );
+
+ const logoData = await readFile(join(process.cwd(), "src/app/icon.png"));
+ const logoSrc = Uint8Array.from(logoData).buffer;
+
+ const packageName = a.name;
+ const versionCompare = `${a.version} → ${b.version}`;
+
+ return new ImageResponse(
+
+ {/* Logo */}
+ {/* eslint-disable-next-line @next/next/no-img-element -- OpenGraph images require standard img tag */}
+

+
+ {/* Main content */}
+
+ {/* Package name */}
+
+ {packageName}
+
+
+ {/* Version comparison */}
+
+ {versionCompare}
+
+
+ {/* Stats */}
+
+ {/* Files changed */}
+
+
+ {files.length}
+
+
+ files
+
+
+
+ {/* Additions */}
+
+
+ +{formatNumber(additions)}
+
+
+ additions
+
+
+
+ {/* Deletions */}
+
+
+ -{formatNumber(deletions)}
+
+
+ deletions
+
+
+
+
+ {/* Badges */}
+ {!!(hasProvenance || hasTrustedPublisher) && (
+
+ {!!hasProvenance && (
+
+
+
+ Provenance
+
+
+ )}
+ {!!hasTrustedPublisher && (
+
+
+
+ Trusted Publisher
+
+
+ )}
+
+ )}
+
+
+ {/* Footer */}
+
+ npm-diff.app
+
+
,
+ {
+ width: 1200,
+ height: 630,
+ },
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index e2e43b7a..3bcad4fe 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -8,18 +8,50 @@ import Header from "./_layout/Header";
import "./globals.css";
export const metadata: Metadata = {
+ metadataBase: new URL("https://npm-diff.app"),
applicationName: "npm-diff.app",
title: {
default: "npm-diff.app 📦🔃",
template: "%s • npm-diff.app 📦🔃",
},
- description: "Inspect changes between npm packages in a webapp",
+ description:
+ "Inspect and compare changes between npm package versions in a webapp. View diffs, analyze bundle size changes, and audit trust signals like provenance and trusted publishing for safer dependency upgrades.",
manifest: "/site.webmanifest",
formatDetection: {
email: false,
address: false,
telephone: false,
},
+ keywords: [
+ "npm",
+ "diff",
+ "package comparison",
+ "dependency audit",
+ "npm packages",
+ "version diff",
+ "bundle size",
+ "npm security",
+ "provenance",
+ "trusted publishing",
+ ],
+ authors: [{ name: "Oscar Busk", url: "https://github.com/oBusk" }],
+ creator: "Oscar Busk",
+ openGraph: {
+ type: "website",
+ locale: "en_US",
+ url: "https://npm-diff.app",
+ siteName: "npm-diff.app",
+ title: "npm-diff.app 📦🔃",
+ description:
+ "Inspect and compare changes between npm package versions. View diffs, analyze bundle sizes, and audit trust signals for safer dependency upgrades.",
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "npm-diff.app 📦🔃",
+ description:
+ "Inspect and compare changes between npm package versions. View diffs, analyze bundle sizes, and audit trust signals for safer dependency upgrades.",
+ creator: "@oBusk",
+ },
};
export const viewport = {
From 72aefb48a04e114116f184a5c8e771d711f82020 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 25 Jan 2026 01:02:00 +0000
Subject: [PATCH 4/6] Fix OpenGraph image rendering with proper display styles
- Add display: flex to all divs and spans to satisfy Next.js ImageResponse requirements
- Tested dynamic OG images showing stats and badges correctly
- All validation checks pass (lint, typecheck, tests, build)
Co-authored-by: oBusk <13413409+oBusk@users.noreply.github.com>
---
src/app/api/-/og/route.tsx | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/app/api/-/og/route.tsx b/src/app/api/-/og/route.tsx
index cee19a08..6c6f34f2 100644
--- a/src/app/api/-/og/route.tsx
+++ b/src/app/api/-/og/route.tsx
@@ -106,6 +106,7 @@ export async function GET(request: NextRequest) {
{/* Package name */}
Date: Mon, 26 Jan 2026 23:46:41 +0000
Subject: [PATCH 5/6] Make metadataBase dynamic for dev/preview environments
Use VERCEL_URL for preview deployments, localhost for development, and production URL as fallback. This allows OpenGraph images to work in all environments without hardcoding to production.
Co-authored-by: oBusk <13413409+oBusk@users.noreply.github.com>
---
next-env.d.ts | 2 +-
src/app/layout.tsx | 8 +++++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/next-env.d.ts b/next-env.d.ts
index 9edff1c7..c4b7818f 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 3bcad4fe..9863b089 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -8,7 +8,13 @@ import Header from "./_layout/Header";
import "./globals.css";
export const metadata: Metadata = {
- metadataBase: new URL("https://npm-diff.app"),
+ metadataBase: new URL(
+ process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : process.env.NODE_ENV === "development"
+ ? "http://localhost:3000"
+ : "https://npm-diff.app",
+ ),
applicationName: "npm-diff.app",
title: {
default: "npm-diff.app 📦🔃",
From 90620c1b5758cf1e9d0168bd116b63dc8380fba5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 26 Jan 2026 23:48:28 +0000
Subject: [PATCH 6/6] Refactor metadata architecture for SEO with dynamic
OpenGraph images
Co-authored-by: oBusk <13413409+oBusk@users.noreply.github.com>
---
next-env.d.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818f..9edff1c7 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.