Skip to content

Creating Sources

Create JavaScript plugins to connect Ficnest to web novel platforms.

Easy to Build

Ficnest's source system requires only basic JavaScript knowledge (variables, functions, regex). No TypeScript, build systems, or complex tooling required.

What You Need

Your JavaScript file must define:

  1. SourceMetadata - Information about your source
  2. NovelSource - Functions that fetch content

Basic Template

javascript
// SOURCE METADATA
var SourceMetadata = {
    id: "mywebsite",
    name: "My Novel Website",
    baseURL: "https://example.com",
    version: "1.0.0",
    language: "en",
    contentRating: "all",
    iconURL: "https://example.com/icon.png",
    description: "Read novels from My Website",
    author: "Your Name",
    capabilities: {
        popular: true,
        search: true,
        latest: false,
        filters: false
    }
};

// NOVEL SOURCE OBJECT
var NovelSource = {
    baseUrl: "https://example.com",

    getPopularNovels: function(page) {
        var url = this.baseUrl + "/popular?page=" + page;
        var response = fetch(url);

        if (!response || response.error) {
            return [];
        }

        var html = response.text;
        var novels = [];

        // Parse HTML and extract novels

        return novels;
    },

    searchNovels: function(query, page) {
        var url = this.baseUrl + "/search?q=" + encodeURIComponent(query);
        var response = fetch(url);

        if (!response || response.error) {
            return [];
        }

        // Parse and return results
        return [];
    },

    getChapterList: function(novelId) {
        var url = this.baseUrl + "/novel/" + novelId;
        var response = fetch(url);

        if (!response || response.error) {
            return [];
        }

        // Parse chapter list
        return [];
    },

    getChapterContent: function(chapterId) {
        var url = this.baseUrl + "/chapter/" + chapterId;
        var response = fetch(url);

        if (!response || response.error) {
            return "<p>Error loading chapter</p>";
        }

        // Extract and clean content
        return "<p>Chapter content here...</p>";
    }
};

Required Functions

getPopularNovels(page)

Returns popular novels for the given page.

Returns: Array of novel objects

javascript
{
    novelId: "unique-id",        // Required
    title: "Novel Title",        // Required
    coverUrl: "https://...",     // Optional
    author: "Author Name",       // Optional
    description: "Description"   // Optional
}

searchNovels(query, page)

Returns search results.

Parameters:

  • query (string) - Search term
  • page (number) - Page number

Returns: Same structure as getPopularNovels

getChapterList(novelId)

Returns all chapters for a novel.

Returns: Array of chapter objects

javascript
{
    chapterId: "unique-id",   // Required
    title: "Chapter Title",   // Required
    index: 0                  // Required (chapter order)
}

getChapterContent(chapterId)

Returns HTML content of a chapter.

Returns: HTML string with chapter content

Using fetch()

Ficnest's fetch works differently from browser fetch:

javascript
var response = fetch(url);

// Check for errors
if (!response || response.error) {
    return [];
}

// Get content (property, not function!)
var html = response.text;  // Direct access

Response structure:

javascript
{
    ok: true,
    status: 200,
    text: "response body",
    error: null
}

Parsing with Regex

Extract data from HTML using regular expressions:

javascript
// Find all matches
var pattern = /<a href="\/novel\/([^"]+)">([^<]+)<\/a>/g;
var match;

while ((match = pattern.exec(html)) !== null) {
    var novelId = match[1];
    var title = match[2];
}

Helper Functions

Decode HTML Entities

javascript
decodeHTML: function(text) {
    return text
        .replace(/&amp;/g, "&")
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&nbsp;/g, " ");
}

Logging

javascript
log("Fetching page " + page);
log("Found " + novels.length + " novels");

Common Mistakes

Don't use ES6 syntax:

javascript
// DON'T
const NovelSource = { ... };
let myVariable = "value";

// DO
var NovelSource = { ... };
var myVariable = "value";

Don't call .text() as function:

javascript
// DON'T
var html = response.text();

// DO
var html = response.text;

Always encode URLs:

javascript
// DON'T
var url = baseUrl + "?q=" + query;

// DO
var url = baseUrl + "?q=" + encodeURIComponent(query);

Hosting Your Source

Create manifest.json

json
{
    "version": "1.0.0",
    "sources": [
        {
            "id": "mywebsite",
            "name": "My Novel Website",
            "baseUrl": "https://example.com",
            "version": "1.0.0",
            "language": "en",
            "contentRating": "all",
            "scriptUrl": "https://yourserver.com/mywebsite.js",
            "description": "Read novels from My Website",
            "author": "Your Name"
        }
    ]
}

Upload Files

Host both files on a public server:

https://yourserver.com/
├── manifest.json
└── mywebsite.js

Share the manifest URL: https://yourserver.com/manifest.json

Testing

  1. Enable Developer Mode in Settings
  2. Import your source file
  3. Test each function:
    • Popular novels
    • Search
    • Chapter list
    • Chapter content
  4. Check console logs for errors

Testing Checklist

  • [ ] Popular novels load correctly
  • [ ] Search returns relevant results
  • [ ] Chapters appear in correct order
  • [ ] Content displays properly
  • [ ] No ads or scripts in content
  • [ ] Error handling works
  • [ ] Console shows helpful logs

Complete Example

Click to view the full source plugin guide

Full Source Plugin Guide

Ficnest Source Plugin Guide

Welcome! This guide will help you create JavaScript plugins (sources) that work with Ficnest. Sources allow Ficnest to fetch novels and chapters from different websites.

Note: Ficnest's source system is designed to be significantly easier and more intuitive than similar systems like Aidoku or Paperback. If you have basic JavaScript knowledge (variables, functions, and regex), you can create a working source plugin. No complex build systems, TypeScript, or advanced tooling required—just plain JavaScript!

What You Need

Your JavaScript file needs to define two things:

  1. SourceMetadata - Information about your source
  2. NovelSource - Functions that fetch content from the website

Basic Template

Here's a minimal working template you can start with:

javascript
// ================================
// SOURCE METADATA
// ================================
var SourceMetadata = {
    id: "mywebsite",              // Unique ID (lowercase, no spaces)
    name: "My Novel Website",      // Display name
    baseURL: "https://example.com", // Website URL
    version: "1.0.0",              // Your plugin version
    language: "en",                // Language code (en, es, fr, etc.)
    contentRating: "all",          // all, teen, mature, adult
    
    // Optional fields:
    iconURL: "https://example.com/icon.png",
    description: "Read novels from My Website",
    author: "Your Name",
    
    capabilities: {
        popular: true,    // Can get popular novels?
        search: true,     // Can search novels?
        latest: false,    // Can get latest updates?
        filters: false    // Has custom filters?
    }
};

// ================================
// NOVEL SOURCE OBJECT
// ================================
var NovelSource = {
    baseUrl: "https://example.com",
    
    // Required: Get popular novels
    getPopularNovels: function(page) {
        log("Fetching page " + page);
        
        var url = this.baseUrl + "/popular?page=" + page;
        var response = fetch(url);
        
        // Always check for errors!
        if (!response || response.error) {
            log("Error: " + (response ? response.error : "No response"));
            return [];
        }
        
        var html = response.text;
        var novels = [];
        
        // Parse HTML and extract novels
        // ... your parsing code here ...
        
        return novels;
    },
    
    // Required: Search for novels
    searchNovels: function(query, page) {
        log("Searching: " + query);
        
        var url = this.baseUrl + "/search?q=" + encodeURIComponent(query);
        var response = fetch(url);
        
        if (!response || response.error) {
            return [];
        }
        
        var html = response.text;
        var novels = [];
        
        // Parse and return results
        
        return novels;
    },
    
    // Required: Get chapter list for a novel
    getChapterList: function(novelId) {
        log("Getting chapters for: " + novelId);
        
        var url = this.baseUrl + "/novel/" + novelId;
        var response = fetch(url);
        
        if (!response || response.error) {
            return [];
        }
        
        var html = response.text;
        var chapters = [];
        
        // Parse chapter list
        
        return chapters;
    },
    
    // Required: Get chapter content (as HTML)
    getChapterContent: function(chapterId) {
        log("Getting content for: " + chapterId);
        
        var url = this.baseUrl + "/chapter/" + chapterId;
        var response = fetch(url);
        
        if (!response || response.error) {
            return "<p>Error loading chapter</p>";
        }
        
        var html = response.text;
        
        // Extract and clean content
        
        return "<p>Chapter content here...</p>";
    }
};

Required Functions

Your NovelSource object must implement these four functions:

1. getPopularNovels(page)

