No description
Find a file
Nicolas Perraut 2d3973b7bb
feat(go): use real brand logos from local assets
Replace first-letter placeholders on the /go page with real brand
logos. Images downloaded from logo.dev into assets/logos/ and served
from there (no runtime token, no third-party request). Colored-letter
chips remain as onError fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:58:26 +02:00
assets feat(go): use real brand logos from local assets 2026-06-02 09:58:26 +02:00
build feat(go): add Phormian Go B2C landing page 2026-06-02 09:26:47 +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 Import from AI 2026-05-27 17:25:25 +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(go): add Phormian Go B2C landing page 2026-06-02 09:26:47 +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 Import from AI 2026-05-27 17:25:25 +02:00
faq-page.jsx Import from AI 2026-05-27 17:25:25 +02:00
founder-photos.js Import from AI 2026-05-27 17:25:25 +02:00
go-page.jsx feat(go): use real brand logos from local assets 2026-06-02 09:58:26 +02:00
home-sections.jsx feat(go): add Phormian Go B2C landing page 2026-06-02 09:26:47 +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(go): add Phormian Go B2C landing page 2026-06-02 09:26:47 +02:00
logo.jsx feat(go): add Phormian Go B2C landing page 2026-06-02 09:26:47 +02:00
mentions-legales-page.jsx Import from AI 2026-05-27 17:25:25 +02:00
pages.jsx fix(fonctionnement): stack step cards on mobile 2026-06-02 09:31:34 +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 Import from AI 2026-05-27 17:25:25 +02:00
README.md Import from AI 2026-05-27 17:25:25 +02:00
site-shell.jsx feat(home): remove hero kicker 2026-06-02 09:31:39 +02:00
styles.css fix(fonctionnement): stack step cards on mobile 2026-06-02 09:31:34 +02:00
team-page.jsx Import from AI 2026-05-27 17:25:25 +02:00

Phormian landing site

Static site generated from the JSX sources in this directory. React renders the markup at build time (SSR) and hydrates the same tree in the browser, so every route ships pre-rendered HTML and stays interactive (persona switcher, ROI simulator, demo form, phone animation).

Layout

.
├── *.jsx, *.js          ← source components (edit these)
├── 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 + bundle + photo extraction + Strapi fetch
│   └── 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
├── bundle.js                                   ← React app, minified
├── founder-photos.js                           ← slim photo lookup
├── styles.css, phormian-logo.svg, .htaccess
└── assets/                                     ← portrait jpgs

What the build does

  1. Reads all .jsx files, concatenates them in dependency order.
  2. Patches app.jsx so every page (not only the legal ones) gets a real URL.
  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. Minifies the client bundle with Terser.
  8. Emits one HTML file per route, wrapping the SSR markup in a shell that loads React UMD + the bundle.

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 demo-page.jsx
Blog list / post layout blog-page.jsx, blog-post-page.jsx
FAQ layout faq-page.jsx
Blog / FAQ content Strapi admin (cms/)
Footer, routing table app.jsx
Styles styles.css

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. Add nav / footer links as needed.
  5. 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

  • Hydration warning in console. Likely a value that differs between SSR and the browser. Common culprit: code that reads Date.now(), Math.random(), or window.location at render time. Fix by reading those inside a useEffect.
  • A page renders blank. Check the browser console — usually a JS error in the bundle. Re-run the build without minification by commenting out the Terser block in build.mjs to get a readable stack trace.
  • 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 and dist/founder-photos.js references them. Re-run the build.

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/. The client bundle contains no rendering code; 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.
  • Inline base64 photos — extracted to real files.
  • scraps/ and uploads/ — design-mode artifacts, not deployed.