@ranklynk/nextjs
v0.1.2
Published
Hydration-safe RankLynk SEO delivery for the Next.js App Router.
Readme
RankLynk for Next.js
Hydration-safe delivery of RankLynk runtime_page_config through Next.js metadata and React Server Components. The package changes delivery correctness; it does not promise ranking gains.
Support
- Next.js App Router 14, 15, and 16
- React and React DOM 18.2 through 19.x
- Node.js 20.18.1 or newer
The package is server-only. Do not import it from a file containing 'use client'.
Install
npm install @ranklynk/nextjs
npx ranklynk-nextjs init --site-url https://example.comThe initializer creates exactly two files: a server helper at src/lib/ranklynk.ts (or lib/ranklynk.ts) and a public read-only status route at /api/ranklynk/status. It does not edit pages, environment files, or package.json, and it refuses to overwrite files it did not generate.
Set the domain-scoped key only in the server environment:
RANKLYNK_API_KEY=replace-with-your-domain-keyRoute integration
Use the same pathname in generateMetadata and the page. Next memoizes the identical server fetch across metadata and Server Components during a render.
import type { Metadata } from 'next'
import { RankLynkSeo, RankLynkSlot } from '@ranklynk/nextjs/server'
import { ranklynk } from '@/lib/ranklynk'
const pathname = '/about'
const baseMetadata: Metadata = {
title: 'About Example',
description: 'The native page description.',
}
export async function generateMetadata(): Promise<Metadata> {
return ranklynk.generateMetadata(pathname, baseMetadata)
}
export default async function AboutPage() {
const page = await ranklynk.getPage(pathname)
return (
<main>
<article>
<h1>About Example</h1>
<p>Page-owned content.</p>
<RankLynkSlot config={page.config} field="faq_html" className="faq" />
</article>
<RankLynkSeo
config={page.config}
pageOwnedFields={['faq_html']}
existingSchemaTypes={['Organization']}
/>
</main>
)
}pageOwnedFields is the duplicate-output contract. Every declared field must be rendered once with RankLynkSlot; RankLynkSeo then omits that field from its fallback. Undeclared body fields render once in a neutral, dependency-free <details> fallback.
For dynamic routes in Next 15 and 16, await params before building the pathname:
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
return ranklynk.generateMetadata(`/posts/${slug}`)
}Host-owned language and headings
lang belongs to the root <html> element, so the package returns safe props instead of trying to mutate the document after rendering:
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const htmlProps = await ranklynk.htmlProps('/', { lang: 'en' })
return <html {...htmlProps}><body>{children}</body></html>
}The helper accepts only a bounded BCP 47 language tag. Invalid managed values fall back to the valid host language.
Heading text and semantic fixes also stay inside the host React tree. Identify each heading by its original level and its one-indexed occurrence among headings of that level:
import { RankLynkHeadingSlot } from '@ranklynk/nextjs/server'
<RankLynkHeadingSlot config={page.config} originalLevel={2} occurrence={1}>
Host-authored heading
</RankLynkHeadingSlot>The component applies the first valid matching heading_text_updates value, an exact level_corrections rule, or demote_extra_h1 for H1 occurrences after the first. It emits source-level markers for deployed parity checks, bounds occurrences to 1–1,000 and replacement text to 300 characters, and never queries or mutates the DOM. Render each original level/occurrence pair once.
What is delivered
| Runtime field | Native delivery |
|---|---|
| Title, description, canonical, hreflang, robots, keywords, Open Graph, Twitter | generateMetadata helper |
| JSON-LD | Deduplicated Server Component scripts; existingSchemaTypes suppresses host-owned schema |
| FAQ, internal links, discovery, answer block, entity markup | Page-owned RankLynkSlot or one visually safe fallback |
| Missing H1 | Explicit RankLynkH1Slot; never automatic because the package cannot safely inspect the host tree |
| HTML language | ranklynk.htmlProps() spread onto a host-owned <html> element |
| Heading text, extra-H1 demotion, level corrections | Explicit RankLynkHeadingSlot using original level + occurrence |
| Heading-preservation CSS, preconnect, LCP preload | Sanitized/additive Server Component output |
| Viewport | ranklynk.mergeViewport(pathname, base) for a route/layout that owns generateViewport |
| Image DOM rewrites, redirects, response headers, variants, discovery hub document | Not injected generically; these require a host-owned element, routing layer, or edge-safe path |
The package never branches on user agent, cookies, or browser state. Humans and search/AI crawlers receive the same server component tree.
Delivery status
The generated /api/ranklynk/status route is intentionally public so RankLynk's deployed hydration harness can bind proof to the running bundle. It returns only:
{
"deploymentSha": "40-character-git-sha-or-null",
"deploymentId": "vercel-deployment-id-or-null",
"packageVersion": "0.1.2",
"nextVersion": "16.2.10",
"reactVersion": "19.2.7",
"revalidateSeconds": 300
}It never returns the API key, runtime config, environment name, commit message, or customer content. The response is dynamically generated with revalidation disabled at the HTTP cache layer.
The deployment SHA is resolved in this order: VERCEL_GIT_COMMIT_SHA, RANKLYNK_DEPLOYMENT_SHA, then GITHUB_SHA. RankLynk accepts only a full 40-character hexadecimal Git SHA; partial or malformed values are reported as null. Set RANKLYNK_DEPLOYMENT_SHA in the server runtime when the hosting platform does not expose a Vercel or GitHub deployment SHA automatically.
Failure and cache behavior
- Config is fetched during rendering, never through deferred
after()work. - Requests use Next's Data Cache with a 300-second default revalidation interval.
- Redirects from the config endpoint are rejected so the domain key cannot be forwarded to another origin.
- Invalid, oversized, timed-out, or empty responses fail closed to the host page's native SEO.
- Use
onErrorto connect delivery failures to server-side observability; callback failures are isolated from page rendering.
Initializer safety
Preview the two-file change:
npx ranklynk-nextjs init --site-url https://example.com --dry-runRefresh a previously generated helper after changing the site URL:
npx ranklynk-nextjs init --site-url https://www.example.com --force--force still refuses to overwrite a file without the RankLynk generated marker.
