# 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)
