Files
soleprint/docs/architecture/05-sidebar-injection.md
2026-01-27 09:24:05 -03:00

7.1 KiB

Sidebar Injection Architecture

Overview

The soleprint sidebar is injected into managed app pages using a hybrid nginx + JavaScript approach. This allows any frontend framework (React, Next.js, static HTML) to receive the sidebar without code modifications.

How It Works

┌─────────────────────────────────────────────────────────────────┐
│                         Browser Request                          │
│                    http://room.spr.local.ar/                     │
└─────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────┐
│                            Nginx                                 │
│                                                                  │
│  1. Routes /spr/* → soleprint:PORT (sidebar assets + API)       │
│  2. Routes /* → frontend:PORT (app pages)                       │
│  3. Injects CSS+JS into HTML responses via sub_filter           │
│                                                                  │
│  sub_filter '</head>'                                           │
│    '<link rel="stylesheet" href="/spr/sidebar.css">             │
│     <script src="/spr/sidebar.js" defer></script></head>';      │
└─────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────┐
│                       Browser Renders                            │
│                                                                  │
│  1. Page loads with injected CSS (sidebar styles ready)         │
│  2. sidebar.js executes (deferred, after DOM ready)             │
│  3. JS fetches /spr/api/sidebar/config (room name, auth, etc)   │
│  4. JS creates sidebar DOM elements and injects into page       │
│  5. Sidebar appears on left side, pushes content with margin    │
└─────────────────────────────────────────────────────────────────┘

Key Design Decisions

Why nginx sub_filter instead of modifying app code?

  • Framework agnostic: Works with any frontend (Next.js, React, Vue, static HTML)
  • No app changes needed: The managed app doesn't need to know about soleprint
  • Easy to disable: Just access room.local.ar instead of room.spr.local.ar

Why inject into </head> instead of </body>?

Next.js and other streaming SSR frameworks may not include </body> in the initial HTML response. The </head> tag is always present, so we inject both CSS and JS there using defer to ensure JS runs after DOM is ready.

Why JavaScript injection instead of iframe?

  • No iframe isolation issues: Sidebar can interact with page if needed
  • Better UX: No double scrollbars, native feel
  • Simpler CSS: Just push content with margin-left

Nginx Configuration

For Local Development (system nginx)

Each room needs entries in /etc/nginx/sites-enabled/:

# room.spr.local.ar - app with sidebar
server {
    listen 80;
    server_name room.spr.local.ar;

    # Soleprint assets and API
    location /spr/ {
        proxy_pass http://127.0.0.1:SOLEPRINT_PORT/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Backend API (if applicable)
    location /api/ {
        proxy_pass http://127.0.0.1:BACKEND_PORT/api/;
        # ... headers
    }

    # Frontend with sidebar injection
    location / {
        proxy_pass http://127.0.0.1:FRONTEND_PORT;
        proxy_set_header Host $host;
        proxy_set_header Accept-Encoding "";  # Required for sub_filter
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Inject sidebar
        sub_filter '</head>' '<link rel="stylesheet" href="/spr/sidebar.css"><script src="/spr/sidebar.js" defer></script></head>';
        sub_filter_once off;
        sub_filter_types text/html;
    }
}

# room.local.ar - app without sidebar (direct access)
server {
    listen 80;
    server_name room.local.ar;
    # ... same as above but without sub_filter
}

For Docker/AWS (nginx container)

The nginx config lives in cfg/<room>/soleprint/nginx/local.conf and uses docker service names instead of localhost ports:

location /spr/ {
    proxy_pass http://soleprint:8000/spr/;
}

location / {
    proxy_pass http://frontend:3000;
    # ... sub_filter same as above
}

Port Allocation

Each room should use unique ports to allow concurrent operation:

Room Soleprint Frontend Backend
amar 12000 3000 8001
dlt 12010 3010 -
sample 12020 3020 8020

Sidebar Assets

The sidebar consists of two files served by soleprint:

  • /sidebar.css - Styles for the sidebar (dark theme, positioning)
  • /sidebar.js - Self-contained JS that fetches config and renders sidebar

Source location: soleprint/station/tools/sbwrapper/

Sidebar Config API

The sidebar JS fetches configuration from /spr/api/sidebar/config:

{
  "room": "amar",
  "soleprint_base": "/spr",
  "auth_enabled": true,
  "tools": {
    "artery": "/spr/artery",
    "atlas": "/spr/atlas",
    "station": "/spr/station"
  },
  "auth": {
    "login_url": "/spr/artery/google/oauth/login",
    "logout_url": "/spr/artery/google/oauth/logout"
  }
}

Troubleshooting

Sidebar not appearing

  1. Check if soleprint is running: curl http://room.spr.local.ar/spr/sidebar.js
  2. Check browser console for [Soleprint] messages
  3. Verify nginx has Accept-Encoding "" set (required for sub_filter)
  4. Hard refresh (Ctrl+Shift+R) to clear cached HTML

Wrong room config showing

Each room needs its own docker network (room_network) to isolate services. Check NETWORK_NAME in .env files and ensure containers are on correct networks.

sub_filter not working

  • Ensure proxy_set_header Accept-Encoding "" is set
  • Check that response is text/html (sub_filter_types)
  • Streaming SSR may not have </body>, use </head> injection instead