Some checks failed
- 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>
502 lines
15 KiB
Markdown
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)
|