Returns an array of popular novels for the given page number.

Parameters:

  • page (number) - The page number (starts at 1)

Returns: Array of objects with this structure:

javascript
{
    novelId: "unique-novel-id",           // Required
    title: "Novel Title",                 // Required
    coverUrl: "https://example.com/cover.jpg", // Optional
    author: "Author Name",                // Optional
    description: "Brief description"      // Optional
}

Example:

javascript
getPopularNovels: function(page) {
    var url = this.baseUrl + "/popular?page=" + page;
    var response = fetch(url);
    
    if (!response || response.error || !response.ok) {
        return [];
    }
    
    var novels = [];
    var html = response.text;
    
    // Use regex to find novel links
    var pattern = /<a href="\/novel\/([^"]+)">([^<]+)<\/a>/g;
    var match;
    
    while ((match = pattern.exec(html)) !== null) {
        novels.push({
            novelId: match[1],
            title: match[2],
            coverUrl: "",
            author: "",
            description: ""
        });
    }
    
    return novels;
}

2. searchNovels(query, page)

Returns search results for the given query.

Parameters:

  • query (string) - The search term
  • page (number) - The page number

Returns: Same structure as getPopularNovels

Example:

javascript
searchNovels: function(query, page) {
    var url = this.baseUrl + "/search?q=" + encodeURIComponent(query);
    var response = fetch(url);
    
    if (!response || response.error) {
        return [];
    }
    
    // Parse search results (same format as popular novels)
    return novels;
}

3. getChapterList(novelId)

Returns all chapters for a specific novel.

Parameters:

  • novelId (string) - The unique ID of the novel (from getPopularNovels or searchNovels)

Returns: Array of chapter objects:

javascript
{
    chapterId: "unique-chapter-id",  // Required
    title: "Chapter 1: Beginning",   // Required
    index: 0                         // Required (chapter order, starts at 0)
}

Example:

javascript
getChapterList: function(novelId) {
    var url = this.baseUrl + "/novel/" + novelId + "/chapters";
    var response = fetch(url);
    
    if (!response || response.error) {
        return [];
    }
    
    var chapters = [];
    var html = response.text;
    var index = 0;
    
    var pattern = /<a href="\/chapter\/([^"]+)">([^<]+)<\/a>/g;
    var match;
    
    while ((match = pattern.exec(html)) !== null) {
        chapters.push({
            chapterId: match[1],
            title: match[2],
            index: index
        });
        index++;
    }
    
    return chapters;
}

4. getChapterContent(chapterId)

Returns the HTML content of a specific chapter.

Parameters:

  • chapterId (string) - The unique ID of the chapter (from getChapterList)

Returns: HTML string with the chapter content (wrapped in <p> tags)

Example:

javascript
getChapterContent: function(chapterId) {
    var url = this.baseUrl + "/chapter/" + chapterId;
    var response = fetch(url);
    
    if (!response || response.error) {
        return "<p>Error loading chapter</p>";
    }
    
    var html = response.text;
    
    // Extract content between markers
    var startMarker = '<div class="chapter-content">';
    var endMarker = '</div>';
    
    var startIdx = html.indexOf(startMarker);
    var endIdx = html.indexOf(endMarker, startIdx);
    
    if (startIdx === -1 || endIdx === -1) {
        return "<p>Could not extract chapter content</p>";
    }
    
    var content = html.substring(startIdx, endIdx);
    
    // Clean up HTML
    content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
    content = content.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
    
    return content;
}

Using the fetch Function

Ficnest provides a custom fetch function that works differently from browser fetch:

Correct Usage:

javascript
var response = fetch(url);

// Always check for errors first
if (!response || response.error) {
    log("Error: " + (response ? response.error : "Network error"));
    return [];
}

// Check HTTP status
if (!response.ok) {
    log("HTTP Error: " + response.status);
    return [];
}

// Get the content as a string (NOT a function call!)
var html = response.text;  // Direct property access

Don't Do This:

javascript
// WRONG - This is browser syntax, not for Ficnest
var html = response.text();  // This will cause errors!

// WRONG - Using await/async (not supported)
var response = await fetch(url);

// WRONG - Using .json() method
var data = response.json();

Response Object Structure:

javascript
{
    ok: true,           // Boolean: true if status 200-299
    status: 200,        // HTTP status code
    text: "...",        // Response body as string
    error: null         // Error message if request failed
}

