Setting Up Saleor with PKB-theme
This guide provides detailed instructions for developers to install, configure, and integrate Saleor - a modern, GraphQL-first headless commerce platform - with your PKB-theme knowledge base or blog.
Introduction to Saleor
Saleor is a headless e-commerce platform built with Python, Django, and GraphQL. Unlike traditional e-commerce systems, Saleor separates the backend (data and business logic) from the frontend (user interface), allowing for more flexible and customizable implementations.
Key advantages:
- API-first architecture with GraphQL
- Flexible data model
- Customizable without core modifications
- Built with modern technologies
- Performance-focused design
Prerequisites
This guide assumes you have:
- Intermediate to advanced development experience
- Familiarity with Docker and containerization
- Basic understanding of GraphQL
- Command line interface experience
- Python 3.8+ installed
- Docker and Docker Compose installed
- Node.js 16+ and npm/yarn installed
- Git version control
Installation Options
Option 1: Docker Installation (Recommended)
Clone the Saleor repository:
git clone https://github.com/saleor/saleor.git cd saleor
Build and start the Docker containers:
docker-compose up -d
Populate the database with example data:
docker-compose exec api python manage.py populatedb
Access the Dashboard at
http://localhost:9000
and the GraphQL API playground athttp://localhost:8000/graphql/
Option 2: Manual Installation
Clone the Saleor repository:
git clone https://github.com/saleor/saleor.git cd saleor
Create and activate a virtual environment:
python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate
Install Python dependencies:
pip install -r requirements.txt
Set up environment variables:
cp .env.example .env
Edit the
.env
file with your database configurationCreate the database and run migrations:
python manage.py migrate
Create a superuser:
python manage.py createsuperuser
Load example data (optional):
python manage.py populatedb
Start the development server:
python manage.py runserver
Install and run the dashboard:
# In a separate terminal: git clone https://github.com/saleor/saleor-dashboard.git cd saleor-dashboard npm install npm start
Configuration and Core Setup
Step 1: Initial Configuration
- Access the Saleor Dashboard (default:
http://localhost:9000
) - Log in with your admin credentials
- Navigate to Configuration → Site Settings
- Configure:
- Company information
- Default currency and country
- Units of weight and measurement
- Default payment and shipping methods
Step 2: Product Catalog Setup
- Create product categories (Products → Categories)
- Create product types with appropriate attributes (Products → Product Types)
- Add products (Products → Products)
- Include descriptions, images, pricing
- Configure variants if applicable
- Set metadata for SEO
Step 3: API Configuration and Access
Create an App token for API access:
- Go to Apps → Create App
- Name your app (e.g., “PKB Integration”)
- Select permissions (at minimum: products read)
- Save the generated token
Test your API with a GraphQL query:
curl -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -X POST \ -d '{"query": "query { products(first: 5) { edges { node { name } } } }"}' \ http://localhost:8000/graphql/
Integrating with PKB-theme
Saleor’s headless architecture allows for several integration approaches with your PKB-theme website.
Method 1: JAMstack Integration
This approach uses JavaScript to fetch and display products from Saleor within your Hugo-based PKB-theme:
Create a JavaScript module to interact with Saleor’s GraphQL API:
// static/js/saleor-client.js class SaleorClient { constructor(apiUrl, token) { this.apiUrl = apiUrl; this.token = token; } async query(graphqlQuery, variables = {}) { const response = await fetch(this.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': this.token ? `Bearer ${this.token}` : '', }, body: JSON.stringify({ query: graphqlQuery, variables }), }); if (!response.ok) { throw new Error('Network response was not ok'); } return await response.json(); } async getFeaturedProducts(limit = 3) { const query = ` query FeaturedProducts($limit: Int!) { products(first: $limit, channel: "default-channel") { edges { node { id name description thumbnail { url } pricing { priceRange { start { gross { amount currency } } } } slug } } } } `; const result = await this.query(query, { limit }); return result.data.products.edges.map(edge => edge.node); } } window.SaleorClient = SaleorClient;
Create a Hugo shortcode to display products:
<!-- layouts/shortcodes/saleor-products.html --> <div class="saleor-products"> <div id="products-container" class="products-grid"> <p>Loading products...</p> </div> </div> <script> document.addEventListener('DOMContentLoaded', async function() { try { const client = new SaleorClient( '{{ .Site.Params.saleor.apiUrl | default "http://localhost:8000/graphql/" }}', '{{ .Site.Params.saleor.apiToken | default "" }}' ); const products = await client.getFeaturedProducts({{ .Get "limit" | default 3 }}); const container = document.getElementById('products-container'); if (products.length === 0) { container.innerHTML = '<p>No products found</p>'; return; } container.innerHTML = products.map(product => ` <div class="product-card"> <div class="product-image"> <img src="${product.thumbnail?.url || '/images/placeholder.jpg'}" alt="${product.name}"> </div> <div class="product-info"> <h3>${product.name}</h3> <p class="product-price">${product.pricing.priceRange.start.gross.amount} ${product.pricing.priceRange.start.gross.currency}</p> <a href="/product/${product.slug}" class="product-link">View Details</a> </div> </div> `).join(''); } catch (error) { console.error('Failed to load products:', error); document.getElementById('products-container').innerHTML = '<p>Failed to load products. Please try again later.</p>'; } }); </script>
Add configuration to your
config.toml
:[params.saleor] apiUrl = "https://your-saleor-instance.com/graphql/" apiToken = "your-api-token" storeUrl = "https://your-storefront.com"
Add CSS for the product displays:
/* assets/css/saleor-integration.css */ .saleor-products { margin: 2rem 0; } .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 2rem; } .product-card { border: 1px solid var(--color-border); border-radius: var(--border-radius); overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; } .product-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); } .product-image img { width: 100%; height: 200px; object-fit: cover; } .product-info { padding: 1rem; } .product-price { font-weight: bold; color: var(--color-primary); } .product-link { display: inline-block; padding: 0.5rem 1rem; background-color: var(--color-primary); color: white; text-decoration: none; border-radius: var(--border-radius); margin-top: 1rem; transition: background-color 0.2s ease; } .product-link:hover { background-color: var(--color-primary-variant); }
Use the shortcode in your markdown content:
## Shop Our Products Here are some featured items from our store: {{< saleor-products limit="4" >}}
Method 2: Full Product Pages with Hugo
For a deeper integration with product detail pages:
Create a data fetching script to run during Hugo build:
# scripts/fetch-saleor-products.py import json import os import requests SALEOR_API_URL = os.getenv('SALEOR_API_URL', 'http://localhost:8000/graphql/') SALEOR_API_TOKEN = os.getenv('SALEOR_API_TOKEN') def fetch_all_products(): headers = { 'Content-Type': 'application/json', } if SALEOR_API_TOKEN: headers['Authorization'] = f'Bearer {SALEOR_API_TOKEN}' query = """ query { products(first: 100, channel: "default-channel") { edges { node { id name description slug thumbnail { url } images { url alt } pricing { priceRange { start { gross { amount currency } } } } category { name slug } } } } } """ response = requests.post( SALEOR_API_URL, headers=headers, json={'query': query} ) if response.status_code != 200: print(f"Query failed with status code {response.status_code}") print(response.text) return [] data = response.json() return [edge['node'] for edge in data['data']['products']['edges']] def generate_product_files(products): for product in products: content = f"""--- title: "{product['name']}" date: {os.environ.get('HUGO_DATE', '2023-01-01T00:00:00Z')} draft: false product_id: "{product['id']}" price: "{product['pricing']['priceRange']['start']['gross']['amount']}" currency: "{product['pricing']['priceRange']['start']['gross']['currency']}" image: "{product['thumbnail']['url'] if product['thumbnail'] else ''}" category: "{product['category']['name'] if product['category'] else 'Uncategorized'}" layout: product --- {product['description']} """ slug = product['slug'] os.makedirs('content/products', exist_ok=True) with open(f'content/products/{slug}.md', 'w', encoding='utf-8') as f: f.write(content) # Also save the full data for reference with open('data/saleor_products.json', 'w', encoding='utf-8') as f: json.dump(products, f, indent=2) if __name__ == '__main__': products = fetch_all_products() generate_product_files(products) print(f"Generated {len(products)} product files")
Create a product layout template:
<!-- layouts/_default/product.html --> {{ define "main" }} <article class="product-page"> <div class="product-container"> <div class="product-image"> <img src="{{ .Params.image }}" alt="{{ .Title }}"> </div> <div class="product-details"> <h1>{{ .Title }}</h1> <div class="product-meta"> <div class="product-price">{{ .Params.price }} {{ .Params.currency }}</div> <div class="product-category">{{ .Params.category }}</div> </div> <div class="product-description"> {{ .Content }} </div> <div class="product-actions"> <button class="add-to-cart" data-product-id="{{ .Params.product_id }}" onclick="addToCart('{{ .Params.product_id }}')"> Add to Cart </button> </div> </div> </div> </article> <script> function addToCart(productId) { const saleorUrl = '{{ .Site.Params.saleor.storeUrl }}'; // In a real implementation, you'd either: // 1. Use Saleor's checkout API directly // 2. Redirect to your Storefront with this product in the cart // For simplicity, we'll redirect to the product page: window.location.href = `${saleorUrl}/products/${productId}`; } </script> {{ end }}
Add a build script to your package.json:
{ "scripts": { "fetch-products": "python scripts/fetch-saleor-products.py", "build": "npm run fetch-products && hugo" } }
Method 3: Full Storefront Integration
For a complete storefront solution:
- Set up Saleor’s Storefront API and React Storefront
- Create custom React components for product listings and details
- Use server-side rendering (SSR) or static site generation (SSG) to build product pages
- Integrate authentication between your PKB-theme and the storefront
- Style the storefront to match your PKB-theme’s design language
Advanced Integration: Checkout Process
To create a complete shopping experience:
Integrate with Saleor’s Checkout API:
// static/js/saleor-checkout.js class SaleorCheckout { constructor(apiUrl, token) { this.client = new SaleorClient(apiUrl, token); } async createCheckout(variantId, quantity) { const query = ` mutation { checkoutCreate( input: { lines: [{ quantity: ${quantity}, variantId: "${variantId}" }], channel: "default-channel" } ) { checkout { id token totalPrice { gross { amount currency } } checkoutUrl } errors { field message } } } `; const result = await this.client.query(query); return result.data.checkoutCreate; } } window.SaleorCheckout = SaleorCheckout;
Use the Checkout API in your product pages:
<script> async function addToCart(variantId) { try { const checkout = new SaleorCheckout( '{{ .Site.Params.saleor.apiUrl }}', '{{ .Site.Params.saleor.apiToken }}' ); const quantity = parseInt(document.getElementById('quantity').value); const result = await checkout.createCheckout(variantId, quantity); if (result.errors && result.errors.length > 0) { alert(`Error: ${result.errors[0].message}`); return; } // Redirect to Saleor's checkout page window.location.href = result.checkout.checkoutUrl; } catch (error) { console.error('Checkout error:', error); alert('There was an error adding this item to your cart.'); } } </script>
Security Considerations
API Security:
- Store API tokens securely
- Use environment variables for sensitive credentials
- Implement proper CORS settings in Saleor
- Consider using a proxy for client-side API calls
User Data Protection:
- Ensure GDPR compliance
- Implement proper SSL/TLS encryption
- Use HTTPS for all communications
- Follow PCI DSS guidelines for payment data
Maintaining Updates:
- Keep Saleor updated to the latest version
- Monitor for security advisories
- Use dependency scanning tools
Performance Optimization
API Optimization:
- Request only needed fields in GraphQL queries
- Implement pagination for large datasets
- Use query batching where appropriate
- Cache API responses where possible
Image Optimization:
- Use Saleor’s thumbnail feature
- Implement responsive images
- Consider a CDN for media assets
Frontend Performance:
- Minimize JavaScript payload
- Use lazy loading for images and components
- Implement code splitting
- Consider pre-rendering product pages
Conclusion
Integrating Saleor with your PKB-theme offers a powerful combination of content and commerce. The API-first architecture allows for flexible implementations, whether you want a simple product showcase or a complete shopping experience.
Remember that Saleor is a developer-focused platform. While the integration requires more technical knowledge than traditional e-commerce systems, it provides greater flexibility and customization potential. For complex implementations, consider working with a developer experienced in both Saleor and web development.
For further assistance, refer to the Saleor documentation or join the Saleor community.