No description
  • JavaScript 88.2%
  • CSS 6.5%
  • HTML 4.1%
  • TypeScript 1.2%
Find a file
Nicolas Perraut e7475ac4da
fix(css): collapse ROI simulator to 1-col on mobile
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>
2026-06-22 18:35:49 +02:00
assets feat(go): use real brand logos from local assets 2026-06-02 09:58:26 +02:00
build feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
cms Import from AI 2026-05-27 17:25:25 +02:00
legal/fr Import from AI 2026-05-27 17:25:25 +02:00
webhook Import from AI 2026-05-27 17:25:25 +02:00
.gitignore feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
.htaccess Import from AI 2026-05-27 17:25:25 +02:00
.image-slots.state.json Import from AI 2026-05-27 17:25:25 +02:00
.thumbnail Import from AI 2026-05-27 17:25:25 +02:00
app.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
blog-page.jsx Import from AI 2026-05-27 17:25:25 +02:00
blog-post-page.jsx Import from AI 2026-05-27 17:25:25 +02:00
demo-page.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
faq-page.jsx Import from AI 2026-05-27 17:25:25 +02:00
favicon.svg chore: add SVG favicon 2026-06-16 13:52:00 +02:00
founder-photos.js Import from AI 2026-05-27 17:25:25 +02:00
go-page.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
home-sections.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
image-slot.js Import from AI 2026-05-27 17:25:25 +02:00
index-print.html feat(go): add Phormian Go B2C landing page 2026-06-02 09:26:47 +02:00
index.html feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
islands.js feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
logo.jsx feat(go): add Phormian Go B2C landing page 2026-06-02 09:26:47 +02:00
mentions-legales-page.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
pages.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
phone.jsx Import from AI 2026-05-27 17:25:25 +02:00
phormian-logo.svg Import from AI 2026-05-27 17:25:25 +02:00
privacy-page.jsx docs: revise privacy policy and add versement/fraude retention rows 2026-06-16 13:52:30 +02:00
README.md docs: update README 2026-06-16 13:52:30 +02:00
site-shell.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
styles.css fix(css): collapse ROI simulator to 1-col on mobile 2026-06-22 18:35:49 +02:00
support-page.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00
team-page.jsx feat: static SSR build with progressive-enhancement islands 2026-06-16 13:52:21 +02:00

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

  1. Reads all .jsx files, concatenates them in dependency order.
  2. Prepends a global hrefFor(page) helper (a route-table lookup) so components emit real <a href> links for static navigation.
  3. Patches team-page.jsx to use plain <img> instead of the <image-slot> custom element (the custom element was a Claude-design upload tool, dropped from production).
  4. Decodes every base64 portrait in founder-photos.js into a real .jpg under dist/assets/.
  5. Compiles JSX once with @babel/preset-react.
  6. Renders each route to HTML with ReactDOMServer.renderToString against a minimal window / document shim.
  7. Emits one HTML file per route, wrapping the SSR markup in a shell that loads only styles.css and islands.js (no React, no client bundle).
  8. Copies islands.js into dist/.

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 at https://cms.phormian.fr/admin. Publishing fires a webhook that rebuilds + redeploys the site automatically — see webhook/README.md and cms/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

  1. Add the route to STATIC_ROUTES in build/build.mjs (page key + URL + output path).
  2. Add a title in STATIC_TITLES.
  3. Add the matching page === '…' branch in app.jsx.
  4. Mirror the new page → URL entry in PATHS in islands.js (used by [data-nav] buttons).
  5. Add nav / footer links (<a href={hrefFor('…')}> for links, data-nav="…" for buttons).
  6. 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

  1. Drop the JPG in assets/ (any name).
  2. Either reference it directly with photo: 'assets/foo.jpg' in team-page.jsx, or add it to founder-photos.js as a base64 data URI — the build will extract it on the next run.

Deploy

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.js error, and confirm the markup still has the data-* 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.mjs output for the failing route and stack trace.
  • A nav link goes to / unexpectedly. The page key isn't in the route table. Add it to STATIC_ROUTES (build.mjs) and to PATHS (islands.js).
  • A route 404s in production. The host isn't serving directory index.html. Use one of the configs above, or copy dist/index.html to dist/<route>.html.
  • Photos missing. Confirm dist/assets/photo-*.jpg exists. Re-run the build (portraits are extracted from founder-photos.js and 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:

  1. Writer publishes / updates an entry in https://cms.phormian.fr/admin.
  2. Strapi fires a webhook to https://hooks.phormian.fr/rebuild (the Node listener in webhook/).
  3. The listener git pulls, runs node build.mjs (which re-fetches Strapi and pre-renders blocks to HTML), then deploys.
  4. Site updates live ~12 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/ and uploads/ — design-mode artifacts, not deployed.