@glowrank/content
v0.2.0
Published
Framework-agnostic client for the GlowRank public content API — fetch and server-render your GlowRank pages on your own origin.
Readme
@glowrank/content
Framework-agnostic client for the GlowRank public
content API. Fetch the pages GlowRank generates for your business and
server-render them on your domain, with any Node/JS stack — Express,
Fastify, Remix, plain http, custom SSR.
This content is yours. Pages are served from your origin, canonical URLs point at your domain, and everything the API returns (Markdown, HTML, plain text, JSON-LD, metadata) is yours to keep — if you ever stop using GlowRank, the pages you've published stay yours.
Zero runtime dependencies. Node ≥ 18 (uses global fetch).
Install
npm install @glowrank/contentRecommended: render Markdown with your own components
Every page ships a markdown field — the recommended integration format.
Render it with your own Markdown renderer and your own components, inside your
own layout: you never have to trust our HTML. Your design system owns the
look; GlowRank owns the words.
import { createClient } from "@glowrank/content";
import Markdown from "react-markdown"; // or markdown-it, marked, remark, …
const gr = createClient({ siteKey: process.env.GLOWRANK_SITE_KEY! });
const page = await gr.getPageBySlug("lip-fillers"); // full payload, or null
if (page) {
// Render `page.markdown` inside YOUR components / layout.
render(<YourArticleLayout title={page.title}>
<Markdown>{page.markdown ?? ""}</Markdown>
</YourArticleLayout>);
}The source of truth is sanitised HTML; markdown is derived from it
server-side (deterministic, stored — no per-request conversion) so it always
matches the page body. page.html remains available for the full-page handlers
below and for the WordPress/proxy render paths.
Fastest path: full-page HTML handler
If you don't want to bring a Markdown renderer, renderPageHtml returns a
complete, ready-to-serve HTML document. The body is server-sanitised (see
Trust model).
import { createClient, renderPageHtml } from "@glowrank/content";
const gr = createClient({ siteKey: process.env.GLOWRANK_SITE_KEY! });
const manifest = await gr.getManifest(); // live pages: id, slug, title, updatedAt
const page = await gr.getPageBySlug("lip-fillers"); // full payload, or nullExpress (10 lines)
import express from "express";
import { createClient, renderPageHtml } from "@glowrank/content";
const app = express();
const gr = createClient({ siteKey: process.env.GLOWRANK_SITE_KEY! });
app.get("/guides/__glowrank-check", (_req, res) => {
const probe = gr.checkProbe();
res.set(probe.headers).send(probe.body);
});
app.get("/guides/:slug", async (req, res) => {
const page = await gr.getPageBySlug(req.params.slug);
if (!page) return res.status(404).send("Not found");
res.type("html").send(renderPageHtml(page));
});
app.listen(3000);Plain http (10 lines)
import http from "node:http";
import { createClient, renderPageHtml } from "@glowrank/content";
const gr = createClient({ siteKey: process.env.GLOWRANK_SITE_KEY! });
http.createServer(async (req, res) => {
const slug = (req.url ?? "").replace(/^\/guides\//, "").split("?")[0];
if (slug === "__glowrank-check") {
const probe = gr.checkProbe();
return res.writeHead(200, probe.headers).end(probe.body);
}
const page = await gr.getPageBySlug(slug);
if (!page) return res.writeHead(404).end("Not found");
res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(renderPageHtml(page));
}).listen(3000);The verification probe
When you click Verify in the GlowRank dashboard, GlowRank fetches
https://your-site.com<basePath>/__glowrank-check and expects your siteKey
echoed in the response body — that proves the SDK is live at the path you
configured. client.checkProbe() gives you the exact { body, headers } to
respond with (see the examples above).
API
createClient({ siteKey, apiBase?, cacheTtlMs?, fetch? })apiBasedefaults tohttps://glowrank.io.cacheTtlMs: in-memory response cache TTL (default60_000ms) so per-request usage doesn't hammer the API; pass0orfalseto disable.fetch: custom fetch implementation (used by framework adapters).
client.getManifest()→{ basePath, publicOrigin, items }. ThrowsGlowRankApiError(status 404) for an unknown siteKey.client.getPage(id)/client.getPageBySlug(slug)→ full page payload (markdown,html,text,title,seoTitle,metaDescription,jsonLd,targetPath,canonicalUrl,updatedAt) ornullwhen not found.markdownis the recommended render target;htmlis server-sanitised.client.checkProbe()→{ body, headers }for the verification endpoint.renderPageHtml(page, { lang?, extraHead? })→ a complete HTML document string. Composable pieces are exported too:renderPageHead,jsonLdScriptTag,escapeHtml.
Errors: 404s return null; network failures and 5xx responses throw a typed
GlowRankApiError (status, url — with the siteKey redacted).
Using Next.js? Use @glowrank/next
— a one-line route handler with ISR/tag revalidation built on this client.
Trust model
GlowRank content is model-generated, and the model reads inputs we don't fully control (your scraped site copy, review text). So we treat the output as untrusted and harden it before it ever reaches you — twice: once at write time and again at the API boundary.
markdown(recommended). No markup to trust at all — render it with your own renderer and components. This is why the field exists: a zero-trust integration path for teams who don't want to inject third-party HTML.html(sanitised). Server-side allowlist sanitisation strips everything that isn't semantic content. What we keep: headings (h1–h6), paragraphs, lists,strong/em/b/i, blockquotes, tables,code/pre,hr/br, andalinks (http/https only, forcedrel="noopener noreferrer"). What we strip:script,style,iframe,object,embed,form, everyon*event handler, andjavascript:/data:URLs. Images are not emitted today and are not allowlisted.
Both fields are safe to render. Markdown simply gives you a format with no HTML to review at all.
License
MIT
