A shared, config-driven header and footer shell for multi-team web applications. One script gives every app — React, Vue, Angular, static HTML — the same navigation and branding without touching app content.
┌─────────────────────────────────┐
│ #site-header │ ← Shell renders here
├─────────────────────────────────┤
│ │
│ Your app (React, Vue, etc.) │ ← Untouched by the shell
│ │
├─────────────────────────────────┤
│ #site-footer │ ← Shell renders here
└─────────────────────────────────┘
The shell loads a shared site-config.json, builds the header/footer and injects CSS scoped under .cfde__site-shell (no global resets that break your app).
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/gh/broadinstitute/site-shell@main/dist/site-shell.js"></script>
</head>
<body>
<div id="site-header"></div>
<!-- Your app content — the shell never touches this -->
<div id="app"></div>
<div id="site-footer"></div>
</body>
</html>That's it. The shell auto-initializes on DOMContentLoaded.
Note: The shell uses
fetch()to load config, so you must serve your HTML overhttp://— opening the file directly (file://) won't work. For local testing, runnpm run devor any static server (e.g.npx serve .).
All apps point to the same config URL:
{
"tissue": "Liver",
"cfde_logo": "assets/cfde.png",
"tissue_logo": "assets/liver.png",
"cfde_wheel": "assets/cfde_unified_icon.png",
"nih_logo": "assets/NIH_logo.png",
"drc_logo": "assets/drc_portrait.png",
"kc_logo": "assets/cfde_kc_logo_c.png",
"footer": "© Broad Institute",
"menu": [
{
"label": "About",
"path": "#about",
"submenu": [
{ "label": "About the Portal", "path": "#about-the-portal" },
{ "label": "Consortium", "path": "#consortium" }
]
},
{
"label": "Liver Atlas",
"submenu": [
{ "label": "Cell Atlas", "path": "#cell-atlas" },
{ "label": "Functional Atlas", "path": "#functional-atlas" }
]
},
{
"label": "Data and Resources",
"submenu": [
{ "label": "Datasets", "path": "#datasets" },
{ "label": "APIs", "path": "#apis" }
]
}
]
}In public/index.html (CRA) or index.html (Vite):
<head>
<script src="https://cdn.jsdelivr.net/gh/broadinstitute/site-shell@main/dist/site-shell.js"></script>
</head>
<body>
<div id="site-header"></div>
<div id="root"></div>
<div id="site-footer"></div>
</body>No changes to your React code. The shell renders outside #root.
In index.html:
<head>
<script src="https://cdn.jsdelivr.net/gh/broadinstitute/site-shell@main/dist/site-shell.js"></script>
</head>
<body>
<div id="site-header"></div>
<div id="app"></div>
<div id="site-footer"></div>
</body>Vue mounts to #app. The shell mounts to #site-header and #site-footer. They don't interfere.
In src/index.html:
<head>
<script src="https://cdn.jsdelivr.net/gh/broadinstitute/site-shell@main/dist/site-shell.js"></script>
</head>
<body>
<div id="site-header"></div>
<app-root></app-root>
<div id="site-footer"></div>
</body>In pages/_document.js (or app/layout.tsx):
// pages/_document.js
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html>
<Head>
<script src="https://cdn.jsdelivr.net/gh/broadinstitute/site-shell@main/dist/site-shell.js" />
</Head>
<body>
<div id="site-header" />
<Main />
<div id="site-footer" />
<NextScript />
</body>
</Html>
);
}<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/gh/broadinstitute/site-shell@main/dist/site-shell.js"></script>
</head>
<body>
<div id="site-header"></div>
<main>
<h1>My Static Page</h1>
<p>Content goes here.</p>
</main>
<div id="site-footer"></div>
</body>
</html>| Field | Type | Required | Description |
|---|---|---|---|
tissue |
string |
No | Name displayed in the header title (e.g. "Liver") |
cfde_logo |
string |
No | Path/URL to CFDE logo image |
tissue_logo |
string |
No | Path/URL to tissue-specific logo |
cfde_wheel |
string |
No | Path/URL to CFDE wheel graphic |
nih_logo |
string |
No | Path/URL to NIH logo (footer) |
drc_logo |
string |
No | Path/URL to DRC logo (footer) |
kc_logo |
string |
No | Path/URL to Knowledge Center logo (footer) |
footer |
string |
No | Custom copyright/footer text. Falls back to a default CFDE copyright line. |
menu |
array |
No | Navigation menu items (see below) |
{
"label": "Dashboard",
"path": "/dashboard",
"submenu": [{ "label": "Overview", "path": "/dashboard/overview" }]
}label— Display text (rendered viatextContent, safe from XSS)path— Link URL. Optional — omit for non-clickable parent items that only have a submenu dropdown (e.g. "Liver Atlas" has nopath, justsubmenu)submenu— Optional array of child items (one level deep). Parent highlights automatically when any child is active.
Logo/image paths support three formats:
- Absolute URL:
https://cdn.example.com/logo.png - Root-relative:
/assets/logo.png - Script-relative:
assets/logo.png(resolved relative to wheresite-shell.jsis hosted)
javascript: and data: URIs are blocked automatically.
| Attribute | Description |
|---|---|
data-config-url |
URL to fetch config JSON from. Defaults to config/site-config.json relative to the script. |
All teams deploy independently. A single reverse proxy routes by path prefix and serves the shared config:
Nginx:
server {
listen 443 ssl;
server_name app.example.com;
# Shared config — one file, all apps read it
location = /config.json { alias /shared/site-config.json; }
# Route to team apps by prefix
location /dashboard/ { proxy_pass http://team-a:3000/dashboard/; }
location /reports/ { proxy_pass http://team-b:3000/reports/; }
location /admin/ { proxy_pass http://team-c:3000/admin/; }
location / { proxy_pass http://team-a:3000/; }
}cd site-shell
npm install
npm run devOpen http://localhost:5173 to see the shell with the sample config.
npm run build
# Output: dist/site-shell.js (single IIFE file with CSS injected)Create a config with a malicious payload and verify it renders as plain text, not executable HTML:
{
"tissue": "<img src=x onerror=alert('XSS')>",
"menu": [
{
"label": "<script>alert('XSS')</script>",
"path": "javascript:alert('XSS')"
}
],
"cfde_logo": "javascript:alert('XSS')"
}Expected: The title shows the literal string <img src=x onerror=alert('XSS')>. No alert fires. Logo src is empty (blocked by URI sanitization). Menu items with unsafe URLs do not get an href attribute.
Stop the config server or point to a bad URL:
<div id="site-header" data-config-url="http://localhost:9999/bad.json"></div>Expected: Console shows [site-shell] Network error loading config.... No crash. Shell renders empty (default config).
Load the shell alongside your app and verify no style conflicts:
<head>
<script src="/dist/site-shell.js"></script>
<style>
/* Your app styles — these should NOT be affected */
.nav {
background: red;
padding: 100px;
}
.menu-item {
color: green;
font-size: 3em;
}
a {
color: purple;
}
* {
margin: 50px;
}
</style>
</head>
<body>
<div id="site-header"></div>
<div id="app">
<div class="nav">App nav — should be red with 100px padding</div>
<div class="menu-item">App item — should be green 3em</div>
</div>
<div id="site-footer"></div>
</body>Expected: Shell header/footer render normally with blue theme. App content keeps its own red/green styles. No bleed in either direction.
<body>
<!-- No #site-header or #site-footer -->
<div id="app">Just an app</div>
</body>Expected: Console shows [site-shell] No #site-header or #site-footer found. No error thrown.
Navigate to a URL matching a menu path (e.g. /dashboard/overview):
Expected: The "Dashboard" menu item and "Overview" submenu item both show active styling.
- No Shadow DOM — the shell injects into plain
<div>containers so it works everywhere - Config-driven — all content (labels, logos, links) comes from one shared JSON
- XSS-safe — all rendering uses
document.createElement+textContent(neverinnerHTML) - CSS-scoped — BEM naming under
.cfde__site-shell, no global resets - Framework-agnostic — plain JS, works with any tech stack
- Independent deploys — each team deploys their own app; the shell script is loaded from CDN/npm
git clone git@github.com:broadinstitute/site-shell.git
cd site-shell
npm install
npm run dev # Start dev server at localhost:5173
npm run build # Build dist/site-shell.jsSee LICENSE.