Skip to content

Grav E-commerce — SimpleCart, Payment Gateways and Products

DodaTech Updated 2026-06-27 7 min read

In this tutorial, you'll learn Grav e-commerce with the SimpleCart plugin — setting up products, configuring payment gateways, building a shopping cart, managing checkout flow, and selling digital and physical goods.

What You'll Learn

  • Installing and configuring SimpleCart
  • Creating products with frontmatter
  • Payment gateway integration (PayPal, Stripe)
  • Shopping cart and checkout flow
  • Digital downloads and physical shipping
  • Order management and notifications
  • Cart templates and customization

Why It Matters

In WordPress, e-commerce requires WooCommerce — a heavy plugin with database tables. In Grav, the SimpleCart plugin provides lightweight e-commerce functionality using Grav's file-based architecture. Products are pages with special frontmatter. Orders are stored as files. No database needed. This makes SimpleCart ideal for small shops, digital product sales, and developers who want a Git-friendly e-commerce setup.

Real-World Use

A developer sells Grav themes and plugins. Each product is a page with a "Buy" button. When a customer purchases, SimpleCart processes the payment through Stripe and emails a download link. The order is saved as a JSON file. The developer updates product prices and descriptions by editing Markdown files — no admin panel navigation needed.

Learning Path

flowchart LR
    A["Web Services"] --> B["E-commerce with Grav
← You are here"]:::current B --> C["Caching Deep Dive"] C --> D["Performance Optimization"] D --> E["Security"] E --> F["Git Workflow"] F --> G["CLI Tools"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Installing SimpleCart

bin/gpm install simplecart

Configuration

user/config/plugins/simplecart.yaml:

enabled: true
currency: USD
currency_symbol: $
payment:
    stripe:
        enabled: true
        publishable_key: 'pk_test_your_publishable_key'
        secret_key: 'sk_test_your_secret_key'
    paypal:
        enabled: false
        client_id: 'your-paypal-client-id'
        secret: 'your-paypal-secret'
        mode: sandbox  # or live

shipping:
    methods:
        free:
            label: Free Shipping
            price: 0
        standard:
            label: Standard Shipping
            price: 5.99
        express:
            label: Express Shipping
            price: 14.99

tax:
    enabled: true
    rate: 0.10  # 10%

notifications:
    admin_email: admin@example.com
    customer_email_subject: 'Your order confirmation'

Creating Products

Products are pages with simplecart_product: true in frontmatter:

user/pages/04.shop/01.grav-theme/default.md:

---
title: Grav Documentation Theme
simplecart_product: true
price: 49.99
compare_at: 79.99
sku: GRAV-THEME-001
type: digital
file: /downloads/grav-theme-v1.0.zip
images:
    - /images/products/theme-preview.jpg
    - /images/products/theme-dashboard.jpg
categories:
    - themes
    - documentation
tags:
    - grav
    - theme
    - responsive
shipping:
    weight: 0
    requires_shipping: false
---

Product Types

Digital products:

---
type: digital
file: /downloads/ebook.pdf
---

Physical products:

---
type: physical
shipping:
    weight: 1.5
    dimensions:
        length: 10
        width: 8
        height: 2
    requires_shipping: true
---

Variable products (with options):

---
simplecart_product: true
price: 29.99
variants:
    - name: Basic
      price: 29.99
      sku: LICENSE-BASIC
    - name: Pro
      price: 99.99
      sku: LICENSE-PRO
    - name: Enterprise
      price: 299.99
      sku: LICENSE-ENT
---

Product Listing Template

user/themes/mytheme/templates/product.html.twig:

{% extends 'default.html.twig' %}

{% block content %}
<article class="product-detail">
    <div class="product-gallery">
        {% for image in page.media.images %}
        <img src="{{ image.cropResize(600, 600).url }}"
             alt="{{ page.title }}"
             loading="lazy" />
        {% endfor %}
    </div>

    <div class="product-info">
        <h1>{{ page.title }}</h1>

        {% if page.header.compare_at %}
        <p class="price-compare">
            <s>${{ page.header.compare_at }}</s>
        </p>
        {% endif %}

        <p class="price">${{ page.header.price }}</p>

        <div class="description">
            {{ page.content|raw }}
        </div>

        {% if page.header.simplecart_product %}
        <form class="simplecart-form"
              data-product="{{ page.route }}"
              data-price="{{ page.header.price }}"
              data-name="{{ page.title }}">

            {% if page.header.variants %}
            <div class="product-options">
                <label for="variant">Version</label>
                <select name="variant" id="variant">
                    {% for variant in page.header.variants %}
                    <option value="{{ variant.sku }}"
                            data-price="{{ variant.price }}">
                        {{ variant.name }} - ${{ variant.price }}
                    </option>
                    {% endfor %}
                </select>
            </div>
            {% endif %}

            <div class="quantity">
                <label for="quantity">Quantity</label>
                <input type="number" name="quantity" id="quantity"
                       value="1" min="1" max="99" />
            </div>

            <button type="submit" class="btn btn-primary add-to-cart">
                Add to Cart
            </button>
        </form>
        {% endif %}
    </div>
</article>
{% endblock %}

Cart Template

user/themes/mytheme/templates/cart.html.twig:

{% extends 'default.html.twig' %}

{% block content %}
<h1>Shopping Cart</h1>

<div id="simplecart-cart">
    {% if cart.items|length > 0 %}
    <table class="cart-table">
        <thead>
            <tr>
                <th>Product</th>
                <th>Price</th>
                <th>Quantity</th>
                <th>Total</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            {% for item in cart.items %}
            <tr>
                <td>{{ item.name }}</td>
                <td>${{ item.price }}</td>
                <td>
                    <input type="number" value="{{ item.quantity }}"
                           min="1" class="cart-quantity"
                           data-line="{{ loop.index0 }}" />
                </td>
                <td>${{ item.total }}</td>
                <td>
                    <button class="btn-remove" data-line="{{ loop.index0 }}">
                        Remove
                    </button>
                </td>
            </tr>
            {% endfor %}
        </tbody>
        <tfoot>
            <tr>
                <td colspan="3">Subtotal</td>
                <td>${{ cart.subtotal }}</td>
            </tr>
            {% if cart.shipping > 0 %}
            <tr>
                <td colspan="3">Shipping</td>
                <td>${{ cart.shipping }}</td>
            </tr>
            {% endif %}
            {% if cart.tax > 0 %}
            <tr>
                <td colspan="3">Tax</td>
                <td>${{ cart.tax }}</td>
            </tr>
            {% endif %}
            <tr class="total">
                <td colspan="3">Total</td>
                <td>${{ cart.total }}</td>
            </tr>
        </tfoot>
    </table>

    <div class="cart-actions">
        <a href="/checkout" class="btn btn-primary">
            Proceed to Checkout
        </a>
        <a href="/shop" class="btn btn-secondary">
            Continue Shopping
        </a>
    </div>
    {% else %}
    <div class="empty-cart">
        <p>Your cart is empty.</p>
        <a href="/shop" class="btn btn-primary">Browse Products</a>
    </div>
    {% endif %}
</div>
{% endblock %}

Checkout Template

user/themes/mytheme/templates/checkout.html.twig:

{% extends 'default.html.twig' %}

{% block content %}
<h1>Checkout</h1>

<form id="checkout-form" method="POST">
    <div class="checkout-grid">
        <div class="billing-section">
            <h2>Billing Details</h2>
            <div class="form-group">
                <label for="name">Full Name</label>
                <input type="text" name="name" id="name" required />
            </div>
            <div class="form-group">
                <label for="email">Email</label>
                <input type="email" name="email" id="email" required />
            </div>
            <div class="form-group">
                <label for="address">Address</label>
                <input type="text" name="address" id="address" required />
            </div>
            <div class="form-row">
                <div class="form-group">
                    <label for="city">City</label>
                    <input type="text" name="city" id="city" required />
                </div>
                <div class="form-group">
                    <label for="zip">ZIP Code</label>
                    <input type="text" name="zip" id="zip" required />
                </div>
            </div>
        </div>

        <div class="payment-section">
            <h2>Payment</h2>
            <div id="stripe-card-element"></div>
            <div id="card-errors" class="error-message"></div>

            <div class="order-summary">
                <h3>Order Summary</h3>
                <div id="checkout-summary"></div>
            </div>

            <button type="submit" class="btn btn-primary btn-block">
                Pay ${{ cart.total }}
            </button>
        </div>
    </div>
</form>
{% endblock %}

Learning Path

flowchart LR
    A["Web Services"] --> B["E-commerce with Grav
← You are here"]:::current B --> C["Caching Deep Dive"] C --> D["Performance Optimization"] D --> E["Security"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Common Mistakes

  1. Not setting simplecart_product: true: Products must have this flag in frontmatter. Without it, SimpleCart does not recognize the page as a product and the "Add to Cart" button does not work.

  2. Missing payment gateway configuration: SimpleCart requires at least one payment gateway (Stripe or PayPal) to be configured. Without it, checkout fails even if the cart is valid.

  3. Forgetting SKU for each product: SKU (Stock Keeping Unit) is required for inventory tracking and order management. Each product must have a unique SKU.

  4. Not handling digital file delivery: For digital products, configure the file delivery method (download link, email attachment) and ensure the file path is correct.

  5. Incorrect tax configuration: Tax rates vary by location. Set the correct tax rate for your jurisdiction and clearly show tax amounts in the cart.

Practice Questions

  1. What frontmatter flag marks a page as a SimpleCart product? Answer: simplecart_product: true. Without this, the page is treated as regular content and cannot be added to the cart.

  2. How do you create a product with multiple variants (e.g., different license types)? Answer: Add a variants list in frontmatter with each variant having name, price, and sku. The cart template should show a variant selector.

  3. What payment gateways does SimpleCart support? Answer: Stripe and PayPal. Both require API keys configured in simplecart.yaml. Additional gateways can be added through custom plugins.

  4. How do you handle digital product delivery? Answer: Set type: digital and file: /path/to/file in product frontmatter. SimpleCart sends a download link or provides direct download after payment.

  5. Challenge: Build a complete e-commerce site with 10 products (5 digital, 5 physical). Configure Stripe as the payment gateway. Create a shopping cart with quantity controls, a checkout form with address validation, order confirmation emails, digital download delivery, admin order management, and product filtering by category. Test the entire flow from browsing to payment to delivery.

FAQ

Can I sell subscription products with SimpleCart?

SimpleCart supports one-time purchases. For subscriptions, use Stripe's subscription API in a custom plugin or integrate with a dedicated membership service.

How are orders stored in Grav?

Orders are saved as JSON files in user/data/simplecart/orders/. Each order is a separate file with customer details, products, payment status, and timestamps.

Can I use SimpleCart with a non-USD currency?

Yes. Set the currency and currency_symbol in simplecart.yaml. Stripe supports multiple currencies — the currency code must match your Stripe account configuration.

How do I handle shipping calculations?

SimpleCart supports flat-rate shipping. Configure shipping methods in simplecart.yaml with labels and prices. For complex calculations, use a custom plugin.

Can I customize the checkout form?

Yes. Override checkout.html.twig in your theme. You can add custom fields, change the layout, or integrate with third-party services.

Mini Project

Goal: Build a complete e-commerce store with 10 products.

  1. Install and configure SimpleCart with Stripe
  2. Create 10 products (mix of digital and physical)
  3. Configure shipping methods (free, standard, Express)
  4. Set up tax calculation (10% VAT)
  5. Create product listing and detail templates
  6. Build a shopping cart with quantity controls
  7. Build a checkout form with Stripe payment
  8. Configure order confirmation emails
  9. Set up digital download delivery
  10. Test the complete purchase flow and verify order storage

What's Next

Now you have e-commerce capabilities. Next, learn Caching in depth:

Continue to Lesson 35: Caching Deep Dive — Cache types, cache warming, cache busting, and performance strategies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro