Build an Extension
ManhwaRead is a reader and catalog — it ships with no content of its own. Extensions are what connect it to a source. If you maintain a translation site or an API, publishing an extension lets every ManhwaRead user read from it. This page is the full contract you need to implement.
Two formats
ManhwaRead can load extensions in two ways:
- Paperback 0.8 — existing Paperback repositories work as-is. Paste the URL of any repo (the page hosting its
versioning.json) on the Extensions page and all of its sources are imported. Nothing to build. - Native — a single JavaScript file that exports the
ExtensionAPIbelow. This is the simplest path if you control the source and want a small, dependency-free extension. The rest of this page documents the native format.
The manifest
A manifest is a small JSON file describing your extension and pointing at its script.
{
"id": "my-source",
"name": "My Source",
"description": "Reads from example.com",
"icon": "https://example.com/icon.png",
"version": "1.0.0",
"languages": ["en"],
"nsfw": false,
"script": "https://your-host.example/my-source.js"
}id — unique, stable, kebab-case. Required.
name, version, languages — required.
script — absolute URL to your JS bundle. Required.
description, icon, nsfw — optional.
The ExtensionAPI
Your script exports an object implementing these methods. Four are required; the fifth is optional. All return Promises.
search(query, page?) → list of results. Required.
getManga(id) → full metadata for one title. Required.
getChapters(mangaId) → the chapter list. Required.
getPages(mangaId, chapterId) → ordered page image URLs. Required.
getHomePageSections() → curated home rows. Optional.
Complete example
A full native extension. Copy this, swap the URLs and field mapping for your source, and host the file.
// my-source.js — a native ManhwaRead extension (CommonJS module).
// The runtime gives you a global `fetch`. There is no `require`, no Node
// built-ins, and no DOM — keep dependencies inlined into this single file.
module.exports = {
// 1. Search the source for a query string.
async search(query, page = 1) {
const res = await fetch(
`https://example.com/api/search?q=${encodeURIComponent(query)}&page=${page}`
)
const data = await res.json()
return data.results.map(r => ({
id: r.slug, // your own stable id for this title
title: r.title,
cover: r.cover_url, // optional
status: r.status, // optional: 'ongoing' | 'completed' | ...
language: 'en', // optional
}))
},
// 2. Full metadata for one title (id is what you returned from search()).
async getManga(id) {
const res = await fetch(`https://example.com/api/manga/${id}`)
const m = await res.json()
return {
id,
titles: [{ lang: 'en', title: m.title }],
cover: m.cover_url, // optional
status: m.status, // optional
genres: m.genres, // optional string[]
authors: m.authors, // optional string[]
artists: m.artists, // optional string[]
description: m.synopsis, // optional
}
},
// 3. The chapter list for a title.
async getChapters(mangaId) {
const res = await fetch(`https://example.com/api/manga/${mangaId}/chapters`)
const data = await res.json()
return data.chapters.map(c => ({
id: c.id, // your own stable chapter id
number: c.number, // numeric, e.g. 12 or 12.5
title: c.title, // optional
language: 'en', // optional
scanlator: c.group, // optional
publishedAt: c.date, // optional ISO string
}))
},
// 4. The ordered page image URLs for one chapter.
async getPages(mangaId, chapterId) {
const res = await fetch(`https://example.com/api/chapter/${chapterId}`)
const data = await res.json()
return data.images.map(url => ({
url,
// Optional: headers the reader must send when loading the image
// (e.g. a Referer when the CDN blocks hotlinking).
headers: { Referer: 'https://example.com/' },
}))
},
// 5. OPTIONAL — sections shown on the home tab for this source.
async getHomePageSections() {
const res = await fetch('https://example.com/api/home')
const data = await res.json()
return data.sections.map(s => ({
title: s.name,
items: s.titles.map(t => ({ id: t.slug, title: t.title, cover: t.cover_url })),
}))
},
}Host & install
- Host your script (and manifest, if you make one) at a public URL — GitHub Pages, a CDN, or any static host works.
- Make sure the host sends permissive CORS headers so the server can fetch it.
- On the Extensions page, paste your manifest or repository URL to install it.
- Want it listed in the in-app directory for everyone? Open a PR adding it to
src/lib/extensions/registry.ts, or ask in our community.
Tips & gotchas
- One file, no dependencies. Native extensions run in a sandbox with only
fetch— norequire, Node APIs, or DOM. Inline everything. - Be fast. A native script has a 5-second load budget and each search has a timeout. Avoid heavy work at module top level.
- Hotlink-protected images? Return the needed
headers(e.g. aReferer) fromgetPagesso the reader can load them. - Cloudflare-walled sources may need a FlareSolverr deployment on the server side — mention it when you submit.
- Stable ids. Keep the
idyou return fromsearch/getChaptersconsistent across versions — imported stories and reading progress are keyed to them.