Building An eBook Store on Frappe: Part 1, Building The Storefront

Let's build an online store for listing and selling eBooks using Frappe Framework, Bulma & Razorpay

 · 8 min read

The Goal

In this three part blog series, we are going to build an online store for selling eBooks! Here is how it is going to work: users will be able to browse a list of ebooks, view the details of a particular ebook and purchase it. On a successful purchase, the user will receive the ebook file (PDF, ePUB) via Email.

In this blog, we will build the website using portal pages, Website Generator, and Bulma. In the next part, we will implement the payments and eBook delivery mechanism.

The Stack

We will build the store as a custom Frappe app. For styling, I am going with Bulma this time (miss you tailwind 😁). I really like Bulma's color schemes and minimal components.

For payments, we will use the Frappe Payments app.

Setting up our Frappe App


This tutorial assumes you know the basics of Frappe Framework. You will need a working Frappe bench installation on your local machine to follow along. You can learn more about setting up bench here.

Create a new app

Let's create a new custom Frappe app using the below command (run inside bench directory):

bench new-app star_ebook_store

Create a new site

We can create a site named abc.localhost:

bench new-site abc.localhost

Install the app on our site

Let's install our custom app on our newly created site:

bench --site abc.localhost install-app star_ebook_store

Make sure you enable developer mode and we are ready to go!

Everything Starts with a DocType

We will start by creating two DocTypes:

  1. eBook: Master DocType for storing all things related to an eBook. This DocType will have the following fields:

    • Name - "Set by User" (in naming rule)
    • Cover Image - Attach Image
    • Asset File - Attach (the deliverable asset file)
    • Format - Select (PDF, ePUB)
    • Price - Currency
    • Author - Link (Author DocType)
    • Description - Markdown Editor
    • Table Of Contents - Markdown Editor
    • Route - Data (for web view)
    • Is Published - Check (for web view)
  2. Author: For storing information about book authors, will be linked to an eBook. For simplicity, we will assume exactly 1 author per eBook. This DocType will have just 2 fields:

    • Full Name - Data
    • Bio - Markdown Editor

It became a breeze to build and layout the DocTypes using the new Form Builder contributed by my friend Sharique Ansari recently. Kudos!

For the eBook DocType, make sure you check the "Has Web View" checkbox in the Web View section of the DocType form:

Also, make sure you set the Is Published field to is_published. This will be used to track whether a page is published or not by Frappe Framework. Unpublished pages are not accessible to users.

We will also set the Image Field in Form Settings for eBook to cover_image. The final result is shown below:

Website Generator

When you check the "Has Web View" checkbox for a DocType, two notable things will happen:

  1. The eBook controller class will inherit from the WebsiteGenerator class instead of the default Document class.
  2. Two html template (jinja) files will be generated under the ebook/templates directory: ebook.html and ebook_row.html.

The ebook.html file is rendered when the user visits the route defined in the route field of the document. The ebook_row.html file is rendered when you visit the route you set in the DocType form's Web View section.

Since, we are not going to use the ebook_row.html to display the list, I have kept the Route empty in the eBook DocType form. We will display the list of ebooks in a separate web page.

Let's focus on ebook.html for now. Try to visit /store/hello (the route we set in the "Never Split The Difference" ebook) above. A page will be rendered for you by default:

The content of the ebook.html file is as follows:

{% extends "templates/web.html" %}

{% block page_content %}
<h1>{{ title }}</h1>
{% endblock %}

<!-- this is a sample default web page template -->

As you can see, this template extends another template file at "templates/web.html" which is located in the Frappe app's templates directory. This is the reason why we get the header, footer and the default bootstrap styling.

Building the eBook Details Page

Let's get rid of all the sample code and add in our own code:

{% extends "templates/store/base.html" %}

{% block body %}
<div class="columns">
    <div class="column is-one-third">
        <img class="cover-image px-6" src="{{ doc.cover_image }}" alt="Book Cover Image">
    <div class="column">
            <h1 class="title is-1">{{ }}</h1>
        <div class="mt-3">
            <span class="tag is-link is-normal">{{ doc.format }}</span>
{% endblock %}

Here, I am extending from "templates/store/base.html". Let's create this file and add in our base template code in the next section.

Setting up Templates & Bulma

I will create a new directory in our app root named templates/store and add a file named base.html in it. Add in the following contents:

<!DOCTYPE html>
<html lang="en">
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="">
        <link rel="stylesheet" href="/assets/star_ebook_store/css/store.css">
        <title>Star Ebook Store</title>

    <body class="antialiased">
        {%- block body -%} {%- endblock -%}

I have created this base template so that we can reuse this while building other pages of our store. The below link tag will import Bulma CSS:

<link rel="stylesheet" href="">

I have also added a CSS file to write our custom CSS rules (inside public/css/store.css, content here)

Now, visit the /store/hello route in your browser and you should see a Bulma styled page:

Some Extra Context

