Files
orion/docs/frontend/cdn-fallback-strategy.md
Samir Boulahtit 7879e2f70a 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>
2025-11-22 15:52:46 +01:00

11 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:

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:

<!-- 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:

<!-- 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

{# 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:

  1. Start the application:

    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:

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

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
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:

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:
<!-- 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.