Snapshot of uncommitted work that had accumulated in the tree alongside the Turnstile changes: - marketing pages, SEO helpers (lib/seo.ts, lib/marketing/) and structured data - admin billing actions and a per-user portfolio view, plus an admin error boundary - rate limiting (lib/rate-limit.ts) applied across the /api/v1 surface - CSP and proxy adjustments, accounting/webhook lib updates - Playwright config and an e2e/unit test suite - next bumped to ^16.3.4 with the lockfile regenerated - generated AGENTS.md / CLAUDE.md Authored by other sessions working in this tree; committed here so the Turnstile work could be pushed without leaving the tree dirty. Typecheck passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
72 lines
2.2 KiB
TypeScript
72 lines
2.2 KiB
TypeScript
import type { Metadata } from "next"
|
|
|
|
export const SITE_NAME = "Property Management Network"
|
|
|
|
// The generated Open Graph card (app/opengraph-image.tsx), served at this path.
|
|
// Declared explicitly because a page that sets its own `openGraph` object
|
|
// replaces the inherited one — including the image Next.js attaches from the
|
|
// file convention — which silently leaves that page with no share image.
|
|
const OG_IMAGE = "/opengraph-image"
|
|
|
|
type PageSeo = {
|
|
/**
|
|
* Page title. Rendered through the root layout's "%s | Property Management
|
|
* Network" template unless `absoluteTitle` is set.
|
|
*/
|
|
title: string
|
|
/** Render `title` verbatim, without the site-name suffix. */
|
|
absoluteTitle?: boolean
|
|
description: string
|
|
/** Canonical path, root-relative and without a trailing slash, e.g. "/terms". */
|
|
path: string
|
|
/**
|
|
* Headline used for the Open Graph / Twitter card when the share copy should
|
|
* differ from the page title. Defaults to the resolved page title.
|
|
*/
|
|
socialTitle?: string
|
|
/** Set false for pages that must stay out of the index. */
|
|
index?: boolean
|
|
}
|
|
|
|
/**
|
|
* Builds a complete, self-consistent metadata block for a public page.
|
|
*
|
|
* Every public page must go through this helper so that canonical, og:url,
|
|
* og:title, og:image and the Twitter card always agree with each other and with
|
|
* the page's real URL. Setting these ad hoc per page is what previously left the
|
|
* landing page with no share image and every legal page pointing og:url at "/".
|
|
*/
|
|
export function pageMetadata({
|
|
title,
|
|
absoluteTitle = false,
|
|
description,
|
|
path,
|
|
socialTitle,
|
|
index = true,
|
|
}: PageSeo): Metadata {
|
|
const fullTitle = absoluteTitle ? title : `${title} | ${SITE_NAME}`
|
|
const social = socialTitle ?? fullTitle
|
|
|
|
return {
|
|
title: absoluteTitle ? { absolute: title } : title,
|
|
description,
|
|
alternates: { canonical: path },
|
|
openGraph: {
|
|
title: social,
|
|
description,
|
|
url: path,
|
|
siteName: SITE_NAME,
|
|
locale: "en_US",
|
|
type: "website",
|
|
images: [OG_IMAGE],
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title: social,
|
|
description,
|
|
images: [OG_IMAGE],
|
|
},
|
|
...(index ? {} : { robots: { index: false, follow: true } }),
|
|
}
|
|
}
|