Compare commits
10 commits
eb5ae1fea7
...
18691d930e
| Author | SHA1 | Date | |
|---|---|---|---|
| 18691d930e | |||
| 106c04b91a | |||
| 1183f140a1 | |||
| 789f85609d | |||
| 0788de3c05 | |||
| c231016016 | |||
| b1d66a9be8 | |||
| 197741086b | |||
| de8404873f | |||
| 058ebe09c2 |
11 changed files with 121 additions and 64 deletions
14
.env.example
14
.env.example
|
|
@ -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=
|
||||||
|
|
|
||||||
68
README.md
68
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { onRequest } from '@storyblok/astro/middleware.ts';
|
|
||||||
|
|
@ -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
36
src/pages/api/rebuild.ts
Normal 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 });
|
||||||
|
};
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue