Publishing
Refine can write the article that gets a brand cited, then put it on the brand’s own site. Through the MCP server, an assistant runs that loop on request; a scheduled agent runs it on its own. All of it requires the Autopilot plan.
Three ways onto the site#
| Destination | Setup (in Settings) | Tool | Arrives as |
|---|---|---|---|
| WordPress | Site URL, username and an application password (Author, Editor or Administrator) | publish_article | Draft or published: the customer’s choice |
| Webflow | Site-scoped API token and a CMS collection | publish_article | Always a CMS draft |
| Next.js site | The @getrefine/next package and a site key | publish_to_site | Live at /blog/<slug> within 5 minutes |
Anywhere else (another CMS, a static site, a Reddit thread, LinkedIn), take the HTML from get_content, publish it yourself, then call mark_content_posted with the URL. That closes the item in the queue and starts measuring its effect on visibility.
Drafts by default
Refine writes into someone else’s publication. Making a post live without them seeing it is a choice they opt into in Settings, never a default. Only blog articles can be published; a Reddit reply or a LinkedIn post returns an error naming its type.On request: the five-call loop#
What an assistant does when someone asks “write and publish an article on our weakest prompt”:
1. get_opportunities → the prompt losing the most demand
2. generate_content → { contentId }, generation runs in the background
3. get_content (poll) → status "completed", HTML body, meta, JSON-LD
4. list_publish_targets → is a WordPress or Webflow site connected?
5. publish_article → the article lands in the CMS (draft by default)Pick the prompt
get_opportunities ranks tracked prompts by the Google impressions the brand misses in AI answers (or by lowest mention rate when Search Console is not connected). Its promptId feeds the next call.
Start generation
generate_content with contentType: "blog_article" returns a contentId at once. Without sourceUrls, the article is grounded in the pages AI engines already cite for that prompt.
Poll until it is done
Call get_content every 10 to 15 seconds. status moves from pending to generating to completed; progress and progress_message say where it is. On failed, the reason is in error. Polling faster only spends the 60-per-minute budget.
Check the destination
list_publish_targets returns the connected sites. An empty list means nothing is connected: tell the customer, do not retry. With two sites, note which one to name.
Publish
publish_article with the contentId (and target when two sites are connected). It returns the post URL for WordPress, and the item moves to posted.
On a schedule: the agent loop#
An agent that runs every few days needs three calls, and Refine keeps the pace for it: next_article_to_publish refuses to hand over an article until minGapDays (2 to 5, default 3) have passed since the last post. Scheduling the agent more often than that is safe; extra runs simply stop at cooldown.
every run:
next_article_to_publish
ready: true → publish article.id:
publish_to_site (site runs @getrefine/next)
or publish it yourself, then
mark_content_posted { contentId, publishedUrl }
reason: "cooldown" → stop; try again after next_eligible_at
reason: "queue_empty" → get_opportunities → generate_content
(the next run will find it)Articles come out oldest first. The response carries everything a publisher needs: title, html, meta_description, json_ld and the prompt it answers. Hermes Agent ships with this loop as the refine-publisher skill: see the quickstart to install it.
Publishing on a Next.js site#
@getrefine/next renders Refine articles server-side on the customer’s own domain: real HTML that AI crawlers, which do not run JavaScript, can read and cite. No CMS, no webhook, no rebuild.
Install
npm install @getrefine/nextRequires Next.js 14 or later (App Router) and React 18 or later. Copy the site key from Settings → Site publishing (one key per brand, starting with pk_) into the environment:
REFINE_SITE_KEY=pk_… # Refine → Settings → Site publishing
# optional
REFINE_REVALIDATE=300 # seconds between refreshes (ISR)Add the article route
// app/blog/[slug]/page.tsx
export {
generateStaticParams,
generateMetadata,
default,
} from "@getrefine/next/blog";That single file serves every published article at /blog/<slug>, with its title and meta description in the page metadata. The markup is <article class="refine-article"> with an h1, a time and a .refine-article-body, ready to style.
Add an index (optional)
The package does not create the /blog listing. Build it with getArticles(), or render articles your own way with getArticle(slug):
// app/blog/page.tsx
import { getArticles } from "@getrefine/next";
export default async function BlogIndex() {
const articles = await getArticles();
return (
<ul>
{articles.map((a) => (
<li key={a.slug}>
<a href={`/blog/${a.slug}`}>{a.title}</a>
</li>
))}
</ul>
);
}getArticles() returns { slug, title, description, publishedAt, updatedAt }[] newest first; getArticle(slug) adds html and returns null for an unknown slug. Both read the public content API.
Slugs never move
A slug is derived from the title on first publication, made unique within the brand (-2, -3…) and kept forever, even if the article is republished. Links and citations stay valid.