Thanks for your interest in contributing! Here's what you need to know.
Check if the source is Cloudflare-protected 24/7. If it is, don't bother submitting it - we can't support sources that are constantly behind Cloudflare protection. Test the source multiple times over several hours to confirm it's accessible.
Before writing any code:
- Visit the manga source website
- Check if it's accessible without CAPTCHA or Cloudflare challenges
- Verify it has a search function and chapter listings
- Test it multiple times over different hours/days
- If you hit Cloudflare protection every time, stop here - the source isn't viable
Create a new file in src/lib/scrapers/your-source.ts:
import { BaseScraper } from "./base";
import { ScrapedChapter, SearchResult } from "@/types";
import * as cheerio from "cheerio";
export class YourSourceScraper extends BaseScraper {
getName(): string {
return "YourSource";
}
getBaseUrl(): string {
return "https://yoursource.com";
}
getType(): SourceType {
return "scanlator"; // or "aggregator"
}
canHandle(url: string): boolean {
return url.includes("yoursource.com");
}
async search(query: string): Promise<SearchResult[]> {
const response = await fetch(
`${this.getBaseUrl()}/search?q=${encodeURIComponent(query)}`,
);
const html = await response.text();
const $ = cheerio.load(html);
// Your scraping logic here
return [];
}
async getChapters(url: string): Promise<ScrapedChapter[]> {
const response = await fetch(url);
const html = await response.text();
const $ = cheerio.load(html);
// Your scraping logic here
return [];
}
}Some sources need special headers or server-side execution to bypass bot detection. If your source gets blocked with 403/503 errors or requires specific browser headers, mark it as client-only.
Examples: AsuraScan, WeebCentral
export class YourSourceScraper extends BaseScraper {
isClientOnly(): boolean {
return true; // Marks this source as client-only
}
protected override async fetchWithRetry(url: string): Promise<string> {
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://yoursource.com/",
// Add other headers as needed
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.text();
}
}HTML Proxy: If custom headers aren't enough, there's an HTML proxy at /api/proxy/html. To use it, add your domain to the whitelist in src/app/api/proxy/html/route.ts. See AsuraScan or WeebCentral for examples.
Add it to src/lib/scrapers/index.ts:
import { YourSourceScraper } from "./your-source";
const scrapers: BaseScraper[] = [
// ... existing scrapers
new YourSourceScraper(),
];No tests = no merge. Add tests in src/tests/sources/scrapers.test.ts:
describe("YourSource", () => {
const scraper = new YourSourceScraper();
it("should handle YourSource URLs", () => {
expect(scraper.canHandle("https://yoursource.com/manga/123")).toBe(true);
expect(scraper.canHandle("https://othersource.com/manga/123")).toBe(false);
});
it("should search for manga", async () => {
const results = await scraper.search("one piece");
expect(results.length).toBeGreaterThan(0);
expect(results[0]).toHaveProperty("title");
expect(results[0]).toHaveProperty("url");
expect(results[0].url).toContain("yoursource.com");
}, 10000);
it("should get chapter list", async () => {
const chapters = await scraper.getChapters(
"https://yoursource.com/manga/one-piece",
);
expect(chapters.length).toBeGreaterThan(0);
expect(chapters[0]).toHaveProperty("number");
expect(chapters[0]).toHaveProperty("url");
}, 10000);
});Run tests before submitting:
npm test
npm run test:sources # Must passAdd your source to the table in README.md under "Supported Sources".
Some sources have APIs or pages that provide curated content like trending manga, latest updates, or new releases. If a source supports this, you can add frontpage functionality.
Look for:
- Trending/popular manga sections
- Latest updates feeds
- New releases pages
- API endpoints that return curated lists
Create a new file in src/lib/frontpages/your-source.ts:
import { FrontpageManga, FrontpageSection } from "@/types";
import { BaseFrontpage, FrontpageSectionConfig, FrontpageFetchOptions } from "./base";
export class YourSourceFrontpage extends BaseFrontpage {
private readonly baseUrl = "https://yoursource.com";
getSourceId(): string {
return "yoursource";
}
getSourceName(): string {
return "YourSource";
}
getAvailableSections(): FrontpageSectionConfig[] {
return [
{
id: "trending",
title: "Trending",
type: "trending",
supportsPagination: true,
supportsTimeFilter: false,
},
// Add more sections as needed
];
}
async fetchSection(
sectionId: string,
options: FrontpageFetchOptions = {}
): Promise<FrontpageSection> {
const { page = 1, limit = 30 } = options;
// Fetch and parse the data
const items: FrontpageManga[] = [];
// Your implementation here
return {
id: sectionId,
title: "Trending",
type: "trending",
items,
supportsPagination: true,
supportsTimeFilter: false,
};
}
}Add it to src/lib/frontpages/index.ts:
import { YourSourceFrontpage } from "./your-source";
const frontpages: BaseFrontpage[] = [
// ... existing frontpages
new YourSourceFrontpage(),
];Document the frontpage support in the README under the /api/frontpage section.
trending- Popular/trending mangamost_followed- Most followed/bookmarkedlatest_hot- Latest updates (hot/popular)latest_new- Latest updates (new releases)recently_added- Recently added to the sitecompleted- Completed series
Each FrontpageManga item should include:
{
id: string; // Unique identifier
title: string; // Manga title
url: string; // Full URL to manga page
coverImage?: string; // Cover image URL
latestChapter?: number;
lastUpdated?: string;
rating?: number;
followers?: string;
type?: string; // manga, manhwa, manhua
status?: string; // releasing, finished, etc.
synopsis?: string;
}- TypeScript: No
anytypes unless absolutely necessary - Error Handling: Wrap fetch calls in try-catch blocks
- Consistent Formatting: Run
npm run lintbefore committing - No Console Logs: Remove debugging console.logs before submitting
- Clear Naming: Use descriptive variable names, not
x,y,data
- Fork the repository
- Create a feature branch:
git checkout -b add-yoursource-scraper - Implement scraper + tests
- Run all tests:
npm test(must pass) - Run linter:
npm run lint(must pass) - Commit with clear message:
feat: add YourSource scraper - Push and create PR
Your PR must include:
- New scraper implementation
- Tests with >80% coverage
- Proof that source works (screenshot or testing notes)
- Confirmation that source is not Cloudflare-protected 24/7
- Updated README with source details
- All existing tests still pass
- No TypeScript errors
- No linter warnings
PRs will be immediately closed if:
- No tests included
- Tests don't pass
- Source is Cloudflare-protected 24/7
- Code doesn't follow existing patterns
- TypeScript errors present
- Scrapes actual manga images (metadata only!)
Before submitting, manually test:
- Search functionality returns relevant results
- Chapter lists are accurate and in order
- All URLs are valid
- The source doesn't require login/authentication
- The source is accessible from different IPs/locations
Tests should verify:
- URL pattern matching works
- Search returns array of SearchResult objects
- Chapters returns array of ScrapedChapter objects
- Each result has required fields (title, url, etc.)
- Results are from the correct source
- Website structure may vary by region/IP
- Try from different network
- Check if you're hitting rate limits
- Verify HTML selectors are correct
- If it's blocked >50% of the time, don't submit
- Intermittent protection is okay, but note it in PR
- We may mark it as "unstable" in documentation
Open an issue first if you're unsure about:
- Whether a source is viable
- Implementation approach
- Testing strategy
Be professional. Don't be a jerk. Respect maintainer decisions.
That's it. Write good code, write good tests, and we'll merge it.