New documentation: - docs/frontend/shared/jinja-macros.md: Complete reference for all 94 macros - Alerts, Avatars, Badges, Buttons, Cards, Charts, Datepicker - Dropdowns, Forms, Headers, Modals, Pagination, Tables - Usage examples and parameter documentation - Complete page example showing all macros together Updated documentation: - docs/frontend/cdn-fallback-strategy.md: - Add Chart.js (v4.4.1) and Flatpickr (v4.6.13) sections - Document optional-libs.html loader macros - Update file structure and deployment checklist - docs/frontend/shared/ui-components.md: - Add tip box recommending Jinja macros for new development - Update related documentation links - mkdocs.yml: - Add Jinja Macros Library to navigation (top of Shared Components) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
15 KiB
CDN Fallback Strategy
Overview
All three frontends (Shop, Vendor, Admin) implement a robust CDN fallback strategy for critical CSS and JavaScript assets. This ensures the application works reliably in:
- ✅ Offline development environments
- ✅ Corporate networks with restricted CDN access
- ✅ CDN outages or performance issues
- ✅ Air-gapped deployments without internet access
Assets with Fallback
The following assets are loaded from CDN with automatic fallback to local copies:
Core Assets (Always Loaded)
| Asset | CDN Source | Local Fallback |
|---|---|---|
| Tailwind CSS | https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css |
static/shared/css/tailwind.min.css |
| Alpine.js | https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js |
static/shared/js/vendor/alpine.min.js |
Optional Assets (Loaded On Demand)
| Asset | CDN Source | Local Fallback | Used For |
|---|---|---|---|
| Chart.js | https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js |
static/shared/js/vendor/chart.umd.min.js |
Charts macros |
| Flatpickr JS | https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js |
static/shared/js/vendor/flatpickr.min.js |
Datepicker macros |
| Flatpickr CSS | https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css |
static/shared/css/vendor/flatpickr.min.css |
Datepicker styling |
Implementation
Tailwind CSS Fallback
Tailwind CSS uses the HTML onerror attribute to detect CDN failures and switch to the local copy:
<!-- Tailwind CSS with CDN fallback -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
How it works:
- Browser attempts to load Tailwind CSS from CDN
- If CDN fails (network error, timeout, 404),
onerrorevent fires - Handler sets
onerror=nullto prevent infinite loops - Handler updates
hrefto local copy path - Browser automatically loads local copy
Alpine.js Fallback
Alpine.js uses dynamic script loading with error handling:
<!-- Alpine.js with CDN fallback -->
<script>
(function() {
var script = document.createElement('script');
script.defer = true;
script.src = 'https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js';
script.onerror = function() {
console.warn('Alpine.js CDN failed, loading local copy...');
var fallbackScript = document.createElement('script');
fallbackScript.defer = true;
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>
How it works:
- IIFE creates a script element dynamically
- Sets
deferattribute to match CDN loading behavior - Attempts to load Alpine.js from CDN
- If CDN fails,
onerrorhandler triggers - Logs warning to console for debugging
- Creates new script element pointing to local copy
- Appends fallback script to
<head>
Optional Libraries (Chart.js & Flatpickr)
Optional libraries are loaded on-demand using Jinja macros. This keeps page load times fast by only loading what's needed.
Location: app/templates/shared/includes/optional-libs.html
Loading Chart.js
In your page template:
{% block chartjs_script %}
{% from 'shared/includes/optional-libs.html' import chartjs_loader %}
{{ chartjs_loader() }}
{% endblock %}
Generated code:
<script>
(function() {
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js';
script.onerror = function() {
console.warn('Chart.js CDN failed, loading local copy...');
var fallbackScript = document.createElement('script');
fallbackScript.src = '/static/shared/js/vendor/chart.umd.min.js';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>
Loading Flatpickr
Flatpickr requires both CSS and JS:
{# In head - load CSS #}
{% block flatpickr_css %}
{% from 'shared/includes/optional-libs.html' import flatpickr_css_loader %}
{{ flatpickr_css_loader() }}
{% endblock %}
{# Before page scripts - load JS #}
{% block flatpickr_script %}
{% from 'shared/includes/optional-libs.html' import flatpickr_loader %}
{{ flatpickr_loader() }}
{% endblock %}
Generated CSS code:
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css"
onerror="this.onerror=null; this.href='/static/shared/css/vendor/flatpickr.min.css';">
Available Blocks in admin/base.html
| Block | Purpose | Location |
|---|---|---|
flatpickr_css |
Flatpickr CSS | In <head> |
chartjs_script |
Chart.js | Before page scripts |
flatpickr_script |
Flatpickr JS | Before page scripts |
extra_scripts |
Page-specific JS | Last in body |
File Structure
static/
├── shared/
│ ├── css/
│ │ ├── tailwind.min.css # 2.9M - Tailwind CSS v2.2.19
│ │ └── vendor/
│ │ └── flatpickr.min.css # 16K - Flatpickr v4.6.13
│ └── js/
│ └── vendor/
│ ├── alpine.min.js # 44K - Alpine.js v3.13.3
│ ├── chart.umd.min.js # 205K - Chart.js v4.4.1
│ └── flatpickr.min.js # 51K - Flatpickr v4.6.13
├── shop/
│ └── css/
│ └── shop.css # Shop-specific styles
├── vendor/
│ └── css/
│ └── tailwind.output.css # Vendor-specific overrides
└── admin/
└── css/
└── tailwind.output.css # Admin-specific overrides
app/templates/shared/
├── includes/
│ └── optional-libs.html # CDN fallback loaders for Chart.js & Flatpickr
└── macros/
├── charts.html # Chart.js wrapper macros
└── datepicker.html # Flatpickr wrapper macros
Frontend-Specific Implementations
Shop Frontend
Template: app/templates/shop/base.html
{# Lines 41-42: Tailwind CSS fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Lines 247-263: Alpine.js fallback #}
<script>
(function() {
var script = document.createElement('script');
script.defer = true;
script.src = 'https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js';
script.onerror = function() {
console.warn('Alpine.js CDN failed, loading local copy...');
var fallbackScript = document.createElement('script');
fallbackScript.defer = true;
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>
Vendor Frontend
Template: app/templates/vendor/base.html
Same pattern as Shop frontend, with vendor-specific Tailwind overrides loaded after the base:
{# Lines 13-14: Tailwind CSS fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Line 17: Vendor-specific overrides #}
<link rel="stylesheet" href="{{ url_for('static', path='vendor/css/tailwind.output.css') }}" />
{# Lines 62-78: Alpine.js fallback #}
<!-- Same Alpine.js fallback pattern as Shop -->
Admin Frontend
Template: app/templates/admin/base.html
Same pattern as Vendor frontend, with admin-specific Tailwind overrides:
{# Lines 13-14: Tailwind CSS fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Line 17: Admin-specific overrides #}
<link rel="stylesheet" href="{{ url_for('static', path='admin/css/tailwind.output.css') }}" />
{# Lines 62-78: Alpine.js fallback #}
<!-- Same Alpine.js fallback pattern as Shop -->
Development Setup
Verifying Local Assets
Check that local fallback files exist:
# From project root
ls -lh static/shared/css/tailwind.min.css
ls -lh static/shared/js/vendor/alpine.min.js
Expected output:
-rw-r--r-- 1 user user 2.9M static/shared/css/tailwind.min.css
-rw-r--r-- 1 user user 44K static/shared/js/vendor/alpine.min.js
Testing Offline Mode
To test the fallback behavior:
-
Start the application:
uvicorn app.main:app --reload -
Simulate CDN failure:
- Use browser DevTools Network tab
- Add blocking rule for
cdn.jsdelivr.net - Or disconnect from internet
-
Check console output:
⚠️ Alpine.js CDN failed, loading local copy... -
Verify fallback loaded:
- Open DevTools Network tab
- Check that
/static/shared/js/vendor/alpine.min.jswas loaded - Check that
/static/shared/css/tailwind.min.csswas loaded
Updating Local Assets
To update Tailwind CSS to a newer version:
cd static/shared/css
curl -o tailwind.min.css https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css
To update Alpine.js to a newer version:
cd static/shared/js/vendor
curl -o alpine.min.js https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js
To update Chart.js:
cd static/shared/js/vendor
curl -o chart.umd.min.js https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js
To update Flatpickr:
# JavaScript
cd static/shared/js/vendor
curl -o flatpickr.min.js https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js
# CSS
cd static/shared/css/vendor
curl -o flatpickr.min.css https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css
Important: Update both the local file AND the CDN URL in the following locations:
| Asset | Update Location |
|---|---|
| Tailwind CSS | All three base.html templates |
| Alpine.js | All three base.html templates |
| Chart.js | shared/includes/optional-libs.html |
| Flatpickr | shared/includes/optional-libs.html |
Production Deployment
Verification Checklist
Before deploying to production, verify:
- Local asset files exist in
static/shared/ - Core assets:
tailwind.min.css,alpine.min.js - Optional assets:
chart.umd.min.js,flatpickr.min.js,flatpickr.min.css - File permissions are correct (readable by web server)
- Static file serving is enabled in FastAPI
- CDN URLs are accessible from production network
- Fallback mechanism tested in production-like environment
Docker Deployment
Ensure local assets are included in Docker image:
# Dockerfile
COPY static/ /app/static/
# Verify core files exist
RUN test -f /app/static/shared/css/tailwind.min.css && \
test -f /app/static/shared/js/vendor/alpine.min.js
# Verify optional library files exist
RUN test -f /app/static/shared/js/vendor/chart.umd.min.js && \
test -f /app/static/shared/js/vendor/flatpickr.min.js && \
test -f /app/static/shared/css/vendor/flatpickr.min.css
Static File Configuration
FastAPI configuration in app/main.py:
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")
Troubleshooting
Issue: Both CDN and local copy fail to load
Symptoms:
- No Tailwind styles applied
- Alpine.js not initializing
- Console errors about missing files
Solutions:
- Check static file mounting configuration
- Verify file paths in browser Network tab
- Check file permissions:
chmod 644 static/shared/css/tailwind.min.css - Verify FastAPI StaticFiles mount point
Issue: Fallback triggers unnecessarily
Symptoms:
- Console warnings in normal operation
- Inconsistent CDN loading
Solutions:
- Check network stability
- Verify CDN URLs are correct and accessible
- Check for firewall/proxy blocking CDN
- Consider using local-only mode for restricted networks
Issue: Page loads slowly due to CDN timeout
Symptoms:
- Long wait before fallback triggers
- Poor user experience during CDN issues
Solutions:
- Reduce CDN timeout (browser-dependent)
- Consider Content Security Policy (CSP) with shorter timeouts
- For air-gapped deployments, remove CDN URLs entirely and use local-only
Performance Considerations
CDN Benefits (Normal Operation)
- ✅ Faster initial load (CDN edge caching)
- ✅ Reduced server bandwidth
- ✅ Browser caching across sites using same CDN
Fallback Impact
- ⚠️ Additional ~3MB transferred on first load (if CDN fails)
- ⚠️ Slight delay during CDN failure detection
- ✅ Subsequent page loads use browser cache
Optimization Strategies
-
For production with reliable CDN access:
- Keep current implementation
- Monitor CDN uptime
-
For corporate/restricted networks:
- Consider removing CDN URLs entirely
- Load local assets directly
-
For air-gapped deployments:
- Remove CDN URLs completely
- Update templates to use local-only:
<!-- Local-only mode (no CDN) -->
<link rel="stylesheet" href="{{ url_for('static', path='shared/css/tailwind.min.css') }}">
<script defer src="{{ url_for('static', path='shared/js/vendor/alpine.min.js') }}"></script>
Browser Compatibility
The fallback strategy works in all modern browsers:
| Browser | Tailwind Fallback | Alpine.js Fallback |
|---|---|---|
| Chrome 90+ | ✅ | ✅ |
| Firefox 88+ | ✅ | ✅ |
| Safari 14+ | ✅ | ✅ |
| Edge 90+ | ✅ | ✅ |
Note: Internet Explorer is not supported (Alpine.js requires ES6+).
Security Considerations
Subresource Integrity (SRI)
Currently not implemented. To add SRI hashes:
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
integrity="sha384-..."
crossorigin="anonymous"
onerror="...">
Trade-off: SRI hashes prevent loading if CDN file changes, which may interfere with fallback mechanism.
Content Security Policy (CSP)
If using CSP, ensure CDN domains are whitelisted:
Content-Security-Policy:
style-src 'self' https://cdn.jsdelivr.net;
script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline';
Note: 'unsafe-inline' is required for the Alpine.js fallback IIFE.