chore: sync in-progress work across marketing, admin, API and tests

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>
This commit is contained in:
Leon Serfaty
2026-09-05 16:27:16 -04:00
co-authored by Claude Opus 5
parent 8f90347659
commit 1d02598786
71 changed files with 3641 additions and 819 deletions
+71
View File
@@ -0,0 +1,71 @@
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 } }),
}
}