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 ExtensionAPI below. 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

  1. Host your script (and manifest, if you make one) at a public URL — GitHub Pages, a CDN, or any static host works.
  2. Make sure the host sends permissive CORS headers so the server can fetch it.
  3. On the Extensions page, paste your manifest or repository URL to install it.
  4. 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 — no require, 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. a Referer) from getPages so 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 id you return from search/getChapters consistent across versions — imported stories and reading progress are keyed to them.