// On error:
{
    error: "Error message"
}

Helper Functions & Best Practices

Use Regular Expressions for Parsing

JavaScript regex is powerful for extracting data from HTML:

javascript
// Find all matches
var pattern = /<a href="\/novel\/([^"]+)">([^<]+)<\/a>/g;
var match;

while ((match = pattern.exec(html)) !== null) {
    var novelId = match[1];
    var title = match[2];
    // Use the data...
}

// Find first match
var singleMatch = /<div class="author">([^<]+)<\/div>/.exec(html);
if (singleMatch) {
    var author = singleMatch[1];
}

Clean HTML Entities

Create a helper function to decode HTML entities:

javascript
decodeHTML: function(text) {
    return text
        .replace(/&amp;/g, "&")
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&nbsp;/g, " ");
}

Extract Text Between Markers

javascript
extractBetween: function(text, start, end) {
    var startIndex = text.indexOf(start);
    if (startIndex === -1) return "";
    
    var textAfterStart = text.substring(startIndex + start.length);
    var endIndex = textAfterStart.indexOf(end);
    
    if (endIndex === -1) return textAfterStart;
    return textAfterStart.substring(0, endIndex);
}

Always Log for Debugging

Use the log() function to help debug issues:

javascript
log("Fetching page " + page);
log("Found " + novels.length + " novels");
log("Error: " + response.error);

Optional: Adding Filters

If your source supports filtering (by genre, status, etc.), implement getFilters():

javascript
getFilters: function() {
    return {
        genre: {
            type: "select",
            label: "Genre",
            options: [
                { value: "", label: "All" },
                { value: "fantasy", label: "Fantasy" },
                { value: "romance", label: "Romance" },
                { value: "action", label: "Action" }
            ],
            default: ""
        },
        status: {
            type: "select",
            label: "Status",
            options: [
                { value: "", label: "All" },
                { value: "ongoing", label: "Ongoing" },
                { value: "completed", label: "Completed" }
            ],
            default: ""
        }
    };
}

Then modify getPopularNovels to accept filters:

javascript
getPopularNovels: function(page, filters) {
    var url = this.baseUrl + "/novels?page=" + page;
    
    if (filters && filters.genre) {
        url += "&genre=" + filters.genre;
    }
    
    if (filters && filters.status) {
        url += "&status=" + filters.status;
    }
    
    // ... rest of the code
}

Common Mistakes to Avoid

1. Using const and let

Don't use:

javascript
const NovelSource = { ... };
let myVariable = "value";

Use instead:

javascript
var NovelSource = { ... };
var myVariable = "value";

Why? Ficnest uses JavaScriptCore which may not support modern ES6 syntax in all cases.


2. Forgetting Error Checks

Don't do:

javascript
var response = fetch(url);
var html = response.text;  // Crashes if fetch failed!

Always check:

javascript
var response = fetch(url);

if (!response || response.error) {
    log("Fetch failed: " + (response ? response.error : "Unknown"));
    return [];
}

var html = response.text;

3. Using Wrong Response Format

Don't use:

javascript
var text = response.text();  // ❌ Not a function!
var json = response.json();  // ❌ Method doesn't exist

Use:

javascript
var text = response.text;    // Direct property access

4. Returning Wrong Data Structure

Missing required fields:

javascript
return [{
    title: "My Novel"
    // Missing novelId!
}];

Include all required fields:

javascript
return [{
    novelId: "novel-123",
    title: "My Novel",
    coverUrl: "",
    author: "",
    description: ""
}];

5. Not Handling URL Encoding

Don't do:

javascript
var url = this.baseUrl + "/search?q=" + query;  // Breaks with spaces!

Use encodeURIComponent:

javascript
var url = this.baseUrl + "/search?q=" + encodeURIComponent(query);

Debugging Tips

1. Enable Developer Mode

Always enable Developer Mode in Settings before testing your source. This gives you access to:

  • Real-time console logs
  • Source testing tools
  • Error reporting with stack traces
  • Network request inspection

2. Use Descriptive Log Messages

Add informative log() statements throughout your code:

javascript
log("Starting getPopularNovels for page " + page);
log("Fetching URL: " + url);
log("Response status: " + response.status);
log("Found " + novels.length + " novels");
log("Novel IDs: " + novels.map(n => n.novelId).join(", "));

