fix: implement correct base_url routing for shop frontend
Fix shop frontend links to work correctly across all three access methods:
- Custom domain (wizamart.shop)
- Subdomain (wizamart.localhost)
- Path-based (/vendor/wizamart/)
Changes:
- Update get_shop_context() to calculate base_url based on access method
- Update all shop templates to use {{ base_url }} for links
- Add base_url to shop-layout.js Alpine.js component
- Document multi-access routing in shop architecture docs
This ensures links work correctly regardless of how the shop is accessed,
solving broken navigation issues with path-based access.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -47,51 +47,44 @@ e-commerce experience unique to each vendor. Built with:
|
||||
|
||||
app/
|
||||
├── templates/shop/
|
||||
│ ├── base.html ← Base template (layout)
|
||||
│ ├── home.html ← Homepage / product grid
|
||||
│ ├── product-detail.html ← Single product page
|
||||
│ ├── base.html ← ✅ Base template (layout + theme)
|
||||
│ ├── home.html ← ✅ Homepage / featured products
|
||||
│ ├── products.html ← ✅ Product catalog with filters
|
||||
│ ├── product.html ← Product detail page
|
||||
│ ├── cart.html ← Shopping cart
|
||||
│ ├── checkout.html ← Checkout flow
|
||||
│ ├── search.html ← Search results
|
||||
│ ├── category.html ← Category browse
|
||||
│ ├── about.html ← About the shop
|
||||
│ ├── contact.html ← Contact form
|
||||
│ └── partials/ ← Reusable components
|
||||
│ ├── product-card.html ← Product display card
|
||||
│ ├── cart-item.html ← Cart item row
|
||||
│ ├── search-modal.html ← Search overlay
|
||||
│ └── filters.html ← Product filters
|
||||
│ ├── account/ ← Customer account pages
|
||||
│ │ ├── login.html
|
||||
│ │ ├── register.html
|
||||
│ │ ├── dashboard.html
|
||||
│ │ ├── orders.html
|
||||
│ │ ├── profile.html
|
||||
│ │ └── addresses.html
|
||||
│ └── errors/ ← Error pages
|
||||
│ ├── 400.html
|
||||
│ ├── 404.html
|
||||
│ └── 500.html
|
||||
│
|
||||
├── static/shop/
|
||||
│ ├── css/
|
||||
│ │ ├── shop.css ← Shop-specific styles
|
||||
│ │ └── themes/ ← Optional theme stylesheets
|
||||
│ │ ├── modern.css
|
||||
│ │ ├── minimal.css
|
||||
│ │ └── elegant.css
|
||||
│ │ └── shop.css ← ✅ Shop-specific styles (IMPLEMENTED)
|
||||
│ ├── js/
|
||||
│ │ ├── shop-layout.js ← Base shop functionality
|
||||
│ │ ├── home.js ← Homepage logic
|
||||
│ │ ├── product-detail.js ← Product page logic
|
||||
│ │ ├── cart.js ← Cart management
|
||||
│ │ ├── checkout.js ← Checkout flow
|
||||
│ │ ├── search.js ← Search functionality
|
||||
│ │ └── filters.js ← Product filtering
|
||||
│ │ └── shop-layout.js ← ✅ Base shop functionality (IMPLEMENTED)
|
||||
│ └── img/
|
||||
│ ├── placeholder-product.png
|
||||
│ └── empty-cart.svg
|
||||
│ └── (placeholder images)
|
||||
│
|
||||
├── static/shared/ ← Shared across all areas
|
||||
├── static/shared/ ← ✅ Shared across all areas (IMPLEMENTED)
|
||||
│ ├── js/
|
||||
│ │ ├── log-config.js ← Logging setup
|
||||
│ │ ├── icons.js ← Icon registry
|
||||
│ │ ├── utils.js ← Utility functions
|
||||
│ │ └── api-client.js ← API wrapper
|
||||
│ │ ├── log-config.js ← ✅ Logging setup
|
||||
│ │ ├── icons.js ← ✅ Icon registry
|
||||
│ │ ├── utils.js ← ✅ Utility functions
|
||||
│ │ └── api-client.js ← ✅ API wrapper
|
||||
│ └── css/
|
||||
│ └── base.css ← Global styles
|
||||
│ └── (shared styles if needed)
|
||||
│
|
||||
└── api/v1/shop/
|
||||
└── pages.py ← Route handlers
|
||||
└── routes/
|
||||
└── shop_pages.py ← ✅ Route handlers (IMPLEMENTED)
|
||||
|
||||
|
||||
🏗️ ARCHITECTURE LAYERS
|
||||
@@ -113,32 +106,96 @@ Layer 6: Database
|
||||
Layer 1: ROUTES (FastAPI)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Vendor Detection + Template Rendering
|
||||
Location: app/api/v1/shop/pages.py
|
||||
Location: app/routes/shop_pages.py
|
||||
|
||||
Example:
|
||||
@router.get("/")
|
||||
async def shop_home(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
vendor = request.state.vendor # From middleware
|
||||
theme = request.state.theme # From middleware
|
||||
|
||||
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get("/products", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_products_page(request: Request):
|
||||
"""
|
||||
Render shop homepage / product catalog.
|
||||
Vendor and theme are auto-injected by middleware.
|
||||
"""
|
||||
return templates.TemplateResponse(
|
||||
"shop/home.html",
|
||||
{
|
||||
"request": request,
|
||||
"vendor": vendor,
|
||||
"theme": theme,
|
||||
}
|
||||
"shop/products.html",
|
||||
get_shop_context(request) # Helper function
|
||||
)
|
||||
|
||||
Helper Function:
|
||||
def get_shop_context(request: Request, **extra_context) -> dict:
|
||||
"""Build template context with vendor/theme from middleware"""
|
||||
vendor = getattr(request.state, 'vendor', None)
|
||||
theme = getattr(request.state, 'theme', None)
|
||||
clean_path = getattr(request.state, 'clean_path', request.url.path)
|
||||
vendor_context = getattr(request.state, 'vendor_context', None)
|
||||
|
||||
# Get detection method (domain, subdomain, or path)
|
||||
access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown'
|
||||
|
||||
# Calculate base URL for links
|
||||
# - Domain/subdomain: base_url = "/"
|
||||
# - Path-based: base_url = "/vendor/{vendor_code}/"
|
||||
base_url = "/"
|
||||
if access_method == "path" and vendor:
|
||||
full_prefix = vendor_context.get('full_prefix', '/vendor/')
|
||||
base_url = f"{full_prefix}{vendor.subdomain}/"
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"vendor": vendor,
|
||||
"theme": theme,
|
||||
"clean_path": clean_path,
|
||||
"access_method": access_method,
|
||||
"base_url": base_url, # ⭐ Used for all links in templates
|
||||
**extra_context
|
||||
}
|
||||
|
||||
Responsibilities:
|
||||
✅ Access vendor from middleware
|
||||
✅ Access theme from middleware
|
||||
✅ Render template
|
||||
❌ NO database queries (data loaded client-side)
|
||||
❌ NO business logic
|
||||
✅ Access vendor from middleware (request.state.vendor)
|
||||
✅ Access theme from middleware (request.state.theme)
|
||||
✅ Calculate base_url for routing-aware links
|
||||
✅ Render template with context
|
||||
❌ NO database queries (data loaded client-side via API)
|
||||
❌ NO business logic (handled by API endpoints)
|
||||
|
||||
|
||||
⭐ MULTI-ACCESS ROUTING (Domain, Subdomain, Path-Based)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
The shop frontend supports THREE access methods:
|
||||
|
||||
1. **Custom Domain** (Production)
|
||||
URL: https://customdomain.com/products
|
||||
- Vendor has their own domain
|
||||
- base_url = "/"
|
||||
- Links: /products, /about, /contact
|
||||
|
||||
2. **Subdomain** (Production)
|
||||
URL: https://wizamart.letzshop.com/products
|
||||
- Vendor uses platform subdomain
|
||||
- base_url = "/"
|
||||
- Links: /products, /about, /contact
|
||||
|
||||
3. **Path-Based** (Development/Testing)
|
||||
URL: http://localhost:8000/vendor/wizamart/products
|
||||
- Vendor accessed via path prefix
|
||||
- base_url = "/vendor/wizamart/"
|
||||
- Links: /vendor/wizamart/products, /vendor/wizamart/about
|
||||
|
||||
⚠️ CRITICAL: All template links MUST use {{ base_url }} prefix
|
||||
|
||||
Example:
|
||||
❌ BAD: <a href="/products">Products</a>
|
||||
✅ GOOD: <a href="{{ base_url }}products">Products</a>
|
||||
|
||||
❌ BAD: <a href="/contact">Contact</a>
|
||||
✅ GOOD: <a href="{{ base_url }}contact">Contact</a>
|
||||
|
||||
How It Works:
|
||||
1. VendorContextMiddleware detects access method
|
||||
2. Sets request.state.vendor_context with detection_method
|
||||
3. get_shop_context() calculates base_url from detection_method
|
||||
4. Templates use {{ base_url }} for all internal links
|
||||
5. Links work correctly regardless of access method
|
||||
|
||||
|
||||
Layer 2: MIDDLEWARE
|
||||
@@ -147,12 +204,14 @@ Purpose: Vendor & Theme Identification
|
||||
|
||||
Two middleware components work together:
|
||||
|
||||
1. Vendor Context Middleware
|
||||
• Detects vendor from domain/subdomain
|
||||
1. Vendor Context Middleware (middleware/vendor_context.py)
|
||||
• Detects vendor from domain/subdomain/path
|
||||
• Sets request.state.vendor
|
||||
• Sets request.state.vendor_context (includes detection_method)
|
||||
• Sets request.state.clean_path (path without vendor prefix)
|
||||
• Returns 404 if vendor not found
|
||||
|
||||
2. Theme Context Middleware
|
||||
2. Theme Context Middleware (middleware/theme_context.py)
|
||||
• Loads theme for detected vendor
|
||||
• Sets request.state.theme
|
||||
• Falls back to default theme
|
||||
@@ -160,6 +219,11 @@ Two middleware components work together:
|
||||
Order matters:
|
||||
vendor_context_middleware → theme_context_middleware
|
||||
|
||||
Detection Methods:
|
||||
- custom_domain: Vendor has custom domain
|
||||
- subdomain: Vendor uses platform subdomain
|
||||
- path: Vendor accessed via /vendor/{code}/ or /vendors/{code}/
|
||||
|
||||
|
||||
Layer 3: TEMPLATES (Jinja2)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
@@ -199,38 +263,105 @@ Layer 4: JAVASCRIPT (Alpine.js)
|
||||
Purpose: Client-Side Interactivity + Cart + Search
|
||||
Location: app/static/shop/js/
|
||||
|
||||
Example (shop-layout.js):
|
||||
⚠️ CRITICAL: JavaScript Loading Order
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Scripts MUST load in this exact order (see base.html):
|
||||
|
||||
1. log-config.js ← Logging system (loads first)
|
||||
2. icons.js ← Icon registry
|
||||
3. shop-layout.js ← Alpine component (before Alpine!)
|
||||
4. utils.js ← Utility functions
|
||||
5. api-client.js ← API wrapper
|
||||
6. Alpine.js (deferred) ← Loads last
|
||||
7. Page-specific JS ← Optional page scripts
|
||||
|
||||
Why This Order Matters:
|
||||
• shop-layout.js defines shopLayoutData() BEFORE Alpine initializes
|
||||
• Alpine.js defers to ensure DOM is ready
|
||||
• Shared utilities available to all scripts
|
||||
• Icons and logging available immediately
|
||||
|
||||
Example from base.html:
|
||||
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shop/js/shop-layout.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
Alpine.js Component (shop-layout.js):
|
||||
──────────────────────────────────────────────────────────────────
|
||||
function shopLayoutData() {
|
||||
return {
|
||||
dark: false,
|
||||
// Theme state
|
||||
dark: localStorage.getItem('shop-theme') === 'dark',
|
||||
|
||||
// UI state
|
||||
mobileMenuOpen: false,
|
||||
searchOpen: false,
|
||||
cartCount: 0,
|
||||
|
||||
// Cart state
|
||||
cart: [],
|
||||
|
||||
|
||||
init() {
|
||||
shopLog.info('Shop layout initializing...');
|
||||
this.loadCart();
|
||||
this.loadThemePreference();
|
||||
window.addEventListener('cart-updated', () => {
|
||||
this.loadCart();
|
||||
});
|
||||
shopLog.info('Shop layout initialized');
|
||||
},
|
||||
|
||||
addToCart(product, quantity) {
|
||||
// Add to cart logic
|
||||
this.cart.push({ ...product, quantity });
|
||||
|
||||
addToCart(product, quantity = 1) {
|
||||
const existingIndex = this.cart.findIndex(
|
||||
item => item.id === product.id
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
this.cart[existingIndex].quantity += quantity;
|
||||
} else {
|
||||
this.cart.push({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
quantity: quantity
|
||||
});
|
||||
}
|
||||
|
||||
this.saveCart();
|
||||
this.showToast(`${product.name} added to cart`, 'success');
|
||||
},
|
||||
|
||||
|
||||
toggleTheme() {
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('shop-theme',
|
||||
localStorage.setItem('shop-theme',
|
||||
this.dark ? 'dark' : 'light');
|
||||
shopLog.debug('Theme toggled:', this.dark ? 'dark' : 'light');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Make globally available
|
||||
window.shopLayoutData = shopLayoutData;
|
||||
|
||||
Template Usage:
|
||||
──────────────────────────────────────────────────────────────────
|
||||
{# In base.html #}
|
||||
<html x-data="shopLayoutData()" x-bind:class="{ 'dark': dark }">
|
||||
|
||||
{# In page templates #}
|
||||
{% block alpine_data %}shopLayoutData(){% endblock %}
|
||||
|
||||
Responsibilities:
|
||||
✅ Load products from API
|
||||
✅ Manage cart in localStorage
|
||||
✅ Handle search and filters
|
||||
✅ Update DOM reactively
|
||||
✅ Theme toggling
|
||||
✅ Theme toggling (light/dark)
|
||||
✅ Mobile menu management
|
||||
✅ Toast notifications
|
||||
|
||||
|
||||
Layer 5: API (REST)
|
||||
@@ -584,10 +715,11 @@ Optimization Techniques:
|
||||
• Images load when visible
|
||||
• Infinite scroll for large catalogs
|
||||
|
||||
5. CDN Assets
|
||||
• Tailwind from CDN
|
||||
• Alpine.js from CDN
|
||||
• Vendor assets from CDN
|
||||
5. CDN Assets with Fallback
|
||||
• Tailwind CSS from CDN (fallback to local)
|
||||
• Alpine.js from CDN (fallback to local)
|
||||
• Works offline and in restricted networks
|
||||
• See: [CDN Fallback Strategy](../cdn-fallback-strategy.md)
|
||||
|
||||
|
||||
📊 PAGE-BY-PAGE BREAKDOWN
|
||||
|
||||
Reference in New Issue
Block a user