This is a demo project that guides the creation of a Nuxt-based E-commerce project using Alokai (formerly Vue Storefront) integrated with the ODOO ERP.
- Overview
- Main Pages
- Dynamic Routing System
- Key Features
- Content Management (CMS)
- Tech Stack
- Project Structure
- ODOO Integration
- Setup and Installation
- Cache Warming
The project uses the Alokai SDK to create a modern and high-performance e-commerce experience, directly connected to ODOO as the backend ERP.
The core of the Alokai e-commerce used in this project includes the following pages:
- Home (/) - Homepage with featured products
- Category (/category-page) - Dynamic category pages listing by category slug
- Product (/product-page) - Dynamic product pages listing by product slug
- Cart (/cart) - Shopping cart
- Checkout (/checkout) - Checkout process
- Wishlist (/wishlist) - Wishlist page
- My Account (/my-account) - Customer area with:
- Addresses (/billing-details or /shipping-details)
- Personal information (/personal-data)
The project uses an advanced dynamic route generation system that:
-
Automatic fetch from ODOO: During build time, the system queries ODOO via GraphQL to fetch all slugs for products, categories, and website pages
-
Static route creation: Automatically generates static pages for each product and category found
-
Performance optimization: Uses Redis cache to optimize lookups and applies automatic SWR (Stale-While-Revalidate) configurations to each generated route
-
Real-time route resolution: Identifies at runtime (see more here) new odoo routes and maps to correct type by URL slug and save it on Redis as following:
- `product.template` product - `product.public.category` category - `alokai.website.page` website page
Header suggestions and the /search page (layers/search) search Odoo by
default. To search with Algolia instead, set:
NUXT_PUBLIC_ALGOLIA_ENABLED=1
NUXT_PUBLIC_ALGOLIA_APPLICATION_ID=...
NUXT_PUBLIC_ALGOLIA_SEARCH_API_KEY=... # a search-only key: browsers see it
NUXT_PUBLIC_ALGOLIA_INDEX_NAME=...Algolia then decides which products match and in what order; Odoo still
loads them, so prices, stock, filters, sorting and pagination are Odoo's.
The index needs one record per product template with its Odoo id in id
(or as the objectID). If any of the four is missing, or Algolia does not
answer, search falls back to Odoo. These are runtime variables: no rebuild
is needed to switch.
- Route Cache: Optimizes resources by reducing unnecessary API calls
- Page Cache: HTML caching to improve user experience and performance
- Storage Layers: Multi-driver storage system (Redis) for:
- Cart cache
- Stock cache
- Slug/route cache
- General data cache
- Adyen Payment: Secure and fast payment processing using the Adyen platform
- Dynamic Variants: Size, color, and material selection with automatic URL update
- Image Gallery: Gallery system with thumbnails and main image
- Related Products:
- Frequently bought together
- Alternative products
- Recently viewed
- Wishlist: Add/remove products from wishlist
- Stock Management: Real-time stock availability check
- Filter Navigation: Sidebar with attribute filters
- Pagination: Responsive pagination system
- Breadcrumbs: Hierarchical navigation
- Responsive View: Automatic adaptation for mobile and desktop
Merchants build and edit storefront pages themselves, without touching code.
The editor lives at /cms — e.g. http://localhost:3000/cms. Sign in with
an Odoo account that belongs to the CMS Editor group (Odoo: Settings →
Users). Everyone else gets redirected to /cms/login, and the API refuses
anonymous writes, so the route is not security by obscurity.
- Create a page, give it a name and a URL, and publish it at that URL.
- Drag blocks onto the page, reorder them, and edit their text and images in a side panel. The canvas renders the real storefront components, so what they arrange is what visitors get.
- Change a page's name or URL afterwards, from the page name in the editor header.
- Edit meta title, description and share image per page, in each language.
- Roll back: every publish keeps a revision, and an older one can be restored.
Fifteen blocks are available: Card Grid, Category Grid, Contact Form, FAQ, Feature List, Hero Banner, Image + Text, Info Table, Key Numbers, Newsletter, Page Banner, Products — Automatic, Products — Hand-picked, Selling Points, Text Section.
| Content pages | Any URL the merchant creates, e.g. /about. Rendered by layers/cms/custom-pages/cms-page.vue |
| Homepage | Editable like any other page, but cannot be deleted and its URL is fixed. Behind NUXT_PUBLIC_CMS_HOMEPAGE=1; with the flag off, / renders the hand-written markup instead |
| Category and product pages | Not editable as a whole — their structure is business logic. Code declares named slots with <CmsRegion name="..." /> and merchants fill those. Today: category-after and product-after |
In Odoo, on alokai.website.page (Website → Alokai → CMS Pages), not in this
repo. Blocks are stored as JSON, and publishing writes an immutable revision.
SEO, including the generated structured data, is stored per page — the homepage
included.
Publishing purges the storefront's Redis cache for that URL, so a published change is live immediately.
layers/cms-blocks/— this storefront's block catalogue. A block is a schema inschemas.tsplus a line inregistry.tsnaming the component that renders it; nothing else in the CMS changes.layers/cms/— the engine: rendering, the/api/cmsroutes and the Odoo store, and the shared block machinery (validation, seeding, migrations). It knows no block by name; it reads the catalogue through the#cms-blocksalias.layers/cms-editor/— the editor, a separate layer so it stays out of the shopper bundle.docs/CMS_ODOO_SPEC.mdis the authoritative spec;docs/CMS_ARCHITECTURE.mdcovers the design decisions anddocs/CMS_POC.mdis the operating guide, including known gaps.
- Framework: Nuxt 4
- UI Components: Alokai Storefront UI
- Backend: ODOO ERP
- Styling: Tailwind CSS 3
- Build Tool: Vite
- Cache: Redis (configurable via storage drivers)
- Images: Nuxt Image with custom ODOO provider
├── layers/ # Nuxt layers for modular architecture
│ ├── cart-redis/ # Cart management with Redis caching
│ │ ├── composables/ # Cart-related composables
│ │ └── server/ # Cart server utilities and plugins
│ ├── category/ # Category pages and functionality
│ │ ├── composables/ # Category-related composables
│ │ ├── custom-pages/ # Category page templates (dynamic page routing by categories slug)
│ │ └── utils/ # Category utilities│
│ ├── core/ # Core application components
│ │ ├── components/ # Shared UI components
│ │ │ ├── header/ # Header components (Mobile/Desktop)
│ │ │ └── ui/ # Base UI components
│ │ └── composables/ # Core composables│
│ ├── product/ # Product pages and functionality
│ │ ├── composables/ # Product-related composables
│ │ └── custom-pages/ # Product page templates (dynamic page routing by products slug)
│ └── search/ # Search: Odoo, or Algolia when configured
│ ├── composables/ # Search composables
│ └── pages/ # Search page
├── modules/ # Custom Nuxt modules
│ └── routes-generator/ # Automatic route generator
├── server/ # Server API routes and middleware
├── api/ # API endpoints
├── mutations/ # GraphQL mutations
└── queries/ # GraphQL queries
The project is configured to integrate with the ODOO ERP using GraphQL APIs. Understanding ODOO's e-commerce data model is crucial for effective development.
Product Template
- Represents the main product with shared characteristics
- Contains general information like name, description, category, and base price
- Serves as a container for multiple variants
- Used for SEO, URLs, and product pages
Product Variant
- Represents a specific variation of a product template
- Created by combining different attribute values (size, color, material)
- Each variant has its own stock, price, and SKU
- Used for cart operations and inventory management
// Example: A T-shirt Product Template can have variants like:
// - Red T-shirt Size M
// - Blue T-shirt Size L
// - Green T-shirt Size SAttributes (product.attributeValues)
- Define variant dimensions like "Color", "Size", "Material"
- Control how variants are created and displayed
- Configure filter visibility and display types
Attribute Values (product.attribute.value)
- Specific values like "Red", "Large", "Cotton"
- Combined to create unique product variants
- Can have price extras and HTML colors for UI display
interface ProductTemplate {
id: number
name: string
slug: string
description: string
image: string
firstVariant: ProductVariant // Default variant for display
attributeValues: AttributeValue[] // Available variant options
breadcrumb: BreadcrumbItem[]
// SEO fields
metaTitle: string
metaDescription: string
jsonLd: object
}interface ProductVariant {
id: number
name: string
combinationInfoVariant: PriceInfo // Variant-specific pricing
stock: number // Real-time stock from Redis
// Variant-specific attributes
attributeValueIds: number[]
}interface CombinationInfoVariant {
list_price: number // Regular/original price
price: number // Current/special price
has_discounted_price: boolean // If true indicates a special price is applied
}
// Price calculation priority:
// 1. combinationInfoVariant.price (variant special price)
// 2. combinationInfoVariant.list_price (variant regular price)- Real-time stock stored in Redis cache
- Stock quantity updated according cart operations. More informations
here - Stock checks prevent overselling
// Load template first for general info and SEO
const { loadProductTemplate, productTemplate } = useProductTemplate(slug)
// Load specific variant based on URL attributes
const { loadProductVariant, productVariant } = useProductVariant(fullPath)// URL structure: /product-slug?Color=1&Size=2&Material=3
// Each query parameter maps to an attribute value ID
const params = {
combinationId: [1, 2, 3], // Attribute value IDs
productTemplateId: productTemplate.id
}-
On category page load:
A list of Products is fetched.
Each Product contains attributeValues, which point to Attributes.
-
To render filters:
Unique Attributes are extracted from all attributeValues.
Only Attributes with filterVisibility enabled are shown.
Filters can be displayed as swatches, color pickers, checkboxes, etc., based on displayType.
// Extracted from products in the current category
const filters = [
{
id: 12,
name: 'Color',
displayType: 'color',
options: [
{ id: 1, name: 'Red', htmlColor: '#f00' },
{ id: 2, name: 'Blue', htmlColor: '#00f' }
]
},
{
id: 13,
name: 'Size',
displayType: 'text',
options: [
{ id: 3, name: 'S' },
{ id: 4, name: 'M' },
{ id: 5, name: 'L' }
]
}
]
/**Notes:
The same AttributeValue IDs are used in:
Filters on the category page (for product discovery)
Variant selection on the product detail page (for combination resolution)
This allows consistent behavior between discovery and selection.
**/Product Categories (product.public.category)
- Hierarchical structure with parent/child relationships
- SEO-optimized with slugs and meta information
- Support breadcrumb navigation and filtering
Cart Operations (add/remove/update qty)
- Always use Product Variant ID for cart operations
- Variants ensure accurate pricing and stock validation
- Cart items stored as
OrderLineobjects
Order Processing
- Orders contain multiple order lines
- Each line references a specific product variant
- Supports discounts, coupons, and gift cards
- Product Templates:
/product-slug(base product page) - Product Variants:
/product-slug?Color=1&Size=2(specific variant) - Categories:
/category-slug(category listing) - Search: Integrated with ODOO native search, or Algolia
- Redis Caching: Stock, prices, and route resolution
- Static Generation: Pre-built product and category pages
- SWR Strategy: Stale-while-revalidate for dynamic content
- Lazy Loading: Component-level loading strategies
# Install dependencies (pnpm, via corepack: `corepack enable`)
pnpm install
# Configure environment variables
cp .env.example .env
# Run in development mode
pnpm dev
# Build for production
pnpm buildProduct, category, and listing pages are server-rendered and their Odoo queries are cached in Redis (SWR, NUXT_SWR_CACHE_TIME, default 3600s). After a deploy or a cache flush, the first visitor to each page pays the cold cost of an Odoo round-trip. The scripts/warm-cache.mjs script pre-populates that cache so visitors always hit the warm path.
It crawls the storefront over HTTP — no browser and no dependencies — requesting every product page, every category/listing page (including all pagination pages), and the static pages, for both desktop and mobile. Everything is URL-driven, so each request runs the same query and warms the same cache key a real visitor would read. It does not touch search and adds no filter/sort/variant params.
# NUXT_PUBLIC_MIDDLEWARE_URL (the storefront site URL) must be set —
# load it from your env, e.g.:
source .env && node scripts/warm-cache.mjsRun it after each deploy or cache flush, and on a schedule shorter than NUXT_SWR_CACHE_TIME to keep the cache warm — for example via cron:
*/50 * * * * cd /path/to/app && source .env && node scripts/warm-cache.mjs >> /var/log/warm-cache.log 2>&1To change the concurrency or page size, edit the constants at the top of the script.
For detailed technical implementation, take a look at our main documentation at docs.alokai.com.