These messages will appear in the Developer Mode console, helping you track exactly what's happening.

3. Check the Console

Look for your log() messages in the Developer Mode console view. The console shows:

  • Your custom log messages
  • Automatic logs for network requests
  • JavaScript errors with line numbers
  • Function execution times

4. Test Each Function Separately

Start with getPopularNovels(1) first, then move to other functions. Developer Mode may provide testing tools to call individual functions directly.

5. Verify HTML Structure

Use your browser to view the target website's HTML and understand its structure:

  1. Open the website in your browser (Chrome, Firefox, Safari, Edge, etc.)
  2. Right-click → Inspect Element (or press F12 / ⌘+Option+I)
  3. Look for patterns in the HTML
  4. Test your regex patterns

6. Use Simple Patterns First

Start with basic regex patterns and make them more complex as needed:

javascript
// Start simple
var pattern = /<a href="([^"]+)">([^<]+)<\/a>/g;

// Add specificity if needed
var pattern = /<a href="\/novel\/([^"]+)" class="novel-link">([^<]+)<\/a>/g;

7. Export Logs for Analysis

Use Developer Mode's log export feature to:

  • Save logs to a file
  • Share logs when asking for help
  • Review logs offline to identify patterns

Installing Your Source

Option 1: Install from File

For quick testing, you can install a source directly from a JavaScript file:

  1. Save your JavaScript file (e.g., mywebsite.js)
  2. In Ficnest, go to Sources tab
  3. Tap the "+" button
  4. Select "Import from File"
  5. Choose your JavaScript file

To share your source with others, host it on a web server and create a repository manifest. Users can then add your repository URL to automatically install and update your source.

Step 1: Create Your JavaScript Source File

Save your source plugin as a .js file (e.g., mywebsite.js).

Step 2: Create a manifest.json File

Create a manifest file that describes your source repository:

json
{
    "version": "1.0.0",
    "sources": [
        {
            "id": "mywebsite",
            "name": "My Novel Website",
            "baseUrl": "https://example.com",
            "version": "1.0.0",
            "language": "en",
            "contentRating": "all",
            "scriptUrl": "https://yourserver.com/path/to/mywebsite.js",
            "description": "Read novels from My Website",
            "author": "Your Name",
            "iconURL": "https://example.com/icon.png"
        }
    ]
}

Manifest Field Descriptions:

  • version (string, required): The version of your repository manifest
  • sources (array, required): Array of source objects

Source Object Fields:

  • id (string, required): Unique identifier for your source (lowercase, no spaces)
  • name (string, required): Display name shown in the app
  • baseUrl (string, required): Base URL of the website
  • version (string, required): Version of your source plugin
  • language (string, required): Language code (e.g., "en", "es", "fr", "ja", "zh")
  • contentRating (string, required): Content rating ("all", "teen", "mature", or "adult")
  • scriptUrl (string, required): Direct URL to your JavaScript file (must be publicly accessible)
  • description (string, optional): Brief description of the source
  • author (string, optional): Your name or organization
  • iconURL (string, optional): URL to an icon image for the source

Step 3: Host Your Files

Upload both files to a web server or hosting service:

https://yourserver.com/
├── manifest.json
└── sources/
    └── mywebsite.js

Important: The scriptUrl in your manifest must point to the actual JavaScript file URL that Ficnest can download. Make sure:

  • The URL is publicly accessible (not behind authentication)
  • The URL returns the raw JavaScript file (not an HTML page)
  • CORS headers allow downloads if needed

Step 4: Share Your Repository URL

Users can now add your repository by entering the manifest URL in Ficnest:

https://yourserver.com/manifest.json

Multiple Sources in One Repository

You can include multiple sources in a single repository:

json
{
    "version": "1.0.0",
    "sources": [
        {
            "id": "website1",
            "name": "Website One",
            "baseUrl": "https://website1.com",
            "version": "1.0.0",
            "language": "en",
            "contentRating": "all",
            "scriptUrl": "https://yourserver.com/sources/website1.js"
        },
        {
            "id": "website2",
            "name": "Website Two",
            "baseUrl": "https://website2.com",
            "version": "1.2.0",
            "language": "ja",
            "contentRating": "teen",
            "scriptUrl": "https://yourserver.com/sources/website2.js"
        }
    ]
}

Updating Your Sources

