# SEO metadata and social sharing

URL: https://ship-and-scale.vercel.app/docs/website/seo-metadata

Use the starter's metadata helper, canonical origin, social image and structured data for real public pages.

## Start with the shared defaults [#start-with-the-shared-defaults]

Metadata describes a page to browsers, search engines and link previews. The starter centralizes brand identity in `config/site.ts` and discovery settings in `config/seo.ts`. Review the site name, description, language, locale and social-image path when you replace the sample marketing copy.

Metadata is not authorization and does not make an unfinished product ready to index. Keep `SEO_INDEXABLE=false` while customizing. The [indexing guide](/docs/website/seo-discovery) explains the production switch and development/preview guards.

The root layout intentionally does not assign the homepage canonical URL to every route. A canonical identifies the preferred URL for one public page. Applying the home URL to unknown or private pages would misdescribe those pages.

## Set the canonical origin [#set-the-canonical-origin]

Use your real HTTPS origin in active `APP_URL`, with no path, embedded credentials, query or fragment. `APP_URL_DEV` and `APP_URL_LIVE` are saved references; they do not independently select the current runtime origin. Deployment installs the verified live origin for the production runtime.

The SEO helpers derive URLs from that configured origin, not request Host headers. A development preview can therefore display production-looking canonicals if you configured it that way; it must still remain noindex. Check both the URL and indexability rather than assuming one proves the other.

Changing a production origin also affects authentication, email callbacks and MCP audiences. Follow [domains and deployment recovery](/docs/operations/deployment) instead of treating this as a title-only edit.

## Add a real public page with publicMetadata [#add-a-real-public-page-with-publicmetadata]

The following complete page describes the optional notes feature after you implement it. Create `app/features/notes/page.tsx` in your product copy, and edit its claims to match your actual behavior:

```tsx
import Link from 'next/link';
import { publicMetadata } from '@/lib/seo/metadata';

export const metadata = publicMetadata({
  title: 'Workspace notes',
  description: 'Read short team notes in one workspace. Owners and admins can add a note.',
  path: '/features/notes',
});

export default function NotesFeaturePage() {
  return <main className="mx-auto max-w-3xl space-y-6 px-6 py-16">
    <h1 className="text-4xl font-semibold">Workspace notes</h1>
    <p>Keep a decision and its next action where your teammates can find them.</p>
    <h2 className="text-2xl font-semibold">A clear workspace boundary</h2>
    <p>Members read the latest 50 notes. Workspace owners and admins create them.</p>
    <Link href="/login" className="underline">Sign in to your workspace</Link>
  </main>;
}
```

Then add `{ path: '/features/notes' }` to the existing `pages` array in `config/seo.ts`. Keep its current entries. Register only an implemented public HTML route; do not add a redirect, query variant, private dashboard route or a promise of a future feature.

`publicMetadata` supplies the complete title, description, canonical, robots, Open Graph and Twitter objects. Its full title is the page title followed by the site name. Next.js metadata objects merge shallowly, so using the shared helper avoids accidentally dropping the default image when changing one Open Graph field.

## Open Graph, Twitter and images [#open-graph-twitter-and-images]

The default social image is the generated `/social-image` route: a 1200 × 630 PNG using the site's configured brand. Open it directly after rebranding. Check that long names fit and the image is legible at a small preview size. Replace `app/favicon.ico` with your own icon and verify the actual browser tab, not only the asset file.

To give one page a specific social image, add a reviewed repository asset such as `public/social/notes.png` and pass `image: '/social/notes.png'` into the helper. The image must resolve publicly at your deployment's origin. A private Storage URL or expiring signed link is unsuitable for persistent sharing metadata.

Inspect `og:title`, `og:description`, `og:url`, `og:image` and `twitter:card` in the rendered HTML. Confirm image URLs are absolute, HTTPS on production, and return an image content type. Social services may cache old metadata. Verify the deployed response first, then use the service's own re-scrape/debug tooling when available. A locally rendered image does not prove a social platform fetched it.

## Truthful JSON-LD [#truthful-json-ld]

JSON-LD is structured data embedded in an HTML script. The starter's `components/seo/json-ld.tsx` escapes `<` before embedding serialized JSON, preventing text like a closing script tag from breaking out of the script. Keep that escaping if you extend the component.

The homepage describes the real website. Blog pages produce `BlogPosting` data from their visible author, title, excerpt, publication date and image. Docs and stories include breadcrumbs. Do not add invented reviews, ratings, prices or organization addresses for richer-looking search results. Structured data must agree with visible content and the actual business.

For a feature page, a truthful page title and description are often enough. Additional schema types should be selected for a real requirement and checked against the relevant search engine's official rules. See [Next.js JSON-LD guidance](https://nextjs.org/docs/app/guides/json-ld) for the embedding pattern.

## Blog and documentation metadata [#blog-and-documentation-metadata]

Product documentation titles and descriptions come from MDX frontmatter. A useful description tells the reader what they will accomplish, rather than repeating the title. Keep headings stable where people already link to an anchor.

Published blog metadata comes from the same public reader as the article. Drafts and future publications must remain absent from HTML and discovery. Blog listing pagination has its own canonical per page. Never advertise the first page's canonical for every paginated result or fabricate modification dates when no reliable date exists.

## Verify the page and social preview [#verify-the-page-and-social-preview]

On your running app, open the new route, use View Source or browser tools to inspect metadata, open its image URL and test the mobile page. These POSIX commands inspect public outputs without application credentials:

```sh
curl --fail-with-body http://localhost:3000/features/notes
curl --head http://localhost:3000/social-image
curl --fail-with-body http://localhost:3000/sitemap.xml
```

In PowerShell, use `curl.exe` for these commands. The local sitemap may correctly contain no URLs while indexing is disabled. Run the existing SEO fixture and browser checks listed in [testing](/docs/operations/testing), then repeat against the actual production origin before enabling discovery. Local checks cover generated metadata; remote social-crawler acceptance remains a separate observation.