- JavaScript 88.2%
- CSS 6.5%
- HTML 4.1%
- TypeScript 1.2%
The SSR build serializes inline styles without a space after the property colon (grid-template-columns:1.2fr 1fr), but the responsive selector matched the spaced form (grid-template-columns: 1.2fr 1fr), so the breakpoint never applied in dist and the results panel overflowed on mobile. Match on the value substring (1.2fr 1fr), which is identical in both the SSR and client-rendered builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| assets | ||
| build | ||
| cms | ||
| legal/fr | ||
| webhook | ||
| .gitignore | ||
| .htaccess | ||
| .image-slots.state.json | ||
| .thumbnail | ||
| app.jsx | ||
| blog-page.jsx | ||
| blog-post-page.jsx | ||
| demo-page.jsx | ||
| faq-page.jsx | ||
| favicon.svg | ||
| founder-photos.js | ||
| go-page.jsx | ||
| home-sections.jsx | ||
| image-slot.js | ||
| index-print.html | ||
| index.html | ||
| islands.js | ||
| logo.jsx | ||
| mentions-legales-page.jsx | ||
| pages.jsx | ||
| phone.jsx | ||
| phormian-logo.svg | ||
| privacy-page.jsx | ||
| README.md | ||
| site-shell.jsx | ||
| styles.css | ||
| support-page.jsx | ||
| team-page.jsx | ||
Phormian landing site
Static site generated from the JSX sources in this directory.
React renders the markup at build time (SSR) and the pages ship as plain HTML with no React runtime. The interactive bits run as small vanilla-JS "islands" (islands.js) that progressively enhance the static markup — the page is fully readable and navigable with JS disabled.
The islands: navigation (real <a href> + a [data-nav] click delegate), reveal-on-scroll, persona switcher, ROI simulator, and the demo form's no-reload AJAX submit. The phone mockup ships as a static snapshot (its transaction-feed animation was dropped — it was ambient motion, not an interaction). The demo form also works without JS via native <form action method=POST> + required fields.
Layout
.
├── *.jsx, *.js ← source components (edit these)
├── islands.js ← vanilla progressive-enhancement widgets (no React)
├── styles.css ← shared CSS
├── assets/ ← portrait jpgs
├── phormian-logo.svg
├── .htaccess ← Apache config (HTTPS, cache, gzip, 404)
├── legal/fr/… ← legacy bootstraps (not used by the static build)
├── build/
│ ├── build.mjs ← SSR + photo extraction + Strapi fetch (no client bundle)
│ └── package.json
├── cms/ ← Strapi v5 CMS (blog + FAQ) — see cms/README.md
├── webhook/ ← rebuild webhook listener — see webhook/README.md
└── dist/ ← build output (deploy this)
Build
Requires Node ≥ 18.
cd build
npm install # first time only
STRAPI_URL=https://cms.phormian.fr \
STRAPI_TOKEN=… # read-only API token from cms/admin → Settings → API Tokens
node build.mjs
If Strapi is unreachable (e.g. very first deploy before any content exists), set STRAPI_OPTIONAL=1 to continue with an empty blog and FAQ instead of failing the build.
Output lands in ../dist/. Each route becomes its own index.html inside a directory:
dist/
├── index.html → /
├── sol-assureurs/index.html → /sol-assureurs
├── sol-drh/index.html → /sol-drh
├── sol-marques/index.html → /sol-marques
├── fonctionnement/index.html → /fonctionnement
├── pourquoi/index.html → /pourquoi
├── team/index.html → /team
├── demo/index.html → /demo
├── blog/index.html → /blog (article list)
├── blog/<slug>/index.html → /blog/<slug> (one per article)
├── faq/index.html → /faq
├── legal/fr/privacy/index.html → /legal/fr/privacy
├── legal/fr/mentions-legales/index.html → /legal/fr/mentions-legales
├── islands.js ← vanilla enhancement widgets
├── styles.css, phormian-logo.svg, .htaccess
└── assets/ ← portrait jpgs
Pages load no React and no client bundle — just styles.css and islands.js.
What the build does
- Reads all
.jsxfiles, concatenates them in dependency order. - Prepends a global
hrefFor(page)helper (a route-table lookup) so components emit real<a href>links for static navigation. - Patches
team-page.jsxto use plain<img>instead of the<image-slot>custom element (the custom element was a Claude-design upload tool, dropped from production). - Decodes every base64 portrait in
founder-photos.jsinto a real.jpgunderdist/assets/. - Compiles JSX once with
@babel/preset-react. - Renders each route to HTML with
ReactDOMServer.renderToStringagainst a minimalwindow/documentshim. - Emits one HTML file per route, wrapping the SSR markup in a shell that loads only
styles.cssandislands.js(no React, no client bundle). - Copies
islands.jsintodist/.
Editing content
The site has two kinds of content:
- Static pages (Home, Solutions, Team, Demo, legal): JSX in the repo. Edit, rebuild, deploy.
- Blog + FAQ: Strapi CMS (
cms/). Non-technical writers manage these from the admin UI athttps://cms.phormian.fr/admin. Publishing fires a webhook that rebuilds + redeploys the site automatically — seewebhook/README.mdandcms/README.md.
Common edits:
| Change | File |
|---|---|
| Hero copy, persona pills | site-shell.jsx |
| Home sections | home-sections.jsx |
| Solution / why / how pages | pages.jsx |
| Team roster | team-page.jsx |
| Demo form, ROI sim (markup) | demo-page.jsx |
| Interactive behavior (persona, sim, form, nav, reveal) | islands.js |
| Blog list / post layout | blog-page.jsx, blog-post-page.jsx |
| FAQ layout | faq-page.jsx |
| Blog / FAQ content | Strapi admin (cms/) |
| Footer, page list | app.jsx |
| Styles | styles.css |
Interactive components are split in two: the JSX renders the static markup (with data-* hooks), and islands.js attaches the behavior to those hooks. Change a slider's math or the persona copy in islands.js; change its layout in the JSX. Keep the route table in islands.js (PATHS) in sync with STATIC_ROUTES in build.mjs.
The headline variant, accent color, default persona, and which sections render are controlled by the TWEAK_DEFAULTS block embedded by the original index.html — in the static build that lives in build.mjs at the top.
Adding a route
- Add the route to
STATIC_ROUTESinbuild/build.mjs(page key + URL + output path). - Add a title in
STATIC_TITLES. - Add the matching
page === '…'branch inapp.jsx. - Mirror the new page → URL entry in
PATHSinislands.js(used by[data-nav]buttons). - Add nav / footer links (
<a href={hrefFor('…')}>for links,data-nav="…"for buttons). - Rebuild.
Blog post URLs (/blog/:slug) are generated dynamically by the build from the Strapi article list — no manual route entries needed.
Adding a team photo
- Drop the JPG in
assets/(any name). - Either reference it directly with
photo: 'assets/foo.jpg'inteam-page.jsx, or add it tofounder-photos.jsas a base64 data URI — the build will extract it on the next run.
Deploy
Apache (recommended — .htaccess is included)
rsync -av --delete dist/ user@host:/var/www/phormian/
The shipped .htaccess handles HTTPS redirect, gzip, long-cache for assets, no-cache for HTML, and a 404 → /index.html fallback.
Nginx
server {
listen 443 ssl;
server_name phormian.example;
root /var/www/phormian;
location / {
try_files $uri $uri/index.html /index.html;
}
# Long-lived assets
location ~* \.(css|js|jpg|jpeg|png|svg|webp|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# No cache on the HTML shell
location ~* \.html$ {
add_header Cache-Control "no-cache, must-revalidate";
}
gzip on;
gzip_types text/css application/javascript text/html image/svg+xml;
}
Netlify / Vercel / Cloudflare Pages
Point the host at dist/ as the publish directory. No build command needed (run the build locally and commit dist/, or wire cd build && npm ci && node build.mjs into the host's build step). Directory-based routing works out of the box.
S3 + CloudFront
aws s3 sync dist/ s3://phormian-site/ --delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "*.html"
aws s3 sync dist/ s3://phormian-site/ \
--cache-control "no-cache, must-revalidate" \
--exclude "*" --include "*.html"
Configure the bucket for static hosting with index document index.html. In CloudFront, add a function that appends index.html to paths ending in / so directory URLs resolve.
GitHub Pages
cd dist
git init && git add . && git commit -m "deploy"
git push -f git@github.com:org/repo.git main:gh-pages
Pages serves directory index.html files natively.
Troubleshooting
- An interaction does nothing. Check the browser console for an
islands.jserror, and confirm the markup still has thedata-*hook the island queries (e.g.data-sim-field,data-persona). The page itself stays readable regardless — islands only enhance. - A page renders blank during the build. Usually a JSX/SSR error — check the
node build.mjsoutput for the failing route and stack trace. - A nav link goes to
/unexpectedly. The page key isn't in the route table. Add it toSTATIC_ROUTES(build.mjs) and toPATHS(islands.js). - A route 404s in production. The host isn't serving directory
index.html. Use one of the configs above, or copydist/index.htmltodist/<route>.html. - Photos missing. Confirm
dist/assets/photo-*.jpgexists. Re-run the build (portraits are extracted fromfounder-photos.jsand inlined into the SSR markup).
CMS (blog + FAQ)
The blog and FAQ are managed in Strapi v5 (cms/) and pulled at build time. End-to-end flow:
- Writer publishes / updates an entry in
https://cms.phormian.fr/admin. - Strapi fires a webhook to
https://hooks.phormian.fr/rebuild(the Node listener inwebhook/). - The listener
git pulls, runsnode build.mjs(which re-fetches Strapi and pre-renders blocks to HTML), then deploys. - Site updates live ~1–2 minutes after publish.
All three components — site, Strapi, webhook — run on the same VM. Schemas live in cms/src/api/*/content-types/*/schema.json and are version-controlled.
The Strapi blocks renderer (@strapi/blocks-react-renderer) is a build-time only dependency in build/. No rendering code ships to the browser; article bodies and FAQ answers ship as pre-baked HTML.
i18n is enabled per field with fr as the default locale. To wire English: set STRAPI_LOCALE and add the locale to cms/src/admin/app.tsx. (The current build only fetches the configured locale.)
What was dropped vs the original
image-slot.js— Claude-design upload widget, no production use.- Babel-in-browser via
@babel/standalone— moved to a build step. - The React client runtime + full-tree hydration — replaced by vanilla
islands.js. Pages ship as plain HTML. - The phone mockup's transaction-feed animation — ambient motion, not an interaction.
- Inline base64 photos — extracted to real files.
scraps/anduploads/— design-mode artifacts, not deployed.