When you update your source:

  1. Increment the version field in both your JavaScript file's SourceMetadata and in manifest.json
  2. Upload the new files to your server
  3. Users with your repository installed will be notified of the update

Complete Example

Here's a complete, working example for a fictional website:

javascript
// ================================
// SOURCE METADATA
// ================================
var SourceMetadata = {
    id: "novelsite",
    name: "Novel Site",
    baseURL: "https://novelsite.com",
    iconURL: "https://novelsite.com/icon.png",
    version: "1.0.0",
    language: "en",
    contentRating: "all",
    description: "Read free novels online",
    author: "Community",
    capabilities: {
        popular: true,
        search: true,
        latest: false,
        filters: false
    }
};

// ================================
// NOVEL SOURCE
// ================================
var NovelSource = {
    baseUrl: "https://novelsite.com",
    
    // Helper: Decode HTML entities
    decodeHTML: function(text) {
        return text
            .replace(/&amp;/g, "&")
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">")
            .replace(/&quot;/g, '"')
            .replace(/&#39;/g, "'")
            .replace(/&nbsp;/g, " ");
    },
    
    // Get popular novels
    getPopularNovels: function(page) {
        log("Fetching popular novels, page: " + page);
        
        var url = this.baseUrl + "/popular?page=" + page;
        var response = fetch(url);
        
        if (!response || response.error) {
            log("Error: " + (response ? response.error : "Network error"));
            return [];
        }
        
        if (!response.ok) {
            log("HTTP Error: " + response.status);
            return [];
        }
        
        var html = response.text;
        var novels = [];
        
        // Pattern: <a href="/novel/novel-slug"><img src="cover.jpg" alt="Title">
        var pattern = /<a href="\/novel\/([^"]+)"><img src="([^"]+)" alt="([^"]+)"/g;
        var match;
        
        while ((match = pattern.exec(html)) !== null) {
            var novelId = match[1];
            var coverUrl = match[2].startsWith("http") ? match[2] : this.baseUrl + match[2];
            var title = this.decodeHTML(match[3]);
            
            novels.push({
                novelId: novelId,
                title: title,
                coverUrl: coverUrl,
                author: "",
                description: ""
            });
        }
        
        log("Found " + novels.length + " novels");
        return novels;
    },
    
    // Search novels
    searchNovels: function(query, page) {
        log("Searching: " + query + ", page: " + page);
        
        var url = this.baseUrl + "/search?q=" + encodeURIComponent(query) + "&page=" + page;
        var response = fetch(url);
        
        if (!response || response.error || !response.ok) {
            return [];
        }
        
        var html = response.text;
        var novels = [];
        
        // Use same pattern as popular novels
        var pattern = /<a href="\/novel\/([^"]+)"><img src="([^"]+)" alt="([^"]+)"/g;
        var match;
        
        while ((match = pattern.exec(html)) !== null) {
            novels.push({
                novelId: match[1],
                title: this.decodeHTML(match[3]),
                coverUrl: match[2].startsWith("http") ? match[2] : this.baseUrl + match[2],
                author: "",
                description: ""
            });
        }
        
        log("Found " + novels.length + " results");
        return novels;
    },
    
    // Get chapter list
    getChapterList: function(novelId) {
        log("Getting chapters for: " + novelId);
        
        var url = this.baseUrl + "/novel/" + novelId;
        var response = fetch(url);
        
        if (!response || response.error || !response.ok) {
            return [];
        }
        
        var html = response.text;
        var chapters = [];
        
        // Pattern: <a href="/novel/slug/chapter-1">Chapter 1: Title</a>
        var pattern = /<a href="\/novel\/[^\/]+\/(chapter-\d+)">([^<]+)<\/a>/g;
        var match;
        var index = 0;
        
        while ((match = pattern.exec(html)) !== null) {
            var chapterSlug = match[1];
            var title = this.decodeHTML(match[2]).trim();
            
            chapters.push({
                chapterId: novelId + "/" + chapterSlug,
                title: title,
                index: index
            });
            
            index++;
        }
        
        log("Found " + chapters.length + " chapters");
        return chapters;
    },
    
    // Get chapter content
    getChapterContent: function(chapterId) {
        log("Getting content for: " + chapterId);
        
        // Extract novelId and chapter from chapterId
        var parts = chapterId.split("/");
        if (parts.length !== 2) {
            log("Invalid chapter ID format");
            return "<p>Error: Invalid chapter ID</p>";
        }
        
        var novelId = parts[0];
        var chapterSlug = parts[1];
        
        var url = this.baseUrl + "/novel/" + novelId + "/" + chapterSlug;
        var response = fetch(url);
        
        if (!response || response.error || !response.ok) {
            return "<p>Error loading chapter</p>";
        }
        
        var html = response.text;
        
        // Extract content between markers
        var startMarker = '<div class="chapter-content">';
        var endMarker = '<div class="chapter-nav">';
        
        var startIdx = html.indexOf(startMarker);
        var endIdx = html.indexOf(endMarker, startIdx);
        
        if (startIdx === -1) {
            log("Could not find chapter content");
            return "<p>Error: Could not find chapter content</p>";
        }
        
        if (endIdx === -1) {
            endIdx = html.length;
        }
        
        var content = html.substring(startIdx, endIdx);
        
        // Clean up content
        content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
        content = content.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
        content = content.replace(/<a[^>]+class="[^"]*button[^"]*"[^>]*>[\s\S]*?<\/a>/gi, "");
        
        // Extract paragraphs
        var paragraphPattern = /<p[^>]*>([\s\S]*?)<\/p>/gi;
        var paragraphs = [];
        var match;
        
        while ((match = paragraphPattern.exec(content)) !== null) {
            var pContent = match[1]
                .replace(/<br\s*\/?>/gi, "\n")
                .replace(/<[^>]+>/g, "")
                .trim();
            
            pContent = this.decodeHTML(pContent);
            
            if (pContent.length > 0) {
                paragraphs.push("<p>" + pContent + "</p>");
            }
        }
        
        var finalContent = paragraphs.join("\n");
        
        if (finalContent.length < 50) {
            log("Warning: Content seems too short");
            return "<p>Error: Chapter content too short or parsing failed</p>";
        }
        
        log("Extracted " + finalContent.length + " characters");
        return finalContent;
    }
};

