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

15 KiB

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:

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

{% block chartjs_script %}
{% from 'shared/includes/optional-libs.html' import chartjs_loader %}
{{ chartjs_loader() }}
{% endblock %}

Generated code:

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

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

<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

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

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

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

    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:

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/store
curl -o alpine.min.js https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js

To update Chart.js:

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:

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

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

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