@glowrank/next
v0.3.0
Published
Next.js adapter for GlowRank own-site content — headless <GlowRankArticle> native rendering in your own layout, plus a one-line App Router route handler with ISR/tag revalidation, sitemap, and connection verification.
Readme
@glowrank/next
Next.js (App Router) adapter for GlowRank own-site
content: render GlowRank pages natively inside your own layout — your own
markdown renderer on page.markdown, or the headless <GlowRankArticle>
fragment — or mount a one-line route handler that serves complete documents.
ISR, tag-based revalidation, a sitemap, and the connection-verification probe
built in.
This content is yours — pages render on your origin, in your design, and canonical URLs point at your domain.
Native rendering (recommended)
Render each GlowRank page inside your own layout, with your own styles. No
third-party document, no injected styles or fonts — you never serve markup you
didn't choose. Two ways to do it; both share the same data flow
(createClient → getPageBySlug → render) and the same generateMetadata
helper.
Markdown-first (zero-trust)
Every page payload carries a first-class markdown field — a Markdown
rendition derived server-side from the sanitised body and stored with it, so it
always matches. Render it with the markdown renderer you already use, and there
is no third-party HTML to trust at all — GlowRank owns the words; your
renderer and design system own every element on the page:
// app/guides/[slug]/page.tsx (the base path from your GlowRank connection)
import { notFound } from "next/navigation";
import { createClient, glowrankMetadata } from "@glowrank/next";
import Markdown from "react-markdown"; // or your own renderer/components
const gr = createClient({ siteKey: process.env.GLOWRANK_SITE_KEY! });
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props) {
const page = await gr.getPageBySlug((await params).slug);
return page ? glowrankMetadata(page) : {};
}
export default async function Article({ params }: Props) {
const page = await gr.getPageBySlug((await params).slug);
if (!page) notFound();
return (
<article className="prose">
<Markdown>{page.markdown ?? ""}</Markdown>
</article>
);
}(If you want the page's JSON-LD too, page.jsonLd is on the payload —
jsonLdScriptTag/escapeJsonLd from @glowrank/content build the script tag
safely, or let <GlowRankArticle> below handle it.)
<GlowRankArticle> — the convenience alternative
No markdown pipeline in your app? <GlowRankArticle> renders the page's
sanitised HTML as a semantic fragment — an <article> with the page body plus
the page's JSON-LD script, nothing else. It still inherits whatever your
stylesheet already does (so e.g. Tailwind's prose just works):
export default async function Article({ params }: Props) {
const page = await gr.getPageBySlug((await params).slug);
if (!page) notFound();
return <GlowRankArticle page={page} className="prose" />;
}<GlowRankArticle> is a plain server-component-compatible function (no hooks,
no "use client"; import it from @glowrank/next alongside createClient).
The body HTML is sanitized twice server-side by GlowRank (at write time and at
the API boundary); see Trust model.
Metadata
glowrankMetadata(page) returns a Next Metadata object — title (seoTitle
wins), description, and the canonical URL on your origin — for
generateMetadata, as in the example above.
Where to mount it: your connection's base path
Each page's canonical URL (and every sitemap entry) is
<your origin><basePath>/<slug>, where basePath is the one configured on your
GlowRank connection (default /guides).
Put the native page at that path, e.g. app/guides/[slug]/page.tsx.
Mounted anywhere else (say /articles/[slug]), the page declares a canonical
URL it does not serve, and search and answer engines are told to index a
different URL.
Native pages do not answer the verification probe or serve the sitemap, and
GlowRank does not publish to a connection until the probe at
<basePath>/__glowrank-check echoes your siteKey.
Add those two routes next to the page; static segments take precedence over
[slug], and %5F is how the App Router spells a leading underscore (a plain
_folder is private and never routed):
// app/guides/%5F_glowrank-check/route.ts -> /guides/__glowrank-check
// app/guides/sitemap.xml/route.ts -> /guides/sitemap.xml
import { createGlowRankHandler } from "@glowrank/next";
export const GET = createGlowRankHandler({ siteKey: process.env.GLOWRANK_SITE_KEY! });(Or merge the sitemap into your root app/sitemap.ts; see
Merging into your own sitemap.)
Why native rendering is the recommended path: it's your site serving your content in your styles — readers (and crawlers) see one coherent origin, not a third-party document embedded in it.
Zero-config alternative: the one-line route handler
Don't want to touch rendering at all? One handler serves complete, server-sanitised HTML documents (unstyled), plus the sitemap and verification probe:
npm install @glowrank/next// app/guides/[[...slug]]/route.ts (use the base path from your GlowRank connection)
import { createGlowRankHandler } from "@glowrank/next";
export const GET = createGlowRankHandler({ siteKey: process.env.GLOWRANK_SITE_KEY! });That's it. The handler now serves, under /guides:
| Path | Response |
| --- | --- |
| /guides/<slug> | Full HTML document for a published GlowRank page |
| /guides/sitemap.xml | Sitemap of all live GlowRank pages |
| /guides/__glowrank-check | Verification probe (echoes your siteKey) |
| anything else | 404 |
Requires next >= 14 (peer dependency) and Node ≥ 18.
ISR & revalidation
Every upstream fetch is made with next: { revalidate, tags: ['glowrank'] }:
Time-based (ISR): pages refresh automatically every
revalidateseconds (default 300). Override per handler:export const GET = createGlowRankHandler({ siteKey: process.env.GLOWRANK_SITE_KEY!, revalidate: 60, // seconds; or `false` to cache until tag revalidation });On-demand: purge all GlowRank content instantly from a Server Action or webhook route:
import { revalidateTag } from "next/cache"; revalidateTag("glowrank"); // exported as GLOWRANK_CACHE_TAG
Responses also carry Cache-Control: public, s-maxage=<revalidate>,
stale-while-revalidate so CDNs in front of your app behave consistently.
Verification (__glowrank-check)
When you click Verify in the GlowRank dashboard, GlowRank requests
https://your-site.com/<basePath>/__glowrank-check and expects your siteKey
echoed back — proving the SDK is mounted at the path configured on your
connection. The handler answers this automatically (no API call involved), so
verification passes as soon as the route above is deployed. If verification
fails, check that the route directory matches your connection's base path
exactly and that GLOWRANK_SITE_KEY is set in the deployed environment.
Merging into your own sitemap
Prefer one sitemap at the site root? Merge GlowRank entries into
app/sitemap.ts instead:
import type { MetadataRoute } from "next";
import { createClient, glowrankSitemapEntries } from "@glowrank/next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const client = createClient({ siteKey: process.env.GLOWRANK_SITE_KEY! });
const glowrank = await glowrankSitemapEntries(client);
return [{ url: "https://example.com" }, ...glowrank];
}Not on the App Router?
Use @glowrank/content
directly — the framework-agnostic core this adapter is built on (Express,
Fastify, Remix, plain http, …).
Trust model
GlowRank content is model-generated from inputs we don't fully control (your scraped site copy, review text), so we treat it as untrusted and harden it before it reaches you — at write time and again at the API boundary.
markdown(recommended). No markup to trust — render it with your own components. The zero-trust path.html(sanitised). Allowlist sanitisation keeps only semantic content (headings, paragraphs, lists, emphasis, blockquotes, tables,code/pre,hr/br, http/httpsalinks with forcedrel="noopener noreferrer") and stripsscript/style/iframe/object/embed/form, allon*handlers, andjavascript:/data:URLs. This is whatcreateGlowRankHandlerserves.
Both are safe to render; Markdown just gives you a format with no HTML to review.
License
MIT