Testing Your Source

Testing is crucial to ensure your source works correctly. Here's a comprehensive testing workflow:

Local Testing Workflow

Step 1: Create Your Source File

Save your JavaScript source as mywebsite.js (or any name you prefer).

Step 2: Import into Ficnest

  1. Open Ficnest
  2. Go to the Sources tab
  3. Tap the "+" button
  4. Select "Import from File"
  5. Choose your .js file

Step 3: Enable the Source

After importing, make sure your source is enabled in the Sources list.

Step 4: Test Each Function

Test your source functions in this order:

  1. Test Popular Novels

    • Navigate to Browse or the source's page
    • Try loading page 1
    • Verify novels appear with correct titles and covers
    • Try loading page 2 to test pagination
    • Check console logs for any errors
  2. Test Search

    • Use the search feature
    • Try different search terms
    • Verify results match your search query
    • Test search pagination if applicable
  3. Test Chapter List

    • Open a novel from the browse/search results
    • Verify all chapters load correctly
    • Check that chapter titles are properly formatted
    • Ensure chapters are in the correct order
  4. Test Chapter Content

    • Open a chapter
    • Verify the content displays correctly
    • Check that paragraphs are properly formatted
    • Ensure no ads, navigation buttons, or scripts appear
    • Test multiple chapters to ensure consistency

Step 5: Check Console Logs

Throughout testing, monitor the console for:

  • Your log() statements
  • Any error messages
  • Unexpected behavior or warnings

Debugging Console

To view console logs while testing, enable Developer Mode in Ficnest:

Enable Developer Mode:

  1. Open Ficnest Settings
  2. Scroll to "Developer" section
  3. Toggle "Developer Mode" on
  4. Return to the app

View Debug Logs:

Once Developer Mode is enabled, you can access the console:

  • Console View: Tap "Open Console" in Developer settings to see real-time log output
  • Source Testing: Test individual source functions directly from the Sources detail view
  • Log Export: Export logs as text files for troubleshooting

All your log() statements from your source plugin will appear in the console, making it easy to troubleshoot without needing Xcode or external tools.

What Gets Logged:

When Developer Mode is active, you'll see:

  • Your custom log() messages from your source code
  • HTTP request URLs being fetched
  • Response status codes and errors
  • Function execution timing
  • Parse results (number of items found, etc.)
  • JavaScript errors with details

Common Issues During Testing

