Files
orion/docs/frontend/cdn-fallback-strategy.md
Samir Boulahtit d648c921b7
Some checks failed
CI / ruff (push) Successful in 10s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
docs: add consolidated dev URL reference and migrate /shop to /storefront
- Add Development URL Quick Reference section to url-routing overview
  with all login URLs, entry points, and full examples
- Replace /shop/ path segments with /storefront/ across 50 docs files
- Update file references: shop_pages.py → storefront_pages.py,
  templates/shop/ → templates/storefront/, api/v1/shop/ → api/v1/storefront/
- Preserve domain references (orion.shop) and /store/ staff dashboard paths
- Archive docs left unchanged (historical)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:23:44 +01:00

502 lines
15 KiB
Markdown

# CDN Fallback Strategy
## Overview
All three frontends (Shop, Store, 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/lib/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/lib/chart.umd.min.js` | Charts macros |
| **Flatpickr JS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js` | `static/shared/js/lib/flatpickr.min.js` | Datepicker macros |
| **Flatpickr CSS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css` | `static/shared/css/store/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:
```html
<!-- 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:**
1. Browser attempts to load Tailwind CSS from CDN
2. If CDN fails (network error, timeout, 404), `onerror` event fires
3. Handler sets `onerror=null` to prevent infinite loops
4. Handler updates `href` to local copy path
5. Browser automatically loads local copy
### Alpine.js Fallback
Alpine.js uses dynamic script loading with error handling:
```html
<!-- 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/lib/alpine.min.js") }}';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>
```
**How it works:**
1. IIFE creates a script element dynamically
2. Sets `defer` attribute to match CDN loading behavior
3. Attempts to load Alpine.js from CDN
4. If CDN fails, `onerror` handler triggers
5. Logs warning to console for debugging
6. Creates new script element pointing to local copy
7. 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:
```jinja
{% block chartjs_script %}
{% from 'shared/includes/optional-libs.html' import chartjs_loader %}
{{ chartjs_loader() }}
{% endblock %}
```
**Generated code:**
```html
<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/lib/chart.umd.min.js';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>
```
#### Loading Flatpickr
Flatpickr requires both CSS and JS:
```jinja
{# 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:**
```html
<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/store/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
│ │ └── store/
│ │ └── flatpickr.min.css # 16K - Flatpickr v4.6.13
│ └── js/
│ └── store/
│ ├── 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
├── store/
│ └── css/
│ └── tailwind.output.css # Store-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/storefront/base.html`
```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/lib/alpine.min.js") }}';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>
```
### Store Frontend
**Template:** `app/templates/store/base.html`
Same pattern as Shop frontend, with store-specific Tailwind overrides loaded after the base:
```html
{# 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: Store-specific overrides #}
<link rel="stylesheet" href="{{ url_for('static', path='store/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 Store frontend, with admin-specific Tailwind overrides:
```html
{# 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:
```bash
# From project root
ls -lh static/shared/css/tailwind.min.css
ls -lh static/shared/js/lib/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/lib/alpine.min.js
```
### Testing Offline Mode
To test the fallback behavior:
1. **Start the application:**
```bash
uvicorn app.main:app --reload
```
2. **Simulate CDN failure:**
- Use browser DevTools Network tab
- Add blocking rule for `cdn.jsdelivr.net`
- Or disconnect from internet
3. **Check console output:**
```
⚠️ Alpine.js CDN failed, loading local copy...
```
4. **Verify fallback loaded:**
- Open DevTools Network tab
- Check that `/static/shared/js/lib/alpine.min.js` was loaded
- Check that `/static/shared/css/tailwind.min.css` was loaded
### Updating Local Assets
To update Tailwind CSS to a newer version:
```bash
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:
```bash
cd static/shared/js/store
curl -o alpine.min.js https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js
```
To update Chart.js:
```bash
cd static/shared/js/store
curl -o chart.umd.min.js https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js
```
To update Flatpickr:
```bash
# JavaScript
cd static/shared/js/store
curl -o flatpickr.min.js https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js
# CSS
cd static/shared/css/store
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
# 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/lib/alpine.min.js
# Verify optional library files exist
RUN test -f /app/static/shared/js/lib/chart.umd.min.js && \
test -f /app/static/shared/js/lib/flatpickr.min.js && \
test -f /app/static/shared/css/store/flatpickr.min.css
```
### Static File Configuration
FastAPI configuration in `app/main.py`:
```python
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:**
1. Check static file mounting configuration
2. Verify file paths in browser Network tab
3. Check file permissions: `chmod 644 static/shared/css/tailwind.min.css`
4. Verify FastAPI StaticFiles mount point
### Issue: Fallback triggers unnecessarily
**Symptoms:**
- Console warnings in normal operation
- Inconsistent CDN loading
**Solutions:**
1. Check network stability
2. Verify CDN URLs are correct and accessible
3. Check for firewall/proxy blocking CDN
4. 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:**
1. Reduce CDN timeout (browser-dependent)
2. Consider Content Security Policy (CSP) with shorter timeouts
3. 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
1. **For production with reliable CDN access:**
- Keep current implementation
- Monitor CDN uptime
2. **For corporate/restricted networks:**
- Consider removing CDN URLs entirely
- Load local assets directly
3. **For air-gapped deployments:**
- Remove CDN URLs completely
- Update templates to use local-only:
```html
<!-- 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/lib/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:
```html
<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:
```http
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.
## Related Documentation
- [Storefront Architecture](storefront/architecture.md)
- [Store Frontend Architecture](store/architecture.md)
- [Admin Frontend Architecture](admin/architecture.md)
- [Production Deployment](../deployment/production.md)
- [Docker Deployment](../deployment/docker.md)
- [Troubleshooting Guide](../development/troubleshooting.md)