feat: switch to SSR for live Sanity updates

Migrate from SSG to SSR with @astrojs/node adapter so Sanity CMS
changes are reflected immediately without rebuild. Separate ports
for Astro SSR (4321) and Fastify API (3000) in production.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-04-04 11:41:34 +02:00
parent 7c77ac6c19
commit 1b53e04b5d
8 changed files with 191 additions and 59 deletions

View File

@ -37,7 +37,7 @@ rebours/
| Couche | Techno | | Couche | Techno |
|--------|--------| |--------|--------|
| Front (SSG) | Astro + HTML/CSS/JS vanilla + GSAP | | Front (SSR) | Astro + HTML/CSS/JS vanilla + GSAP |
| CMS | Sanity (headless, hébergé) | | CMS | Sanity (headless, hébergé) |
| API | Fastify (Node.js) | | API | Fastify (Node.js) |
| Paiement | Stripe Checkout (price_data inline) | | Paiement | Stripe Checkout (price_data inline) |
@ -65,20 +65,21 @@ STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_WEBHOOK_SECRET=whsec_...
DOMAIN=http://localhost:4321 DOMAIN=http://localhost:4321
PORT=8888 FASTIFY_PORT=3000 # Port Fastify API (prod)
ASTRO_PORT=4321 # Port Astro SSR (prod)
``` ```
### Lancer le projet ### Lancer le projet
```bash ```bash
npm install pnpm install
npm run dev pnpm dev
``` ```
Cela lance en parallèle (via `concurrently`) : Cela lance en parallèle (via `concurrently`) :
- `astro dev` sur http://localhost:4321 - `astro dev` sur http://localhost:4321
- `node --watch server.mjs` (mode dev, PORT=8888) - `node --watch server.mjs` (mode dev)
Le proxy Vite dans `astro.config.mjs` redirige `/api/*` vers `http://127.0.0.1:8888`. Le proxy Vite dans `astro.config.mjs` redirige `/api/*` vers le serveur Fastify.
### Sanity Studio ### Sanity Studio
```bash ```bash
@ -90,8 +91,8 @@ Accessible sur http://localhost:3333
### Build ### Build
```bash ```bash
npm run build pnpm build
# Génère ./dist/ (fichiers statiques Astro) # Génère ./dist/ (serveur Astro SSR + assets client)
``` ```
--- ---
@ -116,8 +117,7 @@ Champs principaux :
1. Ouvrir Sanity Studio 1. Ouvrir Sanity Studio
2. Créer un nouveau document "Produit" 2. Créer un nouveau document "Produit"
3. Remplir les champs, uploader l'image 3. Remplir les champs, uploader l'image
4. Publier 4. Publier → visible immédiatement sur le site (SSR, pas de rebuild nécessaire)
5. Rebuild le site : `npm run build` + déployer
### Images ### Images
Les images sont servies via le CDN Sanity avec transformations automatiques. Les images sont servies via le CDN Sanity avec transformations automatiques.
@ -154,12 +154,19 @@ Quand un client clique "Commander" :
### Serveur : ordinarthur@10.10.0.13 ### Serveur : ordinarthur@10.10.0.13
### Architecture prod ### Architecture prod (SSR)
``` ```
Internet -> Nginx (port 80) -> /var/www/html/rebours/dist/ (statiques) Internet -> Nginx (port 80) -> / -> proxy -> Astro SSR :4321
-> /api/* -> proxy -> Fastify :3000 -> /_astro/* -> fichiers statiques (dist/client/)
-> /api/* -> proxy -> Fastify :3000
``` ```
### Services systemd
| Service | Port | Rôle |
|---------|------|------|
| `rebours-ssr` | 4321 | Astro SSR (pages dynamiques) |
| `rebours` | 3000 | Fastify API (Stripe, checkout) |
### Variables d'environnement en prod ### Variables d'environnement en prod
```env ```env
SANITY_PROJECT_ID=... SANITY_PROJECT_ID=...
@ -167,14 +174,15 @@ SANITY_DATASET=production
STRIPE_SECRET_KEY=sk_live_... STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_WEBHOOK_SECRET=whsec_...
DOMAIN=https://rebours.studio DOMAIN=https://rebours.studio
PORT=3000 FASTIFY_PORT=3000
ASTRO_PORT=4321
``` ```
### Déploiement ### Déploiement
```bash ```bash
npm run build pnpm build
scp -r dist/* ordinarthur@10.10.0.13:/tmp/rebours-dist/ scp -r dist/* ordinarthur@10.10.0.13:/tmp/rebours-dist/
ssh ordinarthur@10.10.0.13 "sudo cp -r /tmp/rebours-dist/* /var/www/html/rebours/dist/" ssh ordinarthur@10.10.0.13 "sudo rm -rf /var/www/html/rebours/dist && sudo mkdir -p /var/www/html/rebours/dist && sudo cp -r /tmp/rebours-dist/* /var/www/html/rebours/dist/ && sudo chown -R ordinarthur:ordinarthur /var/www/html/rebours/dist && sudo systemctl restart rebours-ssr"
``` ```
Si server.mjs a changé : Si server.mjs a changé :
@ -189,8 +197,8 @@ ssh ordinarthur@10.10.0.13 "sudo cp /tmp/server.mjs /var/www/html/rebours/server
| URL | Comportement | | URL | Comportement |
|-----|-------------| |-----|-------------|
| `/` | Page principale Astro (SSG) | | `/` | Page principale Astro (SSR, données Sanity live) |
| `/collection/{slug}` | Page produit (SSG), auto-open panel via `window.__OPEN_PANEL__` | | `/collection/{slug}` | Page produit (SSR), auto-open panel via `window.__OPEN_PANEL__` |
| `/success?session_id=...` | Page de confirmation Stripe | | `/success?session_id=...` | Page de confirmation Stripe |
| `/robots.txt` | Généré au build | | `/robots.txt` | Généré au build |
| `/sitemap.xml` | Généré au build depuis Sanity | | `/sitemap.xml` | Généré au build depuis Sanity |

View File

@ -1,8 +1,10 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({ export default defineConfig({
output: 'static', output: 'server',
adapter: node({ mode: 'standalone' }),
outDir: './dist', outDir: './dist',
server: { port: 4321 }, server: { port: 4321 },
vite: { vite: {

View File

@ -2,10 +2,7 @@ server {
listen 80; listen 80;
server_name rebours.studio; server_name rebours.studio;
root /var/www/html/rebours/dist; # ── API proxy Fastify ────────────────<EFBFBD><EFBFBD><EFBFBD>─────────────────────────────────
index index.html;
# ── API proxy Fastify ──────────────────────────────────────────────────
location /api/ { location /api/ {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host; proxy_set_header Host $host;
@ -14,28 +11,23 @@ server {
proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Proto https;
} }
# ── Cache : Astro hashed immutable ───────────────────────────────────── # ── Static assets from Astro client build ──────────────────<EFBFBD><EFBFBD><EFBFBD>─────────────
location /_astro/ { location /_astro/ {
root /var/www/html/rebours/dist/client;
add_header Cache-Control "public, max-age=31536000, immutable"; add_header Cache-Control "public, max-age=31536000, immutable";
} }
# ── Cache : CSS/JS sans hash revalidation ───────────────────────────── location ~* \.(css|js|jpg|jpeg|png|gif|webp|svg|woff2|woff|ttf|ico|mp3)$ {
location ~* \.(css|js)$ { root /var/www/html/rebours/dist/client;
add_header Cache-Control "no-cache";
}
# ── Cache : assets 7 jours ────────────────────────────────────────────
location ~* \.(jpg|jpeg|png|gif|webp|svg|woff2|woff|ttf|ico|mp3)$ {
add_header Cache-Control "public, max-age=604800"; add_header Cache-Control "public, max-age=604800";
} }
# ── HTML : jamais caché ────────────────────────────────────────────────── # ── SSR Astro Node server ─────────────────────<EFBFBD><EFBFBD><EFBFBD>────────────────────────
location ~* \.html$ {
add_header Cache-Control "no-store";
}
# ── SPA fallback ─────────────────────────────────────────────────────────
location / { location / {
try_files $uri $uri/ $uri.html /index.html; proxy_pass http://127.0.0.1:4321;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
} }
} }

View File

@ -7,9 +7,11 @@
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"server": "NODE_ENV=production node server.mjs", "server": "NODE_ENV=production node server.mjs",
"start": "NODE_ENV=production node dist/server/entry.mjs",
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^9.5.5",
"@fastify/cors": "^10.0.2", "@fastify/cors": "^10.0.2",
"@sanity/client": "^7", "@sanity/client": "^7",
"@sanity/image-url": "^1", "@sanity/image-url": "^1",

133
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@astrojs/node':
specifier: ^9.5.5
version: 9.5.5(astro@5.18.1(@types/node@25.4.0)(jiti@2.6.1)(rollup@4.59.0)(typescript@5.9.3))
'@fastify/cors': '@fastify/cors':
specifier: ^10.0.2 specifier: ^10.0.2
version: 10.1.0 version: 10.1.0
@ -47,6 +50,11 @@ packages:
'@astrojs/markdown-remark@6.3.11': '@astrojs/markdown-remark@6.3.11':
resolution: {integrity: sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ==} resolution: {integrity: sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ==}
'@astrojs/node@9.5.5':
resolution: {integrity: sha512-rtU2BGU5u3SfGURpANfMxVzCIoR86MkaN05ncza9rbtuMKJ/XnRJt/BbyVknDbOJ71hoci0SIsJwKcJR8vvi/A==}
peerDependencies:
astro: ^5.17.3
'@astrojs/prism@3.3.0': '@astrojs/prism@3.3.0':
resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==}
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
@ -989,6 +997,10 @@ packages:
defu@6.1.4: defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dequal@2.0.3: dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1038,12 +1050,19 @@ packages:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'} engines: {node: '>=4'}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
emoji-regex@10.6.0: emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
entities@4.5.0: entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@ -1069,6 +1088,9 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'} engines: {node: '>=6'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
escape-string-regexp@5.0.0: escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1079,6 +1101,10 @@ packages:
estree-walker@3.0.3: estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
event-source-polyfill@1.0.31: event-source-polyfill@1.0.31:
resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==} resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==}
@ -1149,6 +1175,10 @@ packages:
resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==}
engines: {node: '>=20'} engines: {node: '>=20'}
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -1218,6 +1248,10 @@ packages:
http-cache-semantics@4.2.0: http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
import-meta-resolve@4.2.0: import-meta-resolve@4.2.0:
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
@ -1423,6 +1457,14 @@ packages:
micromark@4.0.2: micromark@4.0.2:
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
mimic-response@3.1.0: mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1475,6 +1517,10 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
oniguruma-parser@0.12.1: oniguruma-parser@0.12.1:
resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
@ -1553,6 +1599,10 @@ packages:
radix3@1.1.2: radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
readable-stream@3.6.2: readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -1663,9 +1713,19 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
send@1.2.1:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
server-destroy@1.0.1:
resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
set-cookie-parser@2.7.2: set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.34.5: sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -1698,6 +1758,10 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'} engines: {node: '>= 10.x'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
string-width@4.2.3: string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1764,6 +1828,10 @@ packages:
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
engines: {node: '>=12'} engines: {node: '>=12'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
tree-kill@1.2.2: tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true hasBin: true
@ -2061,6 +2129,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@astrojs/node@9.5.5(astro@5.18.1(@types/node@25.4.0)(jiti@2.6.1)(rollup@4.59.0)(typescript@5.9.3))':
dependencies:
'@astrojs/internal-helpers': 0.7.6
astro: 5.18.1(@types/node@25.4.0)(jiti@2.6.1)(rollup@4.59.0)(typescript@5.9.3)
send: 1.2.1
server-destroy: 1.0.1
transitivePeerDependencies:
- supports-color
'@astrojs/prism@3.3.0': '@astrojs/prism@3.3.0':
dependencies: dependencies:
prismjs: 1.30.0 prismjs: 1.30.0
@ -2826,6 +2903,8 @@ snapshots:
defu@6.1.4: {} defu@6.1.4: {}
depd@2.0.0: {}
dequal@2.0.3: {} dequal@2.0.3: {}
destr@2.0.5: {} destr@2.0.5: {}
@ -2869,10 +2948,14 @@ snapshots:
dset@3.1.4: {} dset@3.1.4: {}
ee-first@1.1.1: {}
emoji-regex@10.6.0: {} emoji-regex@10.6.0: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
encodeurl@2.0.0: {}
entities@4.5.0: {} entities@4.5.0: {}
entities@6.0.1: {} entities@6.0.1: {}
@ -2939,6 +3022,8 @@ snapshots:
escalade@3.2.0: {} escalade@3.2.0: {}
escape-html@1.0.3: {}
escape-string-regexp@5.0.0: {} escape-string-regexp@5.0.0: {}
estree-walker@2.0.2: {} estree-walker@2.0.2: {}
@ -2947,6 +3032,8 @@ snapshots:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
etag@1.8.1: {}
event-source-polyfill@1.0.31: {} event-source-polyfill@1.0.31: {}
eventemitter3@5.0.4: {} eventemitter3@5.0.4: {}
@ -3020,6 +3107,8 @@ snapshots:
dependencies: dependencies:
tiny-inflate: 1.0.3 tiny-inflate: 1.0.3
fresh@2.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
@ -3149,6 +3238,14 @@ snapshots:
http-cache-semantics@4.2.0: {} http-cache-semantics@4.2.0: {}
http-errors@2.0.1:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.2
toidentifier: 1.0.1
import-meta-resolve@4.2.0: {} import-meta-resolve@4.2.0: {}
inherits@2.0.4: {} inherits@2.0.4: {}
@ -3525,6 +3622,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
mime-db@1.54.0: {}
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
mimic-response@3.1.0: {} mimic-response@3.1.0: {}
mnemonist@0.40.0: mnemonist@0.40.0:
@ -3565,6 +3668,10 @@ snapshots:
on-exit-leak-free@2.1.2: {} on-exit-leak-free@2.1.2: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
oniguruma-parser@0.12.1: {} oniguruma-parser@0.12.1: {}
oniguruma-to-es@4.3.4: oniguruma-to-es@4.3.4:
@ -3650,6 +3757,8 @@ snapshots:
radix3@1.1.2: {} radix3@1.1.2: {}
range-parser@1.2.1: {}
readable-stream@3.6.2: readable-stream@3.6.2:
dependencies: dependencies:
inherits: 2.0.4 inherits: 2.0.4
@ -3820,8 +3929,28 @@ snapshots:
semver@7.7.4: {} semver@7.7.4: {}
send@1.2.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.1
mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
server-destroy@1.0.1: {}
set-cookie-parser@2.7.2: {} set-cookie-parser@2.7.2: {}
setprototypeof@1.2.0: {}
sharp@0.34.5: sharp@0.34.5:
dependencies: dependencies:
'@img/colour': 1.1.0 '@img/colour': 1.1.0
@ -3881,6 +4010,8 @@ snapshots:
split2@4.2.0: {} split2@4.2.0: {}
statuses@2.0.2: {}
string-width@4.2.3: string-width@4.2.3:
dependencies: dependencies:
emoji-regex: 8.0.0 emoji-regex: 8.0.0
@ -3951,6 +4082,8 @@ snapshots:
toad-cache@3.7.0: {} toad-cache@3.7.0: {}
toidentifier@1.0.1: {}
tree-kill@1.2.2: {} tree-kill@1.2.2: {}
trim-lines@3.0.1: {} trim-lines@3.0.1: {}

View File

@ -131,7 +131,7 @@ app.get('/api/session/:id', async (request) => {
// ── Start ─────────────────────────────────────────────────────────────────── // ── Start ───────────────────────────────────────────────────────────────────
try { try {
await app.listen({ port: process.env.PORT ?? 3000, host: '0.0.0.0' }) await app.listen({ port: process.env.FASTIFY_PORT ?? process.env.PORT ?? 3000, host: '0.0.0.0' })
} catch (err) { } catch (err) {
app.log.error(err) app.log.error(err)
process.exit(1) process.exit(1)

View File

@ -5,7 +5,7 @@ export const sanity = createClient({
projectId: import.meta.env.SANITY_PROJECT_ID, projectId: import.meta.env.SANITY_PROJECT_ID,
dataset: import.meta.env.SANITY_DATASET || 'production', dataset: import.meta.env.SANITY_DATASET || 'production',
apiVersion: '2024-01-01', apiVersion: '2024-01-01',
useCdn: true, useCdn: false,
}) })
const builder = imageUrlBuilder(sanity) const builder = imageUrlBuilder(sanity)

View File

@ -1,26 +1,21 @@
--- ---
import Base from '../../layouts/Base.astro'; import Base from '../../layouts/Base.astro';
import { getPublishedProducts, urlFor } from '../../lib/sanity.mjs'; import { getPublishedProducts, getProductBySlug, urlFor } from '../../lib/sanity.mjs';
export async function getStaticPaths() { const { slug } = Astro.params;
const products = await getPublishedProducts(); const product = await getProductBySlug(slug);
return products.map(p => ({ if (!product) {
params: { slug: p.slug }, return Astro.redirect('/');
props: {
slug: p.slug,
name: p.name,
title: p.seoTitle || `REBOURS — ${p.productDisplayName} | Collection 001`,
description: p.seoDescription || p.description?.substring(0, 155) || '',
ogImage: p.image ? urlFor(p.image).width(1200).url() : '',
productName: p.productDisplayName,
price: p.price ? String(p.price / 100) : null,
availability: p.availability || 'https://schema.org/PreOrder',
},
}));
} }
const { slug, title, description, ogImage, name, productName, price, availability } = Astro.props; const name = product.name;
const title = product.seoTitle || `REBOURS — ${product.productDisplayName} | Collection 001`;
const description = product.seoDescription || product.description?.substring(0, 155) || '';
const ogImage = product.image ? urlFor(product.image).width(1200).url() : '';
const productName = product.productDisplayName;
const price = product.price ? String(product.price / 100) : null;
const availability = product.availability || 'https://schema.org/PreOrder';
const allProducts = await getPublishedProducts(); const allProducts = await getPublishedProducts();
@ -109,11 +104,11 @@ const schemaBreadcrumb = {
<p id="panel-desc" class="panel-desc"></p> <p id="panel-desc" class="panel-desc"></p>
<hr> <hr>
<details class="accordion"> <details class="accordion" open>
<summary>SPÉCIFICATIONS TECHNIQUES <span>↓</span></summary> <summary>SPÉCIFICATIONS TECHNIQUES <span>↓</span></summary>
<div class="accordion-body" id="panel-specs"></div> <div class="accordion-body" id="panel-specs"></div>
</details> </details>
<details class="accordion"> <details class="accordion" open>
<summary>NOTES DE CONCEPTION <span>↓</span></summary> <summary>NOTES DE CONCEPTION <span>↓</span></summary>
<div class="accordion-body" id="panel-notes"></div> <div class="accordion-body" id="panel-notes"></div>
</details> </details>