Problem: No novels appear

  • Check if your regex pattern matches the website's HTML structure
  • Verify the fetch request succeeded (response.ok should be true)
  • Look for log messages indicating what went wrong
  • Visit the website in your browser and inspect the HTML structure (F12 or right-click → Inspect)

Problem: Covers don't load

  • Ensure coverUrl is a complete URL (starts with http:// or https://)
  • Check if relative URLs need to be converted to absolute URLs
  • Example: cover.jpghttps://example.com/cover.jpg

Problem: Chapter content is empty or malformed

  • Verify your content extraction markers are correct
  • Check if the website uses different HTML structure than expected
  • Ensure you're removing all unwanted elements (scripts, ads, buttons)
  • Test with multiple chapters to ensure consistency

Problem: Search returns wrong results

  • Verify your search URL is correctly formatted
  • Check if you're using encodeURIComponent() for the query
  • Test with different search terms
  • Compare with the actual website's search

Testing with Different Websites

When developing a new source:

  1. Open the target website in your browser
  2. Right-click and select "Inspect Element" (or press F12 / ⌘+Option+I)
  3. Find patterns in the HTML:
    • Look for novel listing elements
    • Identify chapter links
    • Locate content containers
  4. Test your regex patterns:
    • Use online regex testers like regex101.com
    • Copy sample HTML from the website
    • Test your patterns before implementing

Version Control for Testing

When making changes to your source:

  1. Increment the version number in SourceMetadata
  2. Document what changed in comments
  3. Re-import the updated file into Ficnest
  4. Re-test all functions to ensure nothing broke

Example:

javascript
var SourceMetadata = {
    id: "mywebsite",
    name: "My Website",
    version: "1.0.1",  // Increment this when you make changes
    // ...
};

Testing Checklist

Before sharing your source, verify:

  • [ ] Enabled Developer Mode in Settings
  • [ ] Popular novels load on page 1
  • [ ] Pagination works (page 2, 3, etc.)
  • [ ] Search returns relevant results
  • [ ] Novel details page loads correctly
  • [ ] Chapter list appears in correct order
  • [ ] Chapter content displays properly formatted text
  • [ ] No ads, scripts, or navigation buttons in content
  • [ ] Covers load correctly (if available)
  • [ ] Error handling works (test with invalid URLs/IDs)
  • [ ] Console shows helpful log messages (check Developer Mode console)
  • [ ] Source works consistently across multiple novels
  • [ ] All required fields are populated (novelId, title, chapterId, etc.)
  • [ ] Tested with different novels from the website
  • [ ] No JavaScript errors appear in debug console

Browser-Based Pre-Testing (Optional)

You can test your regex patterns and logic before importing into Ficnest:

  1. Open the target website in your browser
  2. Open Developer Console (F12 or ⌘+Option+I)
  3. Paste and test your extraction code:
javascript
// Test in browser console
var html = document.body.innerHTML;

// Test your regex pattern
var pattern = /<a href="\/novel\/([^"]+)">([^<]+)<\/a>/g;
var match;
var count = 0;

while ((match = pattern.exec(html)) !== null) {
    console.log("Novel ID:", match[1], "Title:", match[2]);
    count++;
}

console.log("Total found:", count);

This helps you validate your patterns quickly without importing into the app.


Additional Resources

Getting Help

If you run into issues:

  1. Review your console logs for error messages in Developer Mode
  2. Test your regex patterns in an online regex tester like regex101.com
  3. Compare your source with the complete example in this guide
  4. Use browser developer tools (F12) to inspect the target website's HTML structure

Summary Checklist

Before submitting your plugin, make sure:

  • [ ] Used var instead of const or let
  • [ ] Defined both SourceMetadata and NovelSource
  • [ ] Implemented all 4 required functions
  • [ ] Added error checking for all fetch() calls
  • [ ] Used response.text (not response.text())
  • [ ] Returned correct object structures
  • [ ] Added log() statements for debugging
  • [ ] Tested with the actual website
  • [ ] Cleaned up HTML properly
  • [ ] Handled URL encoding for search queries
  • [ ] Used proper chapterId format (include novelId)
  • [ ] Created a manifest.json file with correct scriptUrl
  • [ ] Hosted both files on a publicly accessible server
  • [ ] Tested installation from repository URL

Happy coding!

If you need help or have questions, refer to the existing examples in the project or check the debugging guide.