
A useful AI feature in Gutenberg should feel fast without becoming a publishing backdoor. The editor can stream a draft as it is generated, but the API key must remain on the server, every request must be authorized, and the output must stay visibly separate from approved content until an editor accepts it.
There is also an important 2026 correction: do not start this integration with the OpenAI Assistants API. OpenAI deprecated Assistants after the Responses API reached feature parity and scheduled its shutdown for August 26, 2026. A new WordPress integration should use the Responses API.
Never call OpenAI directly from block-editor JavaScript. Any browser-delivered key can be extracted and abused. The server boundary is also where you apply capability checks, usage quotas, audit logging, and prompt constraints.
The official JavaScript SDK supports asynchronous streaming from responses.create(). This example uses a server route and emits only the text deltas the editor needs.
// Server-side route; never bundle this into Gutenberg JavaScript.
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
export async function POST(request: Request) {
const user = await authenticateWordPressRequest(request)
if (!user?.capabilities?.edit_posts) {
return new Response('Forbidden', { status: 403 })
}
const { brief } = await request.json()
if (typeof brief !== 'string' || brief.length < 20 || brief.length > 4000) {
return new Response('Invalid brief', { status: 400 })
}
await enforceRateLimit(user.id)
const stream = await openai.responses.create({
model: 'gpt-5',
instructions: [
'Create a factual editorial draft from the supplied brief.',
'Do not invent statistics, quotations, links, or customer claims.',
'Return plain text with short section headings.'
].join(' '),
input: brief,
stream: true,
})
const body = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
try {
for await (const event of stream) {
if (event.type === 'response.output_text.delta') {
controller.enqueue(encoder.encode(event.delta))
}
}
} finally {
controller.close()
}
},
})
return new Response(body, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-store',
'X-Content-Type-Options': 'nosniff',
},
})
}authenticateWordPressRequest() and enforceRateLimit() are application-specific on purpose. If the streaming service is separate from WordPress, exchange a short-lived, signed token that contains the user ID, capability, audience, and expiry. Do not forward a reusable WordPress session cookie to another origin.
If you host the proxy inside PHP, test actual streaming through PHP-FPM, your reverse proxy, CDN, and browser. Buffering at any layer can turn a stream into one delayed response. A small Node or edge service is often more predictable for long-lived event delivery.
A plugin sidebar can read the response body incrementally. Keep generated content in component state until the editor deliberately inserts it.
async function generateDraft(brief, setDraft, signal) {
const response = await fetch(window.wpstackAi.endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.wpstackAi.nonce,
},
body: JSON.stringify({ brief }),
signal,
});
if (!response.ok || !response.body) {
throw new Error(`Generation failed (${response.status})`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let draft = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
draft += decoder.decode(value, { stream: true });
setDraft(draft);
}
draft += decoder.decode();
setDraft(draft);
return draft;
}Use an AbortController for a visible Stop button and cancel when the sidebar unmounts. Disable duplicate submissions while a request is active. Show failure states clearly; silently inserting a partial response is worse than showing an error.
Generated output should become normal Gutenberg blocks that an editor can inspect and rearrange. The safest baseline is plain text split into paragraphs and headings, then converted with WordPress block APIs.
import { createBlock } from '@wordpress/blocks';
import { dispatch } from '@wordpress/data';
function insertDraft(draft) {
const blocks = draft
.split(/\n{2,}/)
.map((part) => part.trim())
.filter(Boolean)
.map((content) => createBlock('core/paragraph', { content }));
dispatch('core/block-editor').insertBlocks(blocks);
}This deliberately treats model output as text. If you request structured JSON, validate it against a strict schema on the server and allowlist block types and attributes before creating blocks. Never pass untrusted model HTML directly to dangerouslySetInnerHTML or save it without WordPress sanitization.
Register blocks on the server with block.json; WordPress recommends metadata-based registration. For a sidebar-only tool, enqueue the editor script with the required WordPress packages and expose only the endpoint URL and a REST nonce.
If you store generation state in post meta, register the field with a concrete type, single => true, an authorization callback, and show_in_rest => true. The post type must support custom fields. Do not store the OpenAI API key, complete prompts containing secrets, or raw provider responses in post meta.
edit_posts or the more specific capability required by your workflow on every request.A WordPress nonce helps protect a logged-in REST request from cross-site request forgery, but it is not authorization by itself. Always verify the current user and capability server-side.
The model can produce fluent errors. It may invent a product feature, misstate a version, or create a plausible but nonexistent source. Generated copy should enter a review state, not jump directly to publication.
Give editors a compact checklist: verify every factual claim, open every link, remove fabricated quotations, check licensing for media, confirm brand voice, inspect headings and accessibility, and disclose AI assistance where policy or law requires it. Preserve normal WordPress revisions so accepted changes remain auditable.
Build new integrations on the Responses API, stream through a protected server endpoint, keep generated text outside the post until an editor accepts it, and convert approved text into ordinary Gutenberg blocks. The result is responsive without exposing credentials or bypassing the editorial process.
Use the official OpenAI API quickstart, the Assistants migration notice, and the WordPress block metadata and post meta documentation as the source of truth.

Aditya Bhimrajka is a technology entrepreneur, product strategist, and software solutions expert with over a decade of experience building scalable web and mobile applications. His expertise spans SaaS, AI, cloud technologies, custom software development, and digital transformation. Passionate about solving real-world business challenges through technology, Aditya shares practical insights on WordPress, plugins, software development, startup growth, product strategy, and emerging technologies. At WPStack, he writes actionable, experience-driven content that helps developers, businesses, and website owners build secure, high-performing, and future-ready WordPress solutions.
Post a Comment