Compare commits

..

No commits in common. "18691d930ebdac57b270d7202b5e8c24aee4002e" and "eb5ae1fea791af2cdde675c971b90b48cd97f4f1" have entirely different histories.

11 changed files with 64 additions and 121 deletions

View file

@ -1,16 +1,6 @@
# Token StoryBlok — Preview token en dev/preview, Public token en production
PUBLIC_STORYBLOK_TOKEN=
STORYBLOK_TOKEN=
# Mettre à true sur l'instance preview (SSR + visual editor StoryBlok)
# Ne pas définir ou mettre à false en production (SSG)
# 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=
# STORYBLOK_IS_PREVIEW=true

View file

@ -1,62 +1,20 @@
# Compagnie AspiRêves
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
Site web de la Compagnie AspiRêves, compagnie de spectacle vivant basée dans le Tarn.
# Run and deploy your AI Studio app
Construit avec [Astro](https://astro.build/) et [StoryBlok](https://www.storyblok.com/) comme CMS headless.
This contains everything you need to run your app locally.
## Prérequis
View your app in AI Studio: https://ai.studio/apps/888a76b0-af1d-4274-9218-1817cdc461fb
- Node.js >= 20
- Un fichier `.env` (voir `.env.example`)
## Run Locally
## Développement
**Prerequisites:** Node.js
```bash
npm install
npm run dev
```
Le serveur démarre sur `http://localhost:3030`.
## Builds
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
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

View file

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

View file

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

View file

@ -26,5 +26,22 @@ const { title = 'Compagnie AspiRêves' } = Astro.props;
<Footer />
<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>
</html>

View file

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

1
src/middleware.ts Normal file
View file

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

View file

@ -17,6 +17,10 @@ const now = new Date();
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<!-- Header -->
<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>
<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.

View file

@ -1,36 +0,0 @@
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,6 +14,9 @@ import { companyInfo } from '../data';
<!-- Header -->
<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>
<p class="font-sans text-night/60 max-w-2xl mx-auto text-lg md:text-xl leading-relaxed">
{companyInfo.description}
</p>
</div>
<!-- Présentation -->

View file

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