Compare commits

..

10 commits

Author SHA1 Message Date
18691d930e Utiliser le chemin /organisations/{orgaId}/ pour l'API Clever Cloud 2026-03-07 22:37:36 +01:00
106c04b91a Utiliser api-bridge.clever-cloud.com pour l'authentification Bearer 2026-03-07 22:11:08 +01:00
1183f140a1 Vérifier la signature HMAC du webhook StoryBlok avant de déclencher le rebuild 2026-03-07 22:03:01 +01:00
789f85609d Ajouter un endpoint /api/rebuild pour déclencher le redéploiement production via webhook StoryBlok 2026-03-07 20:00:57 +01:00
0788de3c05 Remplacer les animations JS (IntersectionObserver) par des animations CSS pures
Les animations fade-up/fade-right/fade-scale utilisaient un IntersectionObserver pour ajouter une classe .visible au scroll. Après un remplacement du DOM par le live preview StoryBlok, les nouveaux éléments n'étaient jamais observés et restaient invisibles (opacity: 0). Remplacé par des @keyframes CSS qui s'exécutent sans JS. Supprimé le middleware manuel (auto-enregistré par l'intégration @storyblok/astro quand livePreview est activé).
2026-03-07 01:49:47 +01:00
c231016016 Préfixer les variables StoryBlok avec PUBLIC_ pour les exposer dans import.meta.env
Les variables système (Clever Cloud) n'étaient pas disponibles via import.meta.env car Vite ne les inclut que pour les variables préfixées PUBLIC_. Sans ce préfixe, getVersion() était compilé à "published" même sur l'instance preview, empêchant le live preview du visual editor StoryBlok.
2026-03-07 01:27:43 +01:00
b1d66a9be8 Supprimer l'étiquette « Calendrier » de la page agenda 2026-03-06 09:37:12 +01:00
197741086b Retirer le paragraphe de description de la page « La Compagnie » 2026-03-05 23:05:45 +01:00
de8404873f Documenter les modes de build SSG/SSR et la configuration StoryBlok 2026-03-05 23:04:27 +01:00
058ebe09c2 Écouter sur 0.0.0.0 pour Clever Cloud 2026-03-05 23:00:47 +01:00
11 changed files with 121 additions and 64 deletions

View file

@ -1,6 +1,16 @@
# Token StoryBlok — Preview token en dev/preview, Public token en production # Token StoryBlok — Preview token en dev/preview, Public token en production
STORYBLOK_TOKEN= PUBLIC_STORYBLOK_TOKEN=
# Mettre à true sur l'instance preview (SSR + visual editor StoryBlok) # Mettre à true sur l'instance preview (SSR + visual editor StoryBlok)
# Ne pas définir ou mettre à false en production (SSG) # Ne pas définir ou mettre à false en production (SSG)
# STORYBLOK_IS_PREVIEW=true # PUBLIC_STORYBLOK_IS_PREVIEW=true
# Webhook rebuild — uniquement sur l'instance preview
# Secret partagé avec StoryBlok (Settings > Webhooks > Secret du Webhook)
# STORYBLOK_WEBHOOK_SECRET=
# Token OAuth Clever Cloud (généré via clever login puis ~/.config/clever-cloud/clever-tools.json)
# CLEVER_TOKEN=
# ID de l'organisation Clever Cloud (orga_xxxxxxxx)
# CLEVER_ORGA_ID=
# ID de l'application Clever Cloud de production (app_xxxxxxxx)
# CLEVER_APP_ID_PRODUCTION=

View file

@ -1,20 +1,62 @@
<div align="center"> # Compagnie AspiRêves
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app Site web de la Compagnie AspiRêves, compagnie de spectacle vivant basée dans le Tarn.
This contains everything you need to run your app locally. Construit avec [Astro](https://astro.build/) et [StoryBlok](https://www.storyblok.com/) comme CMS headless.
View your app in AI Studio: https://ai.studio/apps/888a76b0-af1d-4274-9218-1817cdc461fb ## Prérequis
## Run Locally - Node.js >= 20
- Un fichier `.env` (voir `.env.example`)
**Prerequisites:** Node.js ## Développement
```bash
npm install
npm run dev
```
1. Install dependencies: Le serveur démarre sur `http://localhost:3030`.
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key ## Builds
3. Run the app:
`npm run dev` Le projet supporte deux modes de build via la variable `PUBLIC_STORYBLOK_IS_PREVIEW` :
### Production (SSG)
Site statique, performant, sans serveur Node.js. Utilise le **Public Access Token** StoryBlok et ne récupère que le contenu **publié**.
```bash
npm run build
```
Génère des fichiers HTML statiques dans `dist/`. Le rebuild doit être déclenché par un **webhook StoryBlok** (Settings > Webhooks) à chaque publication de contenu.
### Preview (SSR)
Serveur Node.js avec le **visual editor StoryBlok** (bridge + live preview). Utilise le **Preview Access Token** et récupère le contenu en **draft**.
```bash
PUBLIC_STORYBLOK_IS_PREVIEW=true npm run build
HOST=0.0.0.0 node dist/server/entry.mjs
```
Le serveur démarre sur le port `8080` par défaut (configurable via `PORT`).
### Variables d'environnement
| Variable | Production | Preview |
|---|---|---|
| `PUBLIC_STORYBLOK_TOKEN` | Public Access Token | Preview Access Token |
| `PUBLIC_STORYBLOK_IS_PREVIEW` | *(non défini)* | `true` |
| `STORYBLOK_WEBHOOK_SECRET` | - | Secret du webhook StoryBlok |
| `CLEVER_TOKEN` | - | Token OAuth Clever Cloud |
| `CLEVER_ORGA_ID` | - | ID de l'organisation (orga_xxx) |
| `CLEVER_APP_ID_PRODUCTION` | - | ID de l'app production (app_xxx) |
| `CC_POST_BUILD_HOOK` | `npm run build` | `npm run build` |
| `HOST` | - | `0.0.0.0` |
### Configuration StoryBlok
- **Settings > Visual Editor** : mettre l'URL de l'instance preview comme environnement par défaut
- **Settings > Webhooks** : configurer un webhook `POST` vers `https://<preview-instance>/api/rebuild` pour déclencher le rebuild production à chaque publication

View file

@ -6,8 +6,8 @@ import icon from 'astro-icon';
import node from '@astrojs/node'; import node from '@astrojs/node';
import { storyblok } from '@storyblok/astro'; import { storyblok } from '@storyblok/astro';
const env = loadEnv('', process.cwd(), 'STORYBLOK'); const env = loadEnv('', process.cwd(), 'PUBLIC_STORYBLOK');
const isPreview = env.STORYBLOK_IS_PREVIEW === 'true'; const isPreview = env.PUBLIC_STORYBLOK_IS_PREVIEW === 'true';
export default defineConfig({ export default defineConfig({
output: isPreview ? 'server' : 'static', output: isPreview ? 'server' : 'static',
@ -15,7 +15,7 @@ export default defineConfig({
integrations: [ integrations: [
icon(), icon(),
storyblok({ storyblok({
accessToken: env.STORYBLOK_TOKEN, accessToken: env.PUBLIC_STORYBLOK_TOKEN,
bridge: isPreview, bridge: isPreview,
livePreview: isPreview, livePreview: isPreview,
}), }),

View file

@ -7,7 +7,7 @@
"dev": "astro dev --port 3030 --host 0.0.0.0", "dev": "astro dev --port 3030 --host 0.0.0.0",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"start": "node dist/server/entry.mjs", "start": "HOST=0.0.0.0 node dist/server/entry.mjs",
"clean": "rm -rf dist" "clean": "rm -rf dist"
}, },
"dependencies": { "dependencies": {

View file

@ -26,22 +26,5 @@ const { title = 'Compagnie AspiRêves' } = Astro.props;
<Footer /> <Footer />
<ThemeSwitcher /> <ThemeSwitcher />
<script>
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: '-50px' }
);
document.querySelectorAll('.fade-up, .fade-right, .fade-scale').forEach((el) => {
observer.observe(el);
});
</script>
</body> </body>
</html> </html>

View file

@ -2,7 +2,7 @@ import { useStoryblokApi } from '@storyblok/astro';
import type { SbBlokData } from '@storyblok/astro'; import type { SbBlokData } from '@storyblok/astro';
function getVersion(): 'draft' | 'published' { function getVersion(): 'draft' | 'published' {
return import.meta.env.STORYBLOK_IS_PREVIEW === 'true' ? 'draft' : 'published'; return import.meta.env.PUBLIC_STORYBLOK_IS_PREVIEW === 'true' ? 'draft' : 'published';
} }
export interface Spectacle { export interface Spectacle {

View file

@ -1 +0,0 @@
export { onRequest } from '@storyblok/astro/middleware.ts';

View file

@ -17,10 +17,6 @@ const now = new Date();
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10"> <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<!-- Header --> <!-- Header -->
<div class="animate-entrance text-center mb-12 md:mb-20"> <div class="animate-entrance text-center mb-12 md:mb-20">
<div class="inline-flex items-center gap-2 mb-6 px-4 py-1 rounded-full bg-white/50 border border-white/20 text-dream-coral">
<Icon name="lucide:calendar" size={16} />
<span class="font-sans text-[10px] md:text-xs font-bold uppercase tracking-widest">Calendrier</span>
</div>
<h1 class="font-display text-4xl sm:text-6xl md:text-8xl text-night mb-8">L'<span class="text-dream-coral italic">Agenda</span></h1> <h1 class="font-display text-4xl sm:text-6xl md:text-8xl text-night mb-8">L'<span class="text-dream-coral italic">Agenda</span></h1>
<p class="font-sans text-night/60 max-w-2xl mx-auto text-lg md:text-xl leading-relaxed"> <p class="font-sans text-night/60 max-w-2xl mx-auto text-lg md:text-xl leading-relaxed">
Venez nous voir sur scène ! <br class="hidden sm:block" />Chaque date est une nouvelle aventure. Venez nous voir sur scène ! <br class="hidden sm:block" />Chaque date est une nouvelle aventure.

36
src/pages/api/rebuild.ts Normal file
View file

@ -0,0 +1,36 @@
import type { APIRoute } from 'astro';
import { createHmac, timingSafeEqual } from 'node:crypto';
export const POST: APIRoute = async ({ request }) => {
const webhookSecret = import.meta.env.STORYBLOK_WEBHOOK_SECRET;
const token = import.meta.env.CLEVER_TOKEN;
const orgaId = import.meta.env.CLEVER_ORGA_ID;
const appId = import.meta.env.CLEVER_APP_ID_PRODUCTION;
if (!webhookSecret || !token || !orgaId || !appId) {
return new Response('Missing server configuration', { status: 500 });
}
const body = await request.text();
const signature = request.headers.get('webhook-signature') ?? '';
const expected = createHmac('sha1', webhookSecret).update(body).digest('hex');
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return new Response('Invalid signature', { status: 401 });
}
const response = await fetch(
`https://api-bridge.clever-cloud.com/v2/organisations/${orgaId}/applications/${appId}/instances`,
{
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
},
);
if (!response.ok) {
const error = await response.text();
return new Response(`Clever Cloud API error: ${response.status} ${error}`, { status: 502 });
}
return new Response('Rebuild triggered', { status: 200 });
};

View file

@ -14,9 +14,6 @@ import { companyInfo } from '../data';
<!-- Header --> <!-- Header -->
<div class="animate-entrance text-center mb-16 md:mb-24"> <div class="animate-entrance text-center mb-16 md:mb-24">
<h1 class="font-display text-4xl sm:text-6xl md:text-8xl text-night mb-8 leading-tight">La <span class="text-dream-coral italic">Compagnie</span></h1> <h1 class="font-display text-4xl sm:text-6xl md:text-8xl text-night mb-8 leading-tight">La <span class="text-dream-coral italic">Compagnie</span></h1>
<p class="font-sans text-night/60 max-w-2xl mx-auto text-lg md:text-xl leading-relaxed">
{companyInfo.description}
</p>
</div> </div>
<!-- Présentation --> <!-- Présentation -->

View file

@ -54,39 +54,33 @@ body {
animation-delay: 4s; animation-delay: 4s;
} }
/* Scroll-triggered animations (Intersection Observer) */ /* Entrance animations */
.fade-up { .fade-up {
opacity: 0; animation: fadeUp 0.6s ease both;
transform: translateY(2rem);
transition: opacity 0.6s ease, transform 0.6s ease;
} }
.fade-up.visible { @keyframes fadeUp {
opacity: 1; from { opacity: 0; transform: translateY(2rem); }
transform: translateY(0); to { opacity: 1; transform: translateY(0); }
} }
.fade-right { .fade-right {
opacity: 0; animation: fadeRight 0.6s ease both;
transform: translateX(2rem);
transition: opacity 0.6s ease, transform 0.6s ease;
} }
.fade-right.visible { @keyframes fadeRight {
opacity: 1; from { opacity: 0; transform: translateX(2rem); }
transform: translateX(0); to { opacity: 1; transform: translateX(0); }
} }
.fade-scale { .fade-scale {
opacity: 0; animation: fadeScale 1s ease both;
transform: scale(0.9);
transition: opacity 1s ease, transform 1s ease;
} }
.fade-scale.visible { @keyframes fadeScale {
opacity: 1; from { opacity: 0; transform: scale(0.9); }
transform: scale(1); to { opacity: 1; transform: scale(1); }
} }
/* Page entrance animation */ /* Page entrance animation */