The eBook document is available in the jinja context of ebook.html template via the doc variable. But, we need some extra data for displaying information about the Author. We can do this by writing a method named get_context in our eBook controller class (doctype/ebook/

import frappe

from import WebsiteGenerator

class eBook(WebsiteGenerator):
    def get_context(self, context): = frappe.db.get_value(
            "Author",, ["full_name as name", "bio"], as_dict=True

The context is passed as a positional argument and we can attach extra stuff to it. This data will be available in the jinja template ebook.html.

I am attaching the author dictionary fetched from database using frappe.get_value. Now, in ebook.html I can add the following line to render the author's name:

<h2 class="subtitle is-4">by {{ }}</h2>

You can find the full templates in the source code repository here. Let's also render the markdown fields in our ebook page:

<section class="container">
    <div class="tabs">
            <li class="is-active"><a>Description</a></li>
            <li><a>About the author</a></li>

        <p>{{ frappe.utils.md_to_html(doc.description) }}</p>

        <p>{{ frappe.utils.md_to_html(doc.toc) }}</p>

        <p>{{ frappe.utils.md_to_html( }}</p>

I am using the tabs component from Bulma to display three tabs. We will get them working later, but for now, observe how we are using the frappe.utils.md_to_html function to render markdown to html. Many such utilities are available in the frappe namespace in these jinja templates. You can refresh the page to see the updated content:

Notice that I have also added some breadcrumbs and a Download button which links to the ebook.asset_file for now. In the next blog, we will replace this with a working Buy Now button.

Building the Store Landing Page

Now that we have completed the ebook details page, let's move to build our store's landing page. This page will have a hero section and a grid of all ebooks in our database.

We will start by creating a new file named store.html inside our app's www directory. This page will automatically be served at /store on our site. If you want to learn more about portal pages using www directory, visit this link for the docs.

Here are the contents of this page:

{% extends "templates/store/base.html" %}

{% block body %}
<section class="hero is-large is-link">
    <div class="hero-body">
        <p class="title">
            Star eBook Store
        <p class="subtitle">
            Awesome collection of ebooks from all over the internet.
{% endblock %}

Since, we already have the base template in place, we can extend it easily. This way our templates are DRY. Now, visit <your-site-url>/store and you should see a page identical to the one shown below:

It is pretty simple as of now, just the hero section. But we want to render a list of ebooks on this page. How do we insert dynamic content on this page? That's what we are going to do next!

Passing context

If you want to pass some data (context) that you want to use in your web page template (every file in www is a jinja template), we have to pass that context from a Python file. In the case of a DocType web view, we wrote a get_context method in the controller class (eBook above).

But in case of portal pages in www directory, we have to create a separate Python file to pass the context. The Python file should be of the same name as the HTML file and placed in the same directory:

We just need to create a function named get_context (similar to what we did for website generator based web page) which receives the context we can add properties to.

In the above case, I am doing a get_all to get a list of all published eBooks with the required fields and adding it to the context. Now, we should be able to write the following code in our store.html:

<main class="container is-max-widescreen">
    <h3 class="title mt-5">Full Collection</h3>

    <div class="columns my-6">
        {% for ebook in ebooks %}
        <div class="column is-one-fifth">
            <a href="{{ ebook.route }}">
                <div class="card">
                    <div class="card-image">
                        <figure class="card-cover-image">
                            <img src="{{ ebook.cover_image }}" alt="Cover image">
                    <div class="card-content">
                        <div class="media">
                            <div class="media-content">
                                <p class="title is-4">{{ }}</p>
                                <p class="subtitle is-6">by {{ ebook.author_name }}</p>

                        <div class="content">
        {% endfor %}

We are looping over the ebooks list and rendering a card (Bulma component) for each of them. Go ahead, refresh the store page:

Voilà! We have a dynamic page now.

Dynamically Generating the Route

If you recall, we added a route field to the eBook DocType. If you leave it empty, the framework will use the name of the document to generate a route automatically for you. For example, "creative-confidence", "never-split-the-difference" etc. So, the web views will be available at "your-site-url/creative-confidence" and so on. But I want it to be served at "your-site-url/store/book-name". Better than having the ebook pages in the root path in my opinion.

We can do this very easily by overriding the validate lifecycle hook in our eBook controller class:

from import cleanup_page_name

class eBook(WebsiteGenerator):
    def validate(self):
        if not self.route:
            self.route = f"store/{cleanup_page_name(}"

... and done. You can now click on the ebook cards to visit the ebook details page. We have developed a beautiful store in record time!


  • Source Code for this tutorial is on GitHub here
  • Portal Pages in Frappe Framework: docs
  • Web Views / Website Generator: docs


You can find the source code of the completed app here. I hope this post was helpful, let me know in the comments below!

We will continue from here in the next part, where we will add functionality for online payments and also auto-delivery of the purchased eBook to the user via Email. See you in that one!

Md Hussain Nagaria

Hussain Nagaria is a Product Engineer at Frappe. He is very passionate about teaching technical content. He also does product design, writing and marketing. So, you can say, he is a company of one at Frappe!

No comments yet.

Add a comment
Ctrl+Enter to add comment