Install the Blog Plugin
Install @vitnode/blog, enable its API and TanStack Start integration, then publish your first article in AdminCP.
@vitnode/blog is the Content Engine reference plugin. It brings articles,
categories, editorial screens, revisions, public delivery data, and search
indexing—without making your host app impersonate a blog plugin.
Install the package
bun add @vitnode/blog@canaryEnable the API plugin
For a Single App, edit apps/web/src/vitnode.api.config.ts; a split deployment
uses the same change in apps/api/src/vitnode.api.config.ts.
import { blogApiPlugin } from '@vitnode/blog/config.api'
export const vitNodeApiConfig = buildApiConfig({
plugins: [
blogApiPlugin(),
],
})Enable the web plugin
One line, and it brings the blog's pages, its AdminCP screens and its
translations with it - your build reads the factory and writes the loaders into
src/package-messages.gen.ts for you:
import { blogPlugin } from '@vitnode/blog/config'
export const vitNodeConfig = buildConfig({
plugins: [
blogPlugin(),
],
})Migrate, run, and publish
bun run db:migrate
bun devIn AdminCP, create a category at Blog → Categories, then publish an article at Blog → Articles. The plugin owns the editorial work so the host can stay boringly reliable.
Write and translate articles
The article editor reads top to bottom like the published article: the cover image and its alt text, then the title, slug and content. The checks before publishing live in the SEO sheet, so the text gets the whole width.
Check the article before you publish
Press SEO in the header to open the Ready to publish sheet. The badge on the button counts what is still left, so you know when it needs a look. The sheet checks what a published article usually needs:
| Check | Passes when |
|---|---|
| Title fits search results | Every started language has a title of 60 characters or less |
| Excerpt in every language | Every started language has an excerpt |
| Cover image | A cover image is set |
| URL | The language you edit has a friendly URL |
| Cover alt text in every language | Every started language describes the cover |
| Translations complete | Every language has a title, friendly URL and content |
| Translations up to date | No translation is older than the default language |
The title, excerpt and alt text fields show a live counter against the recommended length: 60, 160 and 125 characters. They are recommendations, not limits, so you can still save a longer title.
Each row is green when it is done and says what is missing when it is not, and the bar on top counts how many are ready. You can fix most rows right there: write the excerpt in its row, press Edit under the cover to jump to the alt text, or Open a language that is missing a translation.
Publish or unpublish
The header shows whether the article is a Draft or Published (hover it to see when it went live). To change that, open the arrow next to Save changes and pick Publish or Unpublish. It asks for confirmation first, because readers see the change immediately. Hide and Unhide live in the same menu.
Hide or duplicate an article
The More actions menu of an article in the list has Hide and Duplicate. Hiding takes a published article off the site, search and the sitemap without unpublishing it, and Unhide brings it back with the same URL and date. It needs the Hide and unhide articles permission. Duplicating makes a draft copy with every translation, such as "Hello world (Copy)". See Hide content and Duplicate content.
Translate side by side
Pick a language from Translate. Each translated field then shows the default language on the left, read-only, and the language you translate into on the right. Your shared fields, such as categories and authors, stay in one place.
A translation is marked outdated when the default language was saved after it. Generate from title builds the friendly URL from the translated title.
Use AI helpers
With AI configured, the editor adds:
- Translate on each field, and Translate missing fields for a whole language. Content keeps its formatting.
- Write with AI on the excerpt, which writes it from the title and content.
- AI review in the header, which opens a sheet with suggestions about clarity, completeness, structure and tone. When a suggestion is about the wording, Show fix shows the current text next to a rewrite, and Apply puts the rewrite into the article. Dismiss hides a suggestion you do not agree with. The review never blocks publishing.
The buttons appear only when the API has at least one model in ai.models.
The routes behind them require the can_edit permission on articles. See
AI to configure a model.
AI never saves for you. Read the suggestion, then press Save changes. While others are editing the article, a translated body goes into the shared editor as one change, so everyone sees it and you can undo it.
Write together
Several people can work on one article at once. The header shows who else is in it, and a colored outline with a name tag shows who is editing which field. The title, slug, excerpt and other plain fields are edited by one person at a time; the article body is shared, with everyone's cursor visible. Your changes are autosaved to a draft until someone presses Save changes. See Live editing and autosave for how it works and what it needs on your server.
Upgrade an existing blog
Articles have a localized Excerpt field. Search results use it as the
description and fall back to the article content when it is empty. The article
content is now stored as rich text JSON
instead of HTML; the migration keeps the words of existing articles as one
paragraph, so re-check their formatting. Run your migrations after you upgrade
@vitnode/blog:
bun run db:migrateDeliver it from a plugin
Build the public article page in a plugin too, then use the Content Engine delivery guide for loaders and SEO. That keeps the data model and its public URL together.