Skip to content

Discover Section

Ok, let's start writing real code. Even if your website doesn't have a homepage, you can always recreate one from search results. You can even create different sections by sorting the results in different ways (e.g. Latest, Most Liked).

getDiscoverSections

typescript
// Required
getDiscoverSections(): Promise<DiscoverSection[]>;

The goal of this function is to provide the characteristics of the different Homepage sections. By clicking the red arrow in the top right of a section, you can view all of its results in fullscreen.

getDiscoverSections: Overview

By default, the template extension shows 3 types of discover sections:

  1. featured -- Wide display with plenty of space for the image and title to breathe.
  2. prominentCarousel -- A carousel with a thumbnail, a title below it, and an optional subtitle at the bottom. The prominent carousel is the only discover section with a brighter background than the rest of the homepage.
  3. simpleCarousel -- Mostly the same as prominentCarousel, differing only in that it has a normal background.

However, there are 5 types in total:

  1. chapterUpdates -- A 4-row grid of items, each composed of a small thumbnail on the left and, on the right, a title with an optional subtitle (often the latest chapter number) and the release date, using the Date interface. An important detail about chapterUpdates is that you need to provide the mangaId and chapterId for each displayed item. This section is designed to display the latest chapter releases.
  2. genres -- The most unique type: a horizontal list of genres showing just the title, with the search results for the selected genre displayed as a simpleCarousel. you must provide the query details to do the search but we will talk more about this in the advanced search page.

getDiscoverSections: Implementation

As the signature implies, this function can be async and returns a list of DiscoverSection. By default, the template extension defines each section as a const variable, for example:

typescript
const discover_section_template1: DiscoverSection = {
  id: "discover-section-template1",
  title: "Discover Section Template 1",
  subtitle: "This is a template",
  type: DiscoverSectionType.featured,
};

For your extension, you can edit the name of the variable, the id, and the title. You can change or remove the subtitle, and you can change the type.

Once you've defined all your sections, you can simply return them as a list:

typescript
return [discover_section_template1, discover_section_template2, discover_section_template3];

Sidenote on IDs

You will often notice that Paperback likes to handle things precisely, and to do so, it relies on IDs -- for sections, for manga (as mangaId, even when managing a light novel), chapters (as chapterId), search filters, and more.

Every time you need to provide an ID, it must be a combination of alphanumeric characters (a-zA-Z0-9) and the symbols ._-@()[]%?#+=/&:. If you don't follow this rule, you will get an error:

Could not convert JSValue: Invalid ID `[...]`. IDs must be alphanumeric or only contain `._-@()[]%?#+=/&:` symbols

