Files
orion/docs/frontend/cdn-fallback-strategy.md
Samir Boulahtit dad0989948 docs: add comprehensive Jinja macros documentation
New documentation:
- docs/frontend/shared/jinja-macros.md: Complete reference for all 94 macros
  - Alerts, Avatars, Badges, Buttons, Cards, Charts, Datepicker
  - Dropdowns, Forms, Headers, Modals, Pagination, Tables
  - Usage examples and parameter documentation
  - Complete page example showing all macros together

Updated documentation:
- docs/frontend/cdn-fallback-strategy.md:
  - Add Chart.js (v4.4.1) and Flatpickr (v4.6.13) sections
  - Document optional-libs.html loader macros
  - Update file structure and deployment checklist

- docs/frontend/shared/ui-components.md:
  - Add tip box recommending Jinja macros for new development
  - Update related documentation links

- mkdocs.yml:
  - Add Jinja Macros Library to navigation (top of Shared Components)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 19:15:27 +01:00

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

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/vendor/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/vendor/chart.umd.min.js Charts macros
Flatpickr JS https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js static/shared/js/vendor/flatpickr.min.js Datepicker macros
Flatpickr CSS https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css static/shared/css/vendor/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/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>

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/vendor/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/vendor/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
│   │   └── vendor/
│   │       └── flatpickr.min.css         # 16K - Flatpickr v4.6.13
│   └── js/
│       └── vendor/
│           ├── 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
├── vendor/
│   └── css/
│       └── tailwind.output.css           # Vendor-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/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

To update Chart.js:

cd static/shared/js/vendor
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/vendor
curl -o flatpickr.min.js https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js

# CSS
cd static/shared/css/vendor
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/vendor/alpine.min.js

# Verify optional library files exist
RUN test -f /app/static/shared/js/vendor/chart.umd.min.js && \
    test -f /app/static/shared/js/vendor/flatpickr.min.js && \
    test -f /app/static/shared/css/vendor/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/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.