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> |
||
|---|---|---|
| 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 | ||
| founder-photos.js | ||
| go-page.jsx | ||
| home-sections.jsx | ||
| image-slot.js | ||
| index-print.html | ||
| index.html | ||
| logo.jsx | ||
| mentions-legales-page.jsx | ||
| pages.jsx | ||
| phone.jsx | ||
| phormian-logo.svg | ||
| privacy-page.jsx | ||
| README.md | ||
| site-shell.jsx | ||
| styles.css | ||
| 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 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
- Reads all
.jsxfiles, concatenates them in dependency order. - Patches
app.jsxso every page (not only the legal ones) gets a real URL. - 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. - Minifies the client bundle with Terser.
- 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 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 | 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
- 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. - Add nav / footer links as needed.
- 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
- Hydration warning in console. Likely a value that differs between SSR and the browser. Common culprit: code that reads
Date.now(),Math.random(), orwindow.locationat render time. Fix by reading those inside auseEffect. - 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.mjsto 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 copydist/index.htmltodist/<route>.html. - Photos missing. Confirm
dist/assets/photo-*.jpgexists anddist/founder-photos.jsreferences 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:
- 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/. 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/anduploads/— design-mode artifacts, not deployed.