docs: add CDN fallback strategy documentation
Add comprehensive documentation for CDN fallback strategy used across the platform's frontend. Documents the pattern for loading external libraries (Alpine.js, Tailwind CSS, etc.) with automatic fallback to local copies when CDN is unavailable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
375
docs/frontend/cdn-fallback-strategy.md
Normal file
375
docs/frontend/cdn-fallback-strategy.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# 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:
|
||||
|
||||
| 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` |
|
||||
|
||||
## 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/vendor/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>`
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
static/
|
||||
├── shared/
|
||||
│ ├── css/
|
||||
│ │ └── tailwind.min.css # 2.9M - Tailwind CSS v2.2.19
|
||||
│ └── js/
|
||||
│ └── vendor/
|
||||
│ └── alpine.min.js # 44K - Alpine.js v3.13.3
|
||||
├── shop/
|
||||
│ └── css/
|
||||
│ └── shop.css # Shop-specific styles
|
||||
├── vendor/
|
||||
│ └── css/
|
||||
│ └── tailwind.output.css # Vendor-specific overrides
|
||||
└── admin/
|
||||
└── css/
|
||||
└── tailwind.output.css # Admin-specific overrides
|
||||
```
|
||||
|
||||
## Frontend-Specific Implementations
|
||||
|
||||
### Shop Frontend
|
||||
|
||||
**Template:** `app/templates/shop/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/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:
|
||||
|
||||
```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: 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:
|
||||
|
||||
```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/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:
|
||||
|
||||
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/vendor/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/vendor
|
||||
curl -o alpine.min.js https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js
|
||||
```
|
||||
|
||||
**Important:** Update both the local file AND the CDN URL in all three base templates.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
Before deploying to production, verify:
|
||||
|
||||
- [ ] Local asset files exist in `static/shared/`
|
||||
- [ ] 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 files exist
|
||||
RUN test -f /app/static/shared/css/tailwind.min.css && \
|
||||
test -f /app/static/shared/js/vendor/alpine.min.js
|
||||
```
|
||||
|
||||
### 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/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:
|
||||
|
||||
```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
|
||||
|
||||
- [Shop Frontend Architecture](shop/architecture.md)
|
||||
- [Vendor Frontend Architecture](vendor/architecture.md)
|
||||
- [Admin Frontend Architecture](admin/architecture.md)
|
||||
- [Production Deployment](../deployment/production.md)
|
||||
- [Docker Deployment](../deployment/docker.md)
|
||||
- [Troubleshooting Guide](../development/troubleshooting.md)
|
||||
Reference in New Issue
Block a user