Compare commits
No commits in common. "18691d930ebdac57b270d7202b5e8c24aee4002e" and "eb5ae1fea791af2cdde675c971b90b48cd97f4f1" have entirely different histories.
18691d930e
...
eb5ae1fea7
11 changed files with 64 additions and 121 deletions
14
.env.example
14
.env.example
|
|
@ -1,16 +1,6 @@
|
||||||
# Token StoryBlok — Preview token en dev/preview, Public token en production
|
# 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)
|
# 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)
|
||||||
# PUBLIC_STORYBLOK_IS_PREVIEW=true
|
# 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,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
|
## Run Locally
|
||||||
- Un fichier `.env` (voir `.env.example`)
|
|
||||||
|
|
||||||
## Développement
|
**Prerequisites:** Node.js
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Le serveur démarre sur `http://localhost:3030`.
|
1. Install dependencies:
|
||||||
|
`npm install`
|
||||||
## Builds
|
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||||
|
3. Run the app:
|
||||||
Le projet supporte deux modes de build via la variable `PUBLIC_STORYBLOK_IS_PREVIEW` :
|
`npm run dev`
|
||||||
|
|
||||||
### 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(), 'PUBLIC_STORYBLOK');
|
const env = loadEnv('', process.cwd(), 'STORYBLOK');
|
||||||
const isPreview = env.PUBLIC_STORYBLOK_IS_PREVIEW === 'true';
|
const isPreview = env.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.PUBLIC_STORYBLOK_TOKEN,
|
accessToken: env.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": "HOST=0.0.0.0 node dist/server/entry.mjs",
|
"start": "node dist/server/entry.mjs",
|
||||||
"clean": "rm -rf dist"
|
"clean": "rm -rf dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,22 @@ 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.PUBLIC_STORYBLOK_IS_PREVIEW === 'true' ? 'draft' : 'published';
|
return import.meta.env.STORYBLOK_IS_PREVIEW === 'true' ? 'draft' : 'published';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Spectacle {
|
export interface Spectacle {
|
||||||
|
|
|
||||||
1
src/middleware.ts
Normal file
1
src/middleware.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { onRequest } from '@storyblok/astro/middleware.ts';
|
||||||
|
|
@ -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">
|
<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.
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
|
||||||
};
|
|
||||||
|
|
@ -14,6 +14,9 @@ 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,33 +54,39 @@ body {
|
||||||
animation-delay: 4s;
|
animation-delay: 4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Entrance animations */
|
/* Scroll-triggered animations (Intersection Observer) */
|
||||||
|
|
||||||
.fade-up {
|
.fade-up {
|
||||||
animation: fadeUp 0.6s ease both;
|
opacity: 0;
|
||||||
|
transform: translateY(2rem);
|
||||||
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeUp {
|
.fade-up.visible {
|
||||||
from { opacity: 0; transform: translateY(2rem); }
|
opacity: 1;
|
||||||
to { opacity: 1; transform: translateY(0); }
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-right {
|
.fade-right {
|
||||||
animation: fadeRight 0.6s ease both;
|
opacity: 0;
|
||||||
|
transform: translateX(2rem);
|
||||||
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeRight {
|
.fade-right.visible {
|
||||||
from { opacity: 0; transform: translateX(2rem); }
|
opacity: 1;
|
||||||
to { opacity: 1; transform: translateX(0); }
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-scale {
|
.fade-scale {
|
||||||
animation: fadeScale 1s ease both;
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transition: opacity 1s ease, transform 1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeScale {
|
.fade-scale.visible {
|
||||||
from { opacity: 0; transform: scale(0.9); }
|
opacity: 1;
|
||||||
to { opacity: 1; transform: scale(1); }
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page entrance animation */
|
/* Page entrance animation */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue