Add a JSX app
A "JSX app" is a single-file React component in apps/ that gets auto-wrapped into a standalone HTML page by deploy.sh. They're how every interactive tool on the intranet is built (Recruiting, Dashboards, Team Calendar, etc.).
The recipe
- Create a file at
apps/<name>.jsx. - Default-export a function component.
- Push.
git add apps/my_new_tool.jsx
git commit -m "Add my new tool"
git push origin main
Within 5 minutes:
deploy.shcopies the file to/var/www/team-site/apps/.- It generates an HTML wrapper that loads React 18 + Babel-standalone from a CDN and renders your component into
#root. - The result is reachable at
/apps/<name>.html.
You also probably want to add it to the homepage tool grid in index.html (see below).
The file shape
import { useState } from "react";
export default function MyNewTool() {
const [count, setCount] = useState(0);
return (
<div style={{ padding: 20, fontFamily: 'system-ui' }}>
<h1>Hello, intranet</h1>
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
</div>
);
}
That's it. No index.html, no build config, no router, no package.json.
What you get for free
The wrapper script pre-destructures these from React so you don't have to import them:
const { useState, useEffect, useRef, useCallback, useMemo } = React;
Use any of these directly without import statements. The wrapper strips your import line.
What you cannot import
Only import { useState } from "react" is supported (and it gets stripped — the React hooks are provided by the wrapper). You cannot import:
- Other npm packages (no
import axios from "axios") - Other files (no
import Helper from "./helper.jsx") - CSS files (no
import "./styles.css")
If you need a utility, define it in the same file. If you need a stylesheet, embed it in a <style> tag inside your component or use inline style={{}} props.
If you genuinely need a more complex frontend (multiple files, a real bundle), look at how Laurelin does it: apps/laurelin.entry.jsx is an esbuild entrypoint that pulls in apps/laurelin.jsx plus its dependencies. You'd add a similar entrypoint and a corresponding esbuild line in deploy.sh. But for almost all apps, single-file is correct.
Hitting the API
If you need data, fetch it. The intranet's Node backend exposes Laurelin endpoints at /api/laurelin/* and Recruiting at /api/recruiting/*.
export default function CompaniesView() {
const [companies, setCompanies] = useState([]);
useEffect(() => {
fetch("/api/laurelin/companies")
.then((r) => r.json())
.then(setCompanies);
}, []);
return (
<ul>{companies.map((c) => <li key={c.id}>{c.name}</li>)}</ul>
);
}
The full API surface: API reference.
Naming
snake_case.jsx. Examples in the repo:
apps/company_intake.jsx
apps/content_library.jsx
apps/dd_matrix.jsx
apps/data_ops.jsx
apps/team_vacation_calendar.jsx
apps/portfolio_reports.jsx
Filename becomes the URL slug: apps/team_vacation_calendar.jsx → /apps/team_vacation_calendar.html.
Adding to the homepage
The homepage card grid is hardcoded in index.html. Find the existing pattern (around line 217):
<a href="/apps/my_new_tool.html" class="tool-card">
<span class="tool-badge jsx">JSX</span>
<div class="tool-title">My New Tool</div>
<div class="tool-desc">Short, plain-English description of what it does.</div>
</a>
Available badges:
| Badge | When to use |
|---|---|
JSX |
Standard interactive app — most things |
Draft |
Preview / not-yet-wired-up |
Map |
Diagrams, product flows, architecture visuals |
HTML |
A static HTML page rather than a JSX app |
Commit both files together:
git add apps/my_new_tool.jsx index.html
git commit -m "Add My New Tool"
git push
Styling
Two approaches, pick one:
Inline styles (most common in our apps):
<div style={{
padding: '24px',
background: '#f4f7fa',
border: '1px solid #c5cdd3',
borderRadius: '8px',
}}>
Embedded stylesheet in a <style> block inside the component:
return (
<>
<style>{`
.my-card { padding: 24px; background: #f4f7fa; }
.my-card h2 { color: #324654; }
`}</style>
<div className="my-card">...</div>
</>
);
Either works. Inline is faster to iterate on for small apps. Embedded stylesheet wins once you have repeated visuals.
The brand palette is in CLAUDE.md and in the Add a partnership review guide:
--background: #ffffff
--text: #1c1c1b
--muted: #92a8b6
--accent: #324654 (ocean)
--surface: #f4f7fa
--border: #c5cdd3
--tinted: #e6effa (sky)
Common mistakes
- Multiple top-level functions. The wrapper looks for one
export default function ComponentName(). If you export differently, the wrapper won't find your component and the page renders blank. - Using JSX file extension but writing plain HTML/JS. Has to be a React component. If you want a static page, put it in
static/instead. - Importing a package. Won't work. Either define it inline or use Laurelin's esbuild pattern.
- Forgetting to add to the homepage. The app is reachable at the URL, but no one will find it. Add a card.
- Filename collision with an existing app. Check
apps/first.
Adding data files
If your app needs JSON data (rubrics, library content, configurations):
apps/rubrics/ ← copied as-is to /var/www/team-site/apps/rubrics/
apps/library/ ← copied as-is to /var/www/team-site/apps/library/
apps/proposals/ ← PDFs, copied as-is
Fetch from your app:
useEffect(() => {
fetch("/apps/rubrics/audit-partners.json").then(r => r.json()).then(setRubric);
}, []);
Add new sub-directories under apps/ only if you have a reason — three is enough so far.
Iterating locally
There's no local dev server for the intranet — the deploy pipeline is the build. The two options:
Option A — preview in-browser by serving locally.
cd valinor-intra
python -m http.server 8000
# Open http://localhost:8000/apps/my_new_tool.jsx … no, won't work because Babel needs the HTML wrapper.
Instead, copy a wrapper template (from any deployed .html in /var/www/team-site/apps/) and adjust the inline script tag to point at your local file.
Option B — push to a feature branch and ssh to ValinorPC for a one-off deploy.
Cleaner, but slower turnaround. Most authors just push to main and accept the 5-minute deploy cycle, because Babel-standalone's runtime errors are visible in the browser console immediately.
When to graduate from a JSX app
If your tool becomes:
- A multi-file bundle, or
- Needs npm packages, or
- Needs a backend route that isn't already in Laurelin/Recruiting,
…it should probably move into the Laurelin app or get its own esbuild entry. Talk to engineering before adding a new top-level bundle — keeping the deploy script simple has been a deliberate choice and we don't add new bundle paths lightly.