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( -
- npm-diff.app icon -
-
- 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 */} + npm-diff.app icon +
+
+ {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 */} + npm-diff.app icon + + {/* 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.