Blog seo
Multilingual blog for Medusa v2 with SEO built in: posts, categories, authors, any number of locales with fallback rules, scheduled publishing, preview links, rich editor, shoppable product cards, JSON-LD / hreflang / sitemap / RSS helpers — by Haus Kayu
medusa-blog-seo
A multilingual blog for Medusa v2 with SEO built in. Posts, categories and authors in any number of
languages (configured, not hard-coded), per-language slugs with permanent redirects when they change, fallback
rules, scheduled publishing, preview links, a rich editor with shoppable product cards, and helpers for JSON-LD,
hreflang, sitemaps and RSS.
Blog multibahasa untuk Medusa v2 dengan SEO bawaan: tulisan, kategori, dan penulis dalam bahasa apa pun
(diatur, bukan hard-code), slug per bahasa dengan redirect permanen, aturan fallback, publikasi terjadwal,
tautan pratinjau, editor kaya dengan kartu produk, dan helper JSON-LD, hreflang, sitemap, RSS.
Compatibility
| Plugin | Medusa | Node | Tested in CI on |
|---|---|---|---|
| 0.1.x | 2.19.0 or newer 2.x () | 20.19+ or 22.12+ | Medusa 2.19.0 and 2.22.2 |
Images go through the File Module (local, S3, R2, …). is optional: when it is installed, uploads are turned upright, resized and stripped of EXIF data.
Quick start
npx medusa db:migrateOpen Blog in the Admin sidebar. Add your languages under Settings (the default is one locale, ), write a post, publish it. The storefront reads and with . Full steps: Installation · Languages · Options · Storefront (Next.js) · Store API · Limits.
| Posts | Editor, side by side with the default language |
|---|---|
![]() | ![]() |
| Languages |
|---|
![]() |
English
Features
Content
- Posts with per-language title, slug, excerpt, body, meta title/description, sharing (OG) image, cover alt text and tags; shared cover (4:3) and wide cover (16:9), category, author, related products, one featured post.
- Categories with name, slug and description per language; authors (person or organisation) with photo, website, profile links (), and job title and bio per language.
- Rich editor in the Admin (Tiptap): headings, lists, quotes, links, tables, photos with alt and caption, YouTube embeds, product cards picked from the catalogue, and Q&A blocks. Bodies are sanitised server-side and rendered into blocks, a table of contents (from H2/H3) and FAQ items (an H3 ending in "?" and its paragraphs).
- Draft → scheduled → published. A job (every 5 minutes, to change it) publishes due posts. Drafts autosave in the Admin; published posts save on demand. The first public date is kept when a post is republished; moves on every public change.
- Preview links per language with a secret token (renewable), showing drafts and incomplete translations.
Languages
- Not hard-coded. The list of locales (BCP 47: , , , …), their names, text direction, order, the default locale and the fallback rule are set in Admin → Blog → Settings, or fixed in the plugin options (then the Admin shows them read-only).
- Fallback per store: (show the default language at that locale's address), (same, and the storefront can say "not yet translated"), or (the post exists only in languages with a complete translation).
- Completeness per language in the Admin (tabs, badges, a "missing in …" filter, side-by-side editing with the default language). A post can be published once its default-language translation is complete (and a cover, if ); other languages become visible as they are completed.
- Slug history. When a slug changes (edited by hand, or a locale that used to show the fallback gets its own translation), the old address is kept in and the Store API answers for it. History always points at the post, never at another slug, so there are no chains or loops; a live slug always wins over a historical one.
- and maps, sitemap entries with alternates, an RSS feed per language, values, and per-locale routing (path prefix or a separate origin per language).
Integration
- Store API , Admin API , a typed client (), SEO helpers (), JSON-LD helpers ().
- Events , , , , with slugs (and previous slugs) per locale.
- Revalidate webhook (optional): public changes are batched and POSTed to your storefront with a secret header, so it can revalidate or purge exactly the pages that changed.
- Products are linked through a module link ( in Query), so a product page can list the posts that feature it.
- Admin texts in English and Indonesian (follows the Admin user's language).
Requirements
- Medusa 2.19.0 or newer 2.x, PostgreSQL. Redis recommended (events, workflows).
- The File Module for uploads (the default local provider works; S3/R2 for production). optional.
Installation
- (and if you want uploads processed).
- Add the plugin to (see Quick start). makes the Admin's "preview" and "public page" links absolute.
- creates the tables , , , , , , , and the post–product link table.
- Restart. The job runs on the worker.
- In the Admin, open Blog → Settings and set your languages (unless you fixed them in the options).
Note for pnpm without hoisting or a locally linked copy: the Admin build picks up this plugin's Tailwind classes through from ; if the bundler cannot resolve the package (pnpm with , or a plugin symlinked from outside the project), the plugin's Admin pages lose their layout. npm, Yarn and pnpm's defaults are fine; otherwise install from npm (or ) next to the bundler, or add to .
Languages and fallback
Every post has a translation row per language. The default locale is required for publishing; the others are optional and become visible as they are completed (title, excerpt, body). What a reader sees at a language that has no complete translation depends on :
| Post list and post page in that language | in the API | |
|---|---|---|
| The default-language text at that language's address | ||
| Same; the storefront can show a "not yet translated" note | ||
| The post is not listed and its address answers 404 | – |
A locale that shows the fallback uses the default locale's slug. When its own translation is completed, the slug changes to the translated one and the previous address is kept as a permanent redirect (see Slug history).
Locales can be given per store in the Admin, or fixed in the options:
Names default to the language's own name (, ), direction to for Arabic, Hebrew, Persian and Urdu. Disabling a language in the Admin hides it from the store but keeps its text.
The locale of a Store request is taken from , then the header, then Medusa's own , else the default. An exact match wins, then the same language ( → ), else the default.
Plugin options
Non-default locales get the path prefix unless says otherwise ( for no prefix, or for a separate domain). (env) changes the publish job's schedule.
Storefront (Next.js)
The typed client works with or any object with .
is the body as typed blocks (paragraphs, headings, images, product cards with , YouTube ids, tables, Q&A), for storefronts that render their own components instead of .
gives for . answers the list page (the featured post is returned separately so pages never repeat it; turns that off). renders a preview page; use .
Revalidation. With set, the backend POSTs (batched, header):
Revalidate the post pages for and , the category pages, and the index when is true.
Store API
Requires a publishable API key. Locale from / .
| Method | Path | Answer |
|---|---|---|
| GET | : enabled locales (code, name, dir), default, fallback, routing, x_default | |
| GET | ||
| GET | : every public post with slug/title/excerpt per readable locale (sitemap, feeds, llms.txt) | |
| GET | , or for a previous slug, or 404 | |
| GET | with and | |
| GET | , | authors with job title and bio in the locale |
| GET | for any status, incomplete translations included |
A post answers , (false when shown through the fallback), , (slug per locale that can be read), , , , , , , , , , SEO fields. Errors are with HTTP 404 / 400 / 409 () / 403 ().
Admin API
(GET/POST), (GET), (GET with , , , , , , ; POST), (GET/POST/DELETE), (POST : a future date schedules), , , , , , , (POST JSON or multipart ). in wraps them.
Limits
- The index view (), sitemap and RSS helpers, slug lookups and category counts load up to 5000 public posts into memory per request. That covers most shops' blogs; above that, paginate and build sitemaps from it instead of .
- Bodies are capped at (2 MiB by default) after sanitising; images at (25 MiB) and (2400 px).
- Related products per post: 12. Tags per translation: 20, lower-case.
- Category and author pages, tags and search are per locale; there is no cross-locale search.
FAQ
Can I use it in one language? Yes. The default settings are one locale, ; the Admin shows one tab.
I write in Indonesian and translate some posts to English. What should I set? Locales (default) and , if untranslated posts must not appear under at all, or if they should appear in Indonesian until translated. In every case the English address redirects permanently once the translation is complete.
Does changing a slug break links? No. Old slugs stay as permanent redirects, per language.
Where are images stored? In the File Module (the one configured in your ). The Admin shows only images from the File Module's origin and .
Does it conflict with another blog module? Tables are , the module key is , routes are , the Admin route is : it can run next to an existing blog while you migrate.
Bahasa Indonesia
Fitur
- Tulisan dengan judul, slug, ringkasan, isi, meta title/description, gambar berbagi (OG), teks alt sampul, dan tag per bahasa; sampul 4:3 dan 16:9, kategori, penulis, produk terkait, satu tulisan unggulan.
- Kategori (nama, slug, deskripsi per bahasa) dan penulis (orang/organisasi, foto, situs, tautan profil, jabatan dan bio per bahasa).
- Editor kaya di Admin: heading, daftar, kutipan, tautan, tabel, foto dengan alt dan keterangan, YouTube, kartu produk dari katalog, blok tanya-jawab. Isi disanitasi di server, jadi blok, daftar isi (H2/H3), dan FAQ (H3 berakhiran "?" + paragrafnya).
- Draf → terjadwal → terbit lewat job tiap 5 menit (). Draf tersimpan otomatis; tulisan terbit disimpan saat menekan Simpan. Tanggal terbit pertama dipertahankan saat diterbitkan ulang.
- Tautan pratinjau per bahasa dengan token rahasia (bisa diperbarui).
- Bahasa tidak di-hardcode: daftar locale (BCP 47), nama, arah teks, urutan, bahasa utama, dan aturan fallback diatur di Admin → Blog → Pengaturan atau dikunci lewat opsi plugin.
- Fallback (tampilkan bahasa utama di alamat bahasa itu), (sama, storefront bisa memberi catatan "belum diterjemahkan"), atau (tulisan hanya ada di bahasa yang terjemahannya lengkap).
- Kelengkapan per bahasa di Admin: tab, badge, filter "belum ada …", edit berdampingan dengan bahasa utama. Tulisan bisa terbit begitu terjemahan bahasa utamanya lengkap; bahasa lain muncul begitu lengkap.
- Riwayat slug: slug yang berubah (diedit, atau bahasa yang tadinya fallback dapat terjemahan sendiri) disimpan di ; Store API menjawab untuk slug lama. Riwayat selalu menunjuk ke tulisan, bukan slug lain: tidak ada rantai atau loop; slug yang masih hidup selalu menang.
- + , sitemap dengan alternates, RSS per bahasa, , routing per locale (prefix path atau domain terpisah).
- Store API , Admin API , klien bertipe (), helper SEO () dan JSON-LD (), event , webhook revalidate (opsional, batch), link produk (). Teks Admin dalam Inggris dan Indonesia.
Pasang
npx medusa db:migrateBuka Blog di sidebar Admin. Tabel opsi lengkap: Plugin options. Contoh storefront Next.js (halaman tulisan dengan redirect slug lama, metadata + hreflang, sitemap, RSS): Storefront (Next.js). Batasan (5000 tulisan di memori untuk index/sitemap/RSS, ukuran isi dan gambar): Limits.
Catatan untuk pnpm tanpa hoisting atau plugin yang di-symlink lokal: build Admin mengambil class Tailwind plugin ini lewat dari ; kalau bundler tidak bisa me-resolve paketnya (pnpm dengan , atau plugin di-symlink dari luar proyek), halaman Admin plugin kehilangan layout-nya. npm, Yarn, dan pnpm bawaan aman; selain itu pasang dari npm (atau ) di sebelah bundler, atau tambahkan di .
Alur
- Tulis draf di Admin (tersimpan otomatis), isi bahasa utama sampai lengkap, bahasa lain menyusul.
- Terbitkan sekarang atau jadwalkan; job menerbitkan saat waktunya tiba dan memancarkan .
- Storefront membaca ; slug lama dijawab → redirect 301.
- Dengan , backend mengirim daftar slug (baru dan lama) yang berubah supaya storefront me-revalidate halaman yang tepat.
Development
The end-to-end suite starts Medusa with the plugin, creates languages, posts, categories and authors through the Admin API, uploads images, publishes and schedules, and checks the Store API (fallback modes, redirects for old slugs, preview, index view, product links) and the revalidate webhook, on Medusa 2.19.0 and 2.22.2 in CI.
Built for hauskayu.com
Built and used in production by Haus Kayu, handmade teak homeware from Jepara. The blog there (Indonesian with English translations) runs on this plugin.
Other plugins by Haus Kayu
- medusa-reviews-wishlist: verified product reviews with photos, review-request e-mails and wishlists.
- medusa-storage-r2: Cloudflare R2 file storage configured from the Admin.
- medusa-payment-doit: doit.id payments (QRIS, Virtual Account) for Indonesia.
- medusa-payment-manual-transfer: manual bank transfer with payment-proof upload and admin verification.
- medusa-fulfillment-zone-rate: calculated shipping by zone and chargeable weight, with shipment tracking.
Support
- Issues and feature requests: GitHub issues
- E-mail: dev@hauskayu.com
License
MIT © Haus Kayu