Since titles, chapter names, or other user-facing strings can contain characters outside this allowed set (spaces, accents, punctuation like !, ', *, ~, etc.), you can't always use them as-is for an ID. To safely turn arbitrary text into a valid ID, you can percent-encode it and manually escape the handful of characters that encodeURIComponent leaves untouched but that are still disallowed:

typescript
const textToId = (text: string): string =>
  encodeURIComponent(text).replace(
    /[!'()*~]/g,
    (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
  );

encodeURIComponent already percent-encodes most unsafe characters (spaces, accents, symbols, etc.), but it deliberately leaves !, ', (, ), and * unescaped, since they're valid in URIs. The .replace() call catches these remaining characters and converts them to their percent-encoded hexadecimal form as well, ensuring the resulting string only ever contains characters from the allowed set. This makes textToId a safe, reusable way to derive valid IDs from any text, such as manga titles or chapter names, without worrying about illegal characters causing runtime errors.

If you'd rather just validate an ID than construct one, here's a simple regex you can use to check whether a string already conforms to the allowed character set:

typescript
const isValidId = (id: string): boolean => /^[a-zA-Z0-9._\-@()[\]%?#+=/&:]+$/.test(id);

This returns true only if every character in the string belongs to the allowed set (alphanumerics plus ._-@()[]%?#+=/&:). It's useful as a quick sanity check, for example in tests or assertions, before you rely on an ID elsewhere in your extension.

getDiscoverSectionItems

This is where we will start putting our custom network requests (like web scraping or REST API calls).

typescript
// Required
getDiscoverSectionItems(section: DiscoverSection, metadata?: Metadata): Promise<PagedResults<DiscoverSectionItem>>

getDiscoverSectionItems: Overview

This function will be called for each section that you defined above, and will return all the items for that specific section.

As input, we get section, which is one of the sections we returned in getDiscoverSections. It will mostly be used as section.id in a switch statement.

We also get metadata as an optional argument. This is useful when you want this function to be called multiple times for the same section: for example, if you have a Latest section and the homepage only displays 20 items, but you want an "infinite scroll" experience, you can let Paperback know, after returning your first 20 items, that more elements can be loaded. We'll cover this in detail in Implementation.

This function needs to return Promise<PagedResults<DiscoverSectionItem>>, meaning it can be async and must return a packaged version of DiscoverSectionItem.

Here is a sample of what you need to return:

typescript
{
    items: DiscoverSectionItem[];
    metadata?: Metadata;
}

Here is the full definition of DiscoverSectionItem:

typescript
type InfoItem = {
  symbol: string;
  text: string;
};

interface FeaturedCarouselItem {
  type: "featuredCarouselItem";
  mangaId: string;
  imageUrl: string;
  title: string;
  supertitle?: string;
  summary?: string;
  infoItems?: [InfoItem] | [InfoItem, InfoItem];
  metadata?: Metadata;
  contentRating?: ContentRating;
}

interface SimpleCarouselItem {
  type: "simpleCarouselItem";
  mangaId: string;
  imageUrl: string;
  title: string;
  subtitle?: string;
  metadata?: Metadata;
  contentRating?: ContentRating;
}

interface ProminentCarouselItem {
  type: "prominentCarouselItem";
  mangaId: string;
  imageUrl: string;
  title: string;
  subtitle?: string;
  metadata?: Metadata;
  contentRating?: ContentRating;
}

interface ChapterUpdatesCarouselItem {
  type: "chapterUpdatesCarouselItem";
  mangaId: string;
  chapterId: string;
  imageUrl: string;
  title: string;
  subtitle?: string;
  publishDate?: Date;
  metadata?: Metadata;
  contentRating?: ContentRating;
}

interface GenresCarouselItem {
  type: "genresCarouselItem";
  searchQuery: SearchQuery<Metadata>;
  name: string;
  metadata?: Metadata;
  contentRating?: ContentRating;
}

type DiscoverSectionItem =
  | FeaturedCarouselItem
  | SimpleCarouselItem
  | ProminentCarouselItem
  | ChapterUpdatesCarouselItem
  | GenresCarouselItem;

getDiscoverSectionItems: Implementation

To get the items, we need to make network requests. For this, we use the network.ts file.

In this file, you'll see that some content already exists. We'll talk about it in more detail on the CloudFlare bypass page. But if your website doesn't filter traffic at all, you can remove all the existing code.

Classic network setup

If you can access your website's content by making simple requests, like a REST API call or by scraping a webpage, you'll need a simple base function to do a request.

For example, to get the HTML of a page:

typescript
async function fetchText(url: string): Promise<string> {
  const [, buffer] = await Application.scheduleRequest({ url, method: "GET" });
  return Application.arrayBufferToUTF8String(buffer);
}

Whatever you do with the result, simple parsing or streamed buffer parsing, please use Application.scheduleRequest, since it will let you do more complex things later, like handling interceptors or managing dynamic cookies.

Application.scheduleRequest can accept a lot of parameters for more complex requests:

typescript
export type Request = {
  url: string;
  method: string;
  headers?: Record<string, string>;
  body?: ArrayBuffer | object | string;
  cookies?: Record<string, string>;
};

You can also create other functions for very light parsing, like JSON or XML decoding:

typescript
async function fetchJSON<T>(url: string): Promise<T> {
  return JSON.parse(await fetchText(url)) as T;
}

In this case, the function requests a type, because if your JSON response always has the same shape, you should model it in model.ts. For example, if my fictional endpoint /api/home returns:

json
{
    "status": "ok",
    "latest": {
        "length": 20,
        "items": [
            {
                "name": "manga 1",
                "image": "/static/image/manga1.jpg",
                "url": "/manga/1243"
            },
            ...
        ]
    }
}

You can model everything as:

typescript
interface HomeItem {
  name: string;
  image: string;
  url: string;
}

interface HomeSection {
  length: number;
  items: HomeItem[];
}

interface HomeResponse {
  status: string;
  latest: HomeSection;
}

Then, in getDiscoverSectionItems, you can call fetchJSON with this type and map the raw response into DiscoverSectionItems:

typescript
async function getDiscoverSectionItems(
  section: DiscoverSection,
  metadata?: Metadata,
): Promise<PagedResults<DiscoverSectionItem>> {
  switch (section.id) {
    case "latest": {
      const response = await fetchJSON<HomeResponse>("https://example.com/api/home");

      const items: DiscoverSectionItem[] = response.latest.items.map((item) => ({
        type: "simpleCarouselItem",
        mangaId: item.url.split("/").pop(), // extraact the slug from the url
        imageUrl: item.image,
        title: item.name,
      }));

      return { items };
    }
    default:
      throw new Error(`Unsupported section: ${section.id}`);
  }
}

If your endpoint supports pagination and you want an infinite-scroll experience, use metadata to track your position (e.g. an offset or page number), and return it alongside your items so Paperback knows how to fetch the next batch:

typescript
return {
  items,
  metadata: { page: (metadata?.page ?? 0) + 1 },
};

Paperback will pass this metadata back into getDiscoverSectionItems the next time it needs more items for that section, letting you resume from where you left off.

by default the template extension use the type : metadata: number | undefined but you can replace it for Metadata a custom type that you defined in model.ts for exemple here :

typescript
/** Pagination cursor for Paperback's PagedResults. */
export interface Metadata extends JSONObject {
  page: number;
}