feat(marketing): add captioned product walkthrough video
This commit is contained in:
@@ -15,4 +15,4 @@ Die Metadaten und Bildbeschreibungen stehen in `src/lib/marketing/screenshots.ts
|
|||||||
7. Notdienst, Kalender, Adressbuch und eigene Abwesenheiten zeigen die Elternansicht. Bei Mitteilungen die erste aktualisierte Meldung öffnen. Für Arbeitszeit mit dem Teamkonto anmelden und zur erfassten Woche vom 5. bis 11. Oktober wechseln.
|
7. Notdienst, Kalender, Adressbuch und eigene Abwesenheiten zeigen die Elternansicht. Bei Mitteilungen die erste aktualisierte Meldung öffnen. Für Arbeitszeit mit dem Teamkonto anmelden und zur erfassten Woche vom 5. bis 11. Oktober wechseln.
|
||||||
8. Bilder und Vergrößerung auf Desktop und Mobilgerät prüfen, dann Beispieldatenbank und lokalen Server entfernen.
|
8. Bilder und Vergrößerung auf Desktop und Mobilgerät prüfen, dann Beispieldatenbank und lokalen Server entfernen.
|
||||||
|
|
||||||
Die Screenshot-Files sind zusammen unter 500 KiB groß. Das alte Bild `notdienst-calendar-linen.png` wird nicht mehr von der Landingpage verwendet.
|
Die sechs Screenshot-Files der Galerie sind zusammen unter 500 KiB groß. Die zusätzliche Aufnahme `termin-details.jpg` zeigt Teilnahme und Mitbringliste für das Erklärvideo. Das alte Bild `notdienst-calendar-linen.png` wird nicht mehr von der Landingpage verwendet.
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Erklärvideo für Kita-Planer
|
||||||
|
|
||||||
|
Stand: 14. Oktober 2026. Das Video ist ein zweiminütiger, tonloser Rundgang durch die WebApp. Es verwendet echte App-Screenshots mit ausschließlich fiktiven Beispieldaten. Bildausschnitte und grüne Markierungen führen durch die Funktionen; es wird keine Bildschirmaufnahme von Klicks vorgetäuscht.
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
- `public/landing/video/kita-planer-rundgang.mp4`: teilbare Fassung mit fest eingeblendeten deutschen Untertiteln. H.264, 1920 × 1080, 25 Bilder/s, 2:00 Minuten, ohne Audiospur.
|
||||||
|
- `public/landing/video/kita-planer-rundgang-web.mp4`: Website-Fassung ohne eingebrannte Untertitel. Der native Videoplayer lädt standardmäßig die deutschen WebVTT-Untertitel; sie passen sich dem Player an und lassen sich abschalten.
|
||||||
|
- `kita-planer-untertitel.vtt`: synchronisierte deutsche Untertitel für beide Fassungen.
|
||||||
|
- `kita-planer-kapitel.vtt`: acht Kapitel mit Zeitmarken.
|
||||||
|
- `kita-planer-poster.jpg`: Vorschaubild für die Landingpage.
|
||||||
|
- `kita-planer-transkript.txt`: vollständige Textfassung.
|
||||||
|
|
||||||
|
Alle Dateien liegen unter `public/landing/video/`. Der Player startet erst auf Wunsch und lädt das Video nicht beim Seitenaufruf (`preload="none"`). Er nutzt keine externen Videoanbieter. Unter dem Player gibt es Downloadlinks und eine aufklappbare Textfassung.
|
||||||
|
|
||||||
|
## Erneut rendern
|
||||||
|
|
||||||
|
Das Storyboard in `scripts/product-video/storyboard.json` enthält Dauer, Texte, Quellen, Bildausschnitte und Markierungen jeder Szene. Die Quellen liegen in `public/landing/screenshots/`; die Terminaufnahme entstand ebenfalls mit dem isolierten Screenshot-Fixture. Anleitung siehe `product-screenshots.md`.
|
||||||
|
|
||||||
|
Mit installierten Projektabhängigkeiten und lokalem `ffmpeg`:
|
||||||
|
|
||||||
|
```
|
||||||
|
node scripts/product-video/render.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Renderer verwendet `sharp` aus den installierten Next.js-Abhängigkeiten für SVG-Titel und Video-Compositing. Unter macOS wird die vorhandene Schrift Avenir Next verwendet. Auf anderen Systemen diese Schrift oder einen geeigneten Ersatz bereitstellen und die Umbrüche visuell prüfen. Rendern ist ein lokaler Vorbereitungsschritt und kein Teil des Produktionsbuilds; das Deployment liefert die fertigen Dateien aus.
|
||||||
|
|
||||||
|
Bei Textänderungen beide Fassungen rendern, den Film vollständig auf Lesbarkeit und Synchronität prüfen und die Kapitelstarts mit dem Storyboard abgleichen. Bei Aktualisierung der App die echten Screenshots erneuern und Ausschnitte/Markierungen anpassen. Die temporären Renderdateien werden nach dem Lauf automatisch entfernt.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
WEBVTT
|
||||||
|
|
||||||
|
1
|
||||||
|
00:00:00.000 --> 00:00:10.000
|
||||||
|
Willkommen im Kita-Planer
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:10.000 --> 00:00:32.000
|
||||||
|
Notdienst gemeinsam planen
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:32.000 --> 00:00:52.000
|
||||||
|
Termine und Feste im Blick
|
||||||
|
|
||||||
|
4
|
||||||
|
00:00:52.000 --> 00:01:06.000
|
||||||
|
Freigegebene Kontakte
|
||||||
|
|
||||||
|
5
|
||||||
|
00:01:06.000 --> 00:01:20.000
|
||||||
|
Abwesenheiten verwalten
|
||||||
|
|
||||||
|
6
|
||||||
|
00:01:20.000 --> 00:01:34.000
|
||||||
|
Offizielle Mitteilungen
|
||||||
|
|
||||||
|
7
|
||||||
|
00:01:34.000 --> 00:01:50.000
|
||||||
|
Arbeitszeit für das Team
|
||||||
|
|
||||||
|
8
|
||||||
|
00:01:50.000 --> 00:02:00.000
|
||||||
|
Kita-Planer kennenlernen
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,50 @@
|
|||||||
|
Kita-Planer – Ein Rundgang durch die Anwendung
|
||||||
|
Dauer: 120 Sekunden. Ohne Ton. App-Ansichten mit fiktiven Beispieldaten.
|
||||||
|
|
||||||
|
Willkommen im Kita-Planer
|
||||||
|
Kita-Planer bringt Dienste, Termine und offizielle Informationen an einen gemeinsamen Ort – für Eltern, Vorstand und Kita-Team.
|
||||||
|
|
||||||
|
Notdienst gemeinsam planen
|
||||||
|
Eltern markieren die Tage, an denen ihr Haushalt für den Notdienst verfügbar ist.
|
||||||
|
|
||||||
|
Notdienst gemeinsam planen
|
||||||
|
Feste, Elternabende und gesperrte Teamtage stehen direkt im Kalender. So lässt sich die Auswahl mit dem Kita-Alltag abstimmen.
|
||||||
|
|
||||||
|
Notdienst gemeinsam planen
|
||||||
|
Die Anzeige zeigt, ob genug Tage ausgewählt sind. Mit Speichern wird die Verfügbarkeit für die Planung übernommen.
|
||||||
|
|
||||||
|
Termine und Feste im Blick
|
||||||
|
Im Monatskalender stehen Ausflüge, Elternabende, Feste und Schließtage. Alle sehen die Termine im selben Zusammenhang.
|
||||||
|
|
||||||
|
Termine und Feste im Blick
|
||||||
|
Für dieses Herbstfest antwortet ihr pro Familie und gebt an, wie viele Erwachsene teilnehmen.
|
||||||
|
|
||||||
|
Termine und Feste im Blick
|
||||||
|
In der Mitbringliste seht ihr, wer schon etwas beisteuert. Eigene Beiträge könnt ihr bearbeiten oder löschen.
|
||||||
|
|
||||||
|
Freigegebene Kontakte
|
||||||
|
Das interne Adressbuch verbindet Haushalte, Kinder und die freigegebenen Kontakte der Eltern.
|
||||||
|
|
||||||
|
Freigegebene Kontakte
|
||||||
|
Jeder Elternteil entscheidet selbst über die Freigabe seiner Kontaktdaten. Im Adressbuch erscheinen nur ausdrücklich freigegebene Angaben.
|
||||||
|
|
||||||
|
Abwesenheiten verwalten
|
||||||
|
Eltern melden Abwesenheiten digital. Die Kita sieht die Meldung in ihrer Übersicht.
|
||||||
|
|
||||||
|
Abwesenheiten verwalten
|
||||||
|
Zukünftige Abwesenheiten können Eltern bearbeiten oder zurücknehmen. Sobald eine Meldung begonnen hat, übernimmt die Kita die Änderung.
|
||||||
|
|
||||||
|
Offizielle Mitteilungen
|
||||||
|
Mitteilungen bündeln offizielle Informationen an einem Ort. Formatierter Text und der Aktualisierungshinweis machen Änderungen sichtbar.
|
||||||
|
|
||||||
|
Offizielle Mitteilungen
|
||||||
|
Kita-Admins können Beiträge korrigieren; Anhänge bleiben geschützt abrufbar. Beim optionalen E-Mail-Versand bleibt der Status je Empfänger nachvollziehbar.
|
||||||
|
|
||||||
|
Arbeitszeit für das Team
|
||||||
|
Fachkräfte erfassen ihre Arbeitszeit mit Pausen und reichen vollständige Wochen zur Prüfung ein.
|
||||||
|
|
||||||
|
Arbeitszeit für das Team
|
||||||
|
Das bestätigte Zeitkonto und der verfügbare Urlaub bleiben sichtbar. Urlaub und Freizeitausgleich lassen sich im Personalbereich beantragen.
|
||||||
|
|
||||||
|
Kita-Planer kennenlernen
|
||||||
|
Lernt Kita-Planer in Ruhe kennen und entdeckt die passenden Funktionen. Alle gezeigten App-Ansichten verwenden ausschließlich Beispieldaten.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
WEBVTT
|
||||||
|
|
||||||
|
1
|
||||||
|
00:00:00.000 --> 00:00:10.000
|
||||||
|
Kita-Planer bringt Dienste, Termine und offizielle Informationen
|
||||||
|
an einen gemeinsamen Ort – für Eltern, Vorstand und Kita-Team.
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:10.000 --> 00:00:17.000
|
||||||
|
Eltern markieren die Tage, an denen ihr Haushalt
|
||||||
|
für den Notdienst verfügbar ist.
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:17.000 --> 00:00:24.000
|
||||||
|
Feste, Elternabende und gesperrte Teamtage stehen direkt im Kalender.
|
||||||
|
So lässt sich die Auswahl mit dem Kita-Alltag abstimmen.
|
||||||
|
|
||||||
|
4
|
||||||
|
00:00:24.000 --> 00:00:32.000
|
||||||
|
Die Anzeige zeigt, ob genug Tage ausgewählt sind.
|
||||||
|
Mit Speichern wird die Verfügbarkeit für die Planung übernommen.
|
||||||
|
|
||||||
|
5
|
||||||
|
00:00:32.000 --> 00:00:41.000
|
||||||
|
Im Monatskalender stehen Ausflüge, Elternabende, Feste und Schließtage.
|
||||||
|
Alle sehen die Termine im selben Zusammenhang.
|
||||||
|
|
||||||
|
6
|
||||||
|
00:00:41.000 --> 00:00:46.000
|
||||||
|
Für dieses Herbstfest antwortet ihr pro Familie
|
||||||
|
und gebt an, wie viele Erwachsene teilnehmen.
|
||||||
|
|
||||||
|
7
|
||||||
|
00:00:46.000 --> 00:00:52.000
|
||||||
|
In der Mitbringliste seht ihr, wer schon etwas beisteuert.
|
||||||
|
Eigene Beiträge könnt ihr bearbeiten oder löschen.
|
||||||
|
|
||||||
|
8
|
||||||
|
00:00:52.000 --> 00:00:59.000
|
||||||
|
Das interne Adressbuch verbindet Haushalte, Kinder
|
||||||
|
und die freigegebenen Kontakte der Eltern.
|
||||||
|
|
||||||
|
9
|
||||||
|
00:00:59.000 --> 00:01:06.000
|
||||||
|
Jeder Elternteil entscheidet selbst über die Freigabe seiner Kontaktdaten.
|
||||||
|
Im Adressbuch erscheinen nur ausdrücklich freigegebene Angaben.
|
||||||
|
|
||||||
|
10
|
||||||
|
00:01:06.000 --> 00:01:13.000
|
||||||
|
Eltern melden Abwesenheiten digital.
|
||||||
|
Die Kita sieht die Meldung in ihrer Übersicht.
|
||||||
|
|
||||||
|
11
|
||||||
|
00:01:13.000 --> 00:01:20.000
|
||||||
|
Zukünftige Abwesenheiten können Eltern bearbeiten oder zurücknehmen.
|
||||||
|
Sobald eine Meldung begonnen hat, übernimmt die Kita die Änderung.
|
||||||
|
|
||||||
|
12
|
||||||
|
00:01:20.000 --> 00:01:27.000
|
||||||
|
Mitteilungen bündeln offizielle Informationen an einem Ort.
|
||||||
|
Formatierter Text und der Aktualisierungshinweis machen Änderungen sichtbar.
|
||||||
|
|
||||||
|
13
|
||||||
|
00:01:27.000 --> 00:01:34.000
|
||||||
|
Kita-Admins können Beiträge korrigieren; Anhänge bleiben geschützt abrufbar.
|
||||||
|
Beim optionalen E-Mail-Versand bleibt der Status je Empfänger nachvollziehbar.
|
||||||
|
|
||||||
|
14
|
||||||
|
00:01:34.000 --> 00:01:42.000
|
||||||
|
Fachkräfte erfassen ihre Arbeitszeit mit Pausen
|
||||||
|
und reichen vollständige Wochen zur Prüfung ein.
|
||||||
|
|
||||||
|
15
|
||||||
|
00:01:42.000 --> 00:01:50.000
|
||||||
|
Das bestätigte Zeitkonto und der verfügbare Urlaub bleiben sichtbar.
|
||||||
|
Urlaub und Freizeitausgleich lassen sich im Personalbereich beantragen.
|
||||||
|
|
||||||
|
16
|
||||||
|
00:01:50.000 --> 00:02:00.000
|
||||||
|
Lernt Kita-Planer in Ruhe kennen und entdeckt die passenden Funktionen.
|
||||||
|
Alle gezeigten App-Ansichten verwenden ausschließlich Beispieldaten.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/** Render the local, silent product tour from real app captures and SVG titles. */
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||||
|
const storyboard = JSON.parse(await fs.readFile(path.join(root, 'scripts/product-video/storyboard.json'), 'utf8'));
|
||||||
|
const out = path.join(root, 'public/landing/video');
|
||||||
|
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'kita-product-video-'));
|
||||||
|
await fs.mkdir(out, { recursive: true });
|
||||||
|
const { width: W, height: H, fps } = storyboard;
|
||||||
|
const colors = { paper: '#f7f4ed', ink: '#302820', brown: '#7b492e', green: '#3f745a', muted: '#696358' };
|
||||||
|
const esc = text => String(text).replace(/[<>&"']/g, c => ({ '<':'<', '>':'>', '&':'&', '"':'"', "'":''' }[c]));
|
||||||
|
const text = (x,y,value,size=30,color=colors.ink,weight=400) => `<text x="${x}" y="${y}" font-family="Avenir Next, sans-serif" font-size="${size}" font-weight="${weight}" fill="${color}">${esc(value)}</text>`;
|
||||||
|
const lines = (x,y,values,size=30,step=44,color=colors.ink,weight=400) => values.map((v,i)=>text(x,y+i*step,v,size,color,weight)).join('');
|
||||||
|
const imageStage = { x:600, y:162, width:1240, height:714 };
|
||||||
|
const total = storyboard.scenes.reduce((sum,scene)=>sum+scene.duration,0);
|
||||||
|
const timestamp = seconds => `${String(Math.floor(seconds/3600)).padStart(2,'0')}:${String(Math.floor(seconds/60)%60).padStart(2,'0')}:${String(seconds%60).padStart(2,'0')}.000`;
|
||||||
|
let time = 0;
|
||||||
|
const cues = [];
|
||||||
|
const assets = [];
|
||||||
|
const webAssets = [];
|
||||||
|
|
||||||
|
function ffmpeg(args) {
|
||||||
|
const result = spawnSync('ffmpeg', ['-hide_banner','-loglevel','error','-y',...args], { encoding:'utf8', maxBuffer:8*1024*1024 });
|
||||||
|
if (result.status !== 0) throw new Error(result.stderr || 'ffmpeg failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const [index,scene] of storyboard.scenes.entries()) {
|
||||||
|
const [left,top,width,height] = scene.crop;
|
||||||
|
const shot = await sharp(path.join(root, 'public/landing/screenshots',scene.image))
|
||||||
|
.extract({left,top,width,height}).resize(imageStage.width,imageStage.height,{fit:'contain',background:colors.paper}).png().toBuffer();
|
||||||
|
const scale = Math.min(imageStage.width/width,imageStage.height/height);
|
||||||
|
const offsetX = imageStage.x+(imageStage.width-width*scale)/2;
|
||||||
|
const offsetY = imageStage.y+(imageStage.height-height*scale)/2;
|
||||||
|
let focus = '';
|
||||||
|
if (scene.focus) {
|
||||||
|
const [fx,fy,fw,fh] = scene.focus;
|
||||||
|
focus = `<rect x="${offsetX+(fx-left)*scale}" y="${offsetY+(fy-top)*scale}" width="${fw*scale}" height="${fh*scale}" rx="12" fill="none" stroke="${colors.green}" stroke-width="5"/><circle cx="${offsetX+(fx-left)*scale+12}" cy="${offsetY+(fy-top)*scale-10}" r="18" fill="${colors.green}"/>`;
|
||||||
|
}
|
||||||
|
const chapter = storyboard.chapters[scene.chapter];
|
||||||
|
const title = lines(74,290,scene.title,56,72,colors.ink,600);
|
||||||
|
const hintWords = scene.hint.split(' '); const hintLines = []; let line = '';
|
||||||
|
for (const word of hintWords) { if ((line+' '+word).trim().length > 28) { hintLines.push(line); line=word; } else line=(line+' '+word).trim(); } if(line)hintLines.push(line);
|
||||||
|
const header = `<rect width="${W}" height="${H}" fill="${colors.paper}"/><rect x="72" y="44" width="54" height="54" rx="16" fill="${colors.brown}"/>${text(88,82,'K',30,'white',600)}${text(144,80,'Kita-Planer',32,colors.ink,600)}${text(144,111,'Ein Rundgang durch die Anwendung',19,colors.muted)}${text(1490,80,`${String(scene.chapter+1).padStart(2,'0')} / 08`,24,colors.green,600)}${text(1490,111,'App-Ansichten mit Beispieldaten',18,colors.muted)}<line x1="72" y1="132" x2="1840" y2="132" stroke="#dcd6cb"/>${text(74,208,scene.chapter===0?'WILLKOMMEN':scene.chapter===7?'GEMEINSAM STARTEN':`BEREICH ${String(scene.chapter).padStart(2,'0')}`,20,colors.green,600)}${title}<rect x="74" y="466" width="4" height="${Math.max(90,hintLines.length*40)}" rx="2" fill="${colors.green}"/>${lines(96,498,hintLines,28,40,colors.muted)}${text(74,775,'Für Eltern, Vorstand',24,colors.muted)}${text(74,811,'und das Kita-Team.',24,colors.muted)}<rect x="598" y="160" width="1244" height="718" rx="16" fill="white" stroke="#d9d3c7" stroke-width="2"/>`;
|
||||||
|
const footer = `<rect x="0" y="920" width="1920" height="160" fill="#ebe7dc"/><rect x="72" y="923" width="1768" height="4" rx="2" fill="#d8d1c2"/><rect x="72" y="923" width="${1768*(time+scene.duration)/total}" height="4" rx="2" fill="${colors.green}"/>${text(74,899,chapter.title,20,colors.muted)}${text(1470,899,'mein-kitaplaner.de',20,colors.brown,600)}`;
|
||||||
|
const base = await sharp(Buffer.from(`<svg width="${W}" height="${H}">${header}${footer}</svg>`)).png().toBuffer();
|
||||||
|
const captions = `<rect x="210" y="951" width="1500" height="100" rx="18" fill="#302820"/>${scene.subtitle.map((value,i)=>`<text x="960" y="${989+i*42}" text-anchor="middle" font-family="Avenir Next, sans-serif" font-size="34" font-weight="500" fill="#ffffff">${esc(value)}</text>`).join('')}`;
|
||||||
|
const foreground = Buffer.from(`<svg width="${W}" height="${H}">${focus}${captions}</svg>`);
|
||||||
|
const cleanFrame = await sharp(base).composite([{input:shot,left:imageStage.x,top:imageStage.y},{input:Buffer.from(`<svg width="${W}" height="${H}">${focus}</svg>`),left:0,top:0}]).png().toBuffer();
|
||||||
|
const cleanPath = path.join(temp,`web-frame-${index}.png`);
|
||||||
|
await fs.writeFile(cleanPath,cleanFrame);
|
||||||
|
const frame = await sharp(base).composite([{input:shot,left:imageStage.x,top:imageStage.y},{input:foreground,left:0,top:0}]).png().toBuffer();
|
||||||
|
const framePath = path.join(temp,`frame-${index}.png`);
|
||||||
|
await fs.writeFile(framePath,frame);
|
||||||
|
if(index===0) await sharp(frame).jpeg({quality:88}).toFile(path.join(out,'kita-planer-poster.jpg'));
|
||||||
|
const clip = path.join(temp,`clip-${index}.mp4`);
|
||||||
|
// Gentle transitions leave the UI and all captions still and legible.
|
||||||
|
ffmpeg(['-loop','1','-framerate',String(fps),'-i',framePath,'-t',String(scene.duration),'-vf',`fade=t=in:st=0:d=0.25:color=0xf7f4ed,fade=t=out:st=${scene.duration-0.25}:d=0.25:color=0xf7f4ed,format=yuv420p`,'-c:v','libx264','-preset','fast','-crf','20','-threads','2','-an',clip]);
|
||||||
|
assets.push(clip);
|
||||||
|
const webClip = path.join(temp,`web-clip-${index}.mp4`);
|
||||||
|
ffmpeg(['-loop','1','-framerate',String(fps),'-i',cleanPath,'-t',String(scene.duration),'-vf',`fade=t=in:st=0:d=0.25:color=0xf7f4ed,fade=t=out:st=${scene.duration-0.25}:d=0.25:color=0xf7f4ed,format=yuv420p`,'-c:v','libx264','-preset','fast','-crf','20','-threads','2','-an',webClip]);
|
||||||
|
webAssets.push(webClip);
|
||||||
|
cues.push(`${index+1}\n${timestamp(time)} --> ${timestamp(time+scene.duration)}\n${scene.subtitle.join('\n')}`);
|
||||||
|
time+=scene.duration;
|
||||||
|
console.log(`Rendered scene ${index+1}/${storyboard.scenes.length}: ${chapter.title}`);
|
||||||
|
}
|
||||||
|
await fs.writeFile(path.join(temp,'concat.txt'),assets.map(p=>`file '${p}'`).join('\n'));
|
||||||
|
ffmpeg(['-f','concat','-safe','0','-i',path.join(temp,'concat.txt'),'-c','copy','-movflags','+faststart','-metadata','title=Kita-Planer – Ein Rundgang durch die Anwendung','-metadata','comment=Echte App-Ansichten mit fiktiven Beispieldaten. Deutsche Untertitel und visuelle Hinweise. Ohne Ton.',path.join(out,'kita-planer-rundgang.mp4')]);
|
||||||
|
await fs.writeFile(path.join(temp,'web-concat.txt'),webAssets.map(p=>`file '${p}'`).join('\n'));
|
||||||
|
ffmpeg(['-f','concat','-safe','0','-i',path.join(temp,'web-concat.txt'),'-c','copy','-movflags','+faststart',path.join(out,'kita-planer-rundgang-web.mp4')]);
|
||||||
|
await fs.writeFile(path.join(out,'kita-planer-untertitel.vtt'),`WEBVTT\n\n${cues.join('\n\n')}\n`);
|
||||||
|
await fs.writeFile(path.join(out,'kita-planer-kapitel.vtt'),`WEBVTT\n\n${storyboard.chapters.map((chapter,index)=>`${index+1}\n${timestamp(chapter.start)} --> ${timestamp(storyboard.chapters[index+1]?.start??total)}\n${chapter.title}`).join('\n\n')}\n`);
|
||||||
|
await fs.writeFile(path.join(out,'kita-planer-transkript.txt'),`Kita-Planer – Ein Rundgang durch die Anwendung\nDauer: ${total} Sekunden. Ohne Ton. App-Ansichten mit fiktiven Beispieldaten.\n\n${storyboard.scenes.map(scene=>`${storyboard.chapters[scene.chapter].title}\n${scene.subtitle.join(' ')}`).join('\n\n')}\n`);
|
||||||
|
console.log(`Finished ${total}s video in ${out}`);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(temp,{recursive:true,force:true});
|
||||||
|
}
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
{
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"fps": 25,
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"start": 0,
|
||||||
|
"title": "Willkommen im Kita-Planer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 10,
|
||||||
|
"title": "Notdienst gemeinsam planen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 32,
|
||||||
|
"title": "Termine und Feste im Blick"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 52,
|
||||||
|
"title": "Freigegebene Kontakte"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 66,
|
||||||
|
"title": "Abwesenheiten verwalten"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 80,
|
||||||
|
"title": "Offizielle Mitteilungen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 94,
|
||||||
|
"title": "Arbeitszeit für das Team"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 110,
|
||||||
|
"title": "Kita-Planer kennenlernen"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"duration": 10,
|
||||||
|
"chapter": 0,
|
||||||
|
"title": [
|
||||||
|
"Mehr Ruhe im",
|
||||||
|
"Kita-Alltag."
|
||||||
|
],
|
||||||
|
"hint": "Für Elterninitiativen und Kita-Vereine",
|
||||||
|
"image": "notdienst.jpg",
|
||||||
|
"crop": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1600,
|
||||||
|
1000
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Kita-Planer bringt Dienste, Termine und offizielle Informationen",
|
||||||
|
"an einen gemeinsamen Ort – für Eltern, Vorstand und Kita-Team."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 7,
|
||||||
|
"chapter": 1,
|
||||||
|
"title": [
|
||||||
|
"Wann könnt",
|
||||||
|
"ihr helfen?"
|
||||||
|
],
|
||||||
|
"hint": "Verfügbarkeiten gemeinsam eintragen",
|
||||||
|
"image": "notdienst.jpg",
|
||||||
|
"crop": [
|
||||||
|
310,
|
||||||
|
45,
|
||||||
|
1230,
|
||||||
|
870
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
325,
|
||||||
|
365,
|
||||||
|
394,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Eltern markieren die Tage, an denen ihr Haushalt",
|
||||||
|
"für den Notdienst verfügbar ist."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 7,
|
||||||
|
"chapter": 1,
|
||||||
|
"title": [
|
||||||
|
"Kita-Termine",
|
||||||
|
"gleich sehen."
|
||||||
|
],
|
||||||
|
"hint": "Teamtage sind für die Auswahl gesperrt",
|
||||||
|
"image": "notdienst.jpg",
|
||||||
|
"crop": [
|
||||||
|
330,
|
||||||
|
170,
|
||||||
|
1200,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
355,
|
||||||
|
670,
|
||||||
|
335,
|
||||||
|
105
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Feste, Elternabende und gesperrte Teamtage stehen direkt im Kalender.",
|
||||||
|
"So lässt sich die Auswahl mit dem Kita-Alltag abstimmen."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 8,
|
||||||
|
"chapter": 1,
|
||||||
|
"title": [
|
||||||
|
"Auswahl prüfen.",
|
||||||
|
"Dann speichern."
|
||||||
|
],
|
||||||
|
"hint": "Der Fortschritt bleibt sichtbar",
|
||||||
|
"image": "notdienst.jpg",
|
||||||
|
"crop": [
|
||||||
|
310,
|
||||||
|
45,
|
||||||
|
1230,
|
||||||
|
870
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
1050,
|
||||||
|
80,
|
||||||
|
450,
|
||||||
|
65
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Die Anzeige zeigt, ob genug Tage ausgewählt sind.",
|
||||||
|
"Mit Speichern wird die Verfügbarkeit für die Planung übernommen."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 9,
|
||||||
|
"chapter": 2,
|
||||||
|
"title": [
|
||||||
|
"Was steht",
|
||||||
|
"als Nächstes an?"
|
||||||
|
],
|
||||||
|
"hint": "Ein gemeinsamer Kalender",
|
||||||
|
"image": "kalender.jpg",
|
||||||
|
"crop": [
|
||||||
|
300,
|
||||||
|
35,
|
||||||
|
1240,
|
||||||
|
920
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
830,
|
||||||
|
660,
|
||||||
|
415,
|
||||||
|
155
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Im Monatskalender stehen Ausflüge, Elternabende, Feste und Schließtage.",
|
||||||
|
"Alle sehen die Termine im selben Zusammenhang."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 5,
|
||||||
|
"chapter": 2,
|
||||||
|
"title": [
|
||||||
|
"Zusagen pro",
|
||||||
|
"Familie."
|
||||||
|
],
|
||||||
|
"hint": "Anzahl der Erwachsenen angeben",
|
||||||
|
"image": "termin-details.jpg",
|
||||||
|
"crop": [
|
||||||
|
543,
|
||||||
|
335,
|
||||||
|
501,
|
||||||
|
370
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
561,
|
||||||
|
444,
|
||||||
|
470,
|
||||||
|
150
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Für dieses Herbstfest antwortet ihr pro Familie",
|
||||||
|
"und gebt an, wie viele Erwachsene teilnehmen."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 6,
|
||||||
|
"chapter": 2,
|
||||||
|
"title": [
|
||||||
|
"Wer bringt",
|
||||||
|
"was mit?"
|
||||||
|
],
|
||||||
|
"hint": "Eigene Beiträge bearbeiten oder löschen",
|
||||||
|
"image": "termin-details.jpg",
|
||||||
|
"crop": [
|
||||||
|
543,
|
||||||
|
690,
|
||||||
|
501,
|
||||||
|
318
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
575,
|
||||||
|
748,
|
||||||
|
440,
|
||||||
|
62
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"In der Mitbringliste seht ihr, wer schon etwas beisteuert.",
|
||||||
|
"Eigene Beiträge könnt ihr bearbeiten oder löschen."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 7,
|
||||||
|
"chapter": 3,
|
||||||
|
"title": [
|
||||||
|
"Familien und",
|
||||||
|
"Kontakte finden."
|
||||||
|
],
|
||||||
|
"hint": "Haushalte und Kinder zusammen sehen",
|
||||||
|
"image": "adressbuch.jpg",
|
||||||
|
"crop": [
|
||||||
|
295,
|
||||||
|
35,
|
||||||
|
1220,
|
||||||
|
700
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
315,
|
||||||
|
325,
|
||||||
|
1080,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Das interne Adressbuch verbindet Haushalte, Kinder",
|
||||||
|
"und die freigegebenen Kontakte der Eltern."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 7,
|
||||||
|
"chapter": 3,
|
||||||
|
"title": [
|
||||||
|
"Freigabe bleibt",
|
||||||
|
"eine Entscheidung."
|
||||||
|
],
|
||||||
|
"hint": "Kontaktdaten erscheinen nach Opt-in",
|
||||||
|
"image": "adressbuch.jpg",
|
||||||
|
"crop": [
|
||||||
|
580,
|
||||||
|
290,
|
||||||
|
900,
|
||||||
|
310
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
638,
|
||||||
|
330,
|
||||||
|
420,
|
||||||
|
95
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Jeder Elternteil entscheidet selbst über die Freigabe seiner Kontaktdaten.",
|
||||||
|
"Im Adressbuch erscheinen nur ausdrücklich freigegebene Angaben."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 7,
|
||||||
|
"chapter": 4,
|
||||||
|
"title": [
|
||||||
|
"Kind abmelden.",
|
||||||
|
"Kita informieren."
|
||||||
|
],
|
||||||
|
"hint": "Krankheit, Urlaub und andere Gründe",
|
||||||
|
"image": "abwesenheiten.jpg",
|
||||||
|
"crop": [
|
||||||
|
395,
|
||||||
|
25,
|
||||||
|
1060,
|
||||||
|
640
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
420,
|
||||||
|
320,
|
||||||
|
1010,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Eltern melden Abwesenheiten digital.",
|
||||||
|
"Die Kita sieht die Meldung in ihrer Übersicht."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 7,
|
||||||
|
"chapter": 4,
|
||||||
|
"title": [
|
||||||
|
"Pläne dürfen",
|
||||||
|
"sich ändern."
|
||||||
|
],
|
||||||
|
"hint": "Zukünftige Meldungen korrigieren",
|
||||||
|
"image": "abwesenheiten.jpg",
|
||||||
|
"crop": [
|
||||||
|
415,
|
||||||
|
305,
|
||||||
|
1020,
|
||||||
|
340
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
446,
|
||||||
|
565,
|
||||||
|
320,
|
||||||
|
45
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Zukünftige Abwesenheiten können Eltern bearbeiten oder zurücknehmen.",
|
||||||
|
"Sobald eine Meldung begonnen hat, übernimmt die Kita die Änderung."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 7,
|
||||||
|
"chapter": 5,
|
||||||
|
"title": [
|
||||||
|
"Offizielle Infos",
|
||||||
|
"bleiben auffindbar."
|
||||||
|
],
|
||||||
|
"hint": "Das digitale Schwarze Brett",
|
||||||
|
"image": "mitteilungen.jpg",
|
||||||
|
"crop": [
|
||||||
|
410,
|
||||||
|
275,
|
||||||
|
780,
|
||||||
|
450
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
484,
|
||||||
|
367,
|
||||||
|
630,
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Mitteilungen bündeln offizielle Informationen an einem Ort.",
|
||||||
|
"Formatierter Text und der Aktualisierungshinweis machen Änderungen sichtbar."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 7,
|
||||||
|
"chapter": 5,
|
||||||
|
"title": [
|
||||||
|
"Auch nach einer",
|
||||||
|
"Korrektur klar."
|
||||||
|
],
|
||||||
|
"hint": "Geschützte Anhänge und optionale E-Mails",
|
||||||
|
"image": "mitteilungen.jpg",
|
||||||
|
"crop": [
|
||||||
|
450,
|
||||||
|
330,
|
||||||
|
700,
|
||||||
|
335
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
485,
|
||||||
|
580,
|
||||||
|
625,
|
||||||
|
53
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Kita-Admins können Beiträge korrigieren; Anhänge bleiben geschützt abrufbar.",
|
||||||
|
"Beim optionalen E-Mail-Versand bleibt der Status je Empfänger nachvollziehbar."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 8,
|
||||||
|
"chapter": 6,
|
||||||
|
"title": [
|
||||||
|
"Arbeitszeit für",
|
||||||
|
"das Kita-Team."
|
||||||
|
],
|
||||||
|
"hint": "Stunden erfassen und Wochen einreichen",
|
||||||
|
"image": "personal.jpg",
|
||||||
|
"crop": [
|
||||||
|
315,
|
||||||
|
445,
|
||||||
|
1210,
|
||||||
|
780
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
345,
|
||||||
|
1105,
|
||||||
|
780,
|
||||||
|
105
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Fachkräfte erfassen ihre Arbeitszeit mit Pausen",
|
||||||
|
"und reichen vollständige Wochen zur Prüfung ein."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 8,
|
||||||
|
"chapter": 6,
|
||||||
|
"title": [
|
||||||
|
"Zeitkonto und",
|
||||||
|
"Urlaub im Blick."
|
||||||
|
],
|
||||||
|
"hint": "Bestätigte Zeit von vorläufiger unterscheiden",
|
||||||
|
"image": "personal.jpg",
|
||||||
|
"crop": [
|
||||||
|
308,
|
||||||
|
235,
|
||||||
|
1210,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"focus": [
|
||||||
|
315,
|
||||||
|
275,
|
||||||
|
1185,
|
||||||
|
135
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Das bestätigte Zeitkonto und der verfügbare Urlaub bleiben sichtbar.",
|
||||||
|
"Urlaub und Freizeitausgleich lassen sich im Personalbereich beantragen."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": 10,
|
||||||
|
"chapter": 7,
|
||||||
|
"title": [
|
||||||
|
"Gemeinsam",
|
||||||
|
"besser planen."
|
||||||
|
],
|
||||||
|
"hint": "Startet mit den Abläufen, die euch entlasten",
|
||||||
|
"image": "kalender.jpg",
|
||||||
|
"crop": [
|
||||||
|
300,
|
||||||
|
35,
|
||||||
|
1240,
|
||||||
|
920
|
||||||
|
],
|
||||||
|
"subtitle": [
|
||||||
|
"Lernt Kita-Planer in Ruhe kennen und entdeckt die passenden Funktionen.",
|
||||||
|
"Alle gezeigten App-Ansichten verwenden ausschließlich Beispieldaten."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+6
-3
@@ -40,6 +40,7 @@ import {
|
|||||||
} from "@/lib/marketing/seo";
|
} from "@/lib/marketing/seo";
|
||||||
import { ProductScreenshots, ScreenshotPreview } from "@/components/marketing/product-screenshots";
|
import { ProductScreenshots, ScreenshotPreview } from "@/components/marketing/product-screenshots";
|
||||||
import { getProductScreenshot } from "@/lib/marketing/screenshots";
|
import { getProductScreenshot } from "@/lib/marketing/screenshots";
|
||||||
|
import { ProductVideo } from "@/components/marketing/product-video";
|
||||||
import { ContactForm } from "./contact-form";
|
import { ContactForm } from "./contact-form";
|
||||||
|
|
||||||
export const metadata = publicPageMetadata(
|
export const metadata = publicPageMetadata(
|
||||||
@@ -244,10 +245,10 @@ export default function LandingPage() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="h-12 bg-white px-6 text-slate-950 hover:bg-white/90"
|
className="h-12 bg-white px-6 text-slate-950 hover:bg-white/90"
|
||||||
>
|
>
|
||||||
<Link href="/#kontakt">
|
<a href="#produkt-video">
|
||||||
Demo anfragen
|
Video ansehen
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
</Link>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
@@ -271,6 +272,8 @@ export default function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<ProductVideo />
|
||||||
|
|
||||||
<section className="mx-auto w-full max-w-7xl px-5 py-20 sm:px-6 lg:py-24">
|
<section className="mx-auto w-full max-w-7xl px-5 py-20 sm:px-6 lg:py-24">
|
||||||
<div className="mb-10 grid gap-5 lg:grid-cols-[0.78fr_1fr] lg:items-end">
|
<div className="mb-10 grid gap-5 lg:grid-cols-[0.78fr_1fr] lg:items-end">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import storyboard from "../../../scripts/product-video/storyboard.json";
|
||||||
|
|
||||||
|
export function ProductVideo() {
|
||||||
|
return (
|
||||||
|
<section id="produkt-video" aria-labelledby="produkt-video-heading" className="mx-auto w-full max-w-7xl scroll-mt-8 px-5 pt-16 pb-4 sm:px-6 lg:pt-24">
|
||||||
|
<div className="mb-8 grid gap-5 lg:grid-cols-[0.85fr_1.15fr] lg:items-end">
|
||||||
|
<div>
|
||||||
|
<p className="mb-3 text-sm font-semibold uppercase tracking-wide text-emerald-700">In Ruhe kennenlernen</p>
|
||||||
|
<h2 id="produkt-video-heading" className="text-balance text-3xl font-semibold leading-tight sm:text-4xl">Kita-Planer in zwei Minuten.</h2>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-2xl text-sm leading-6 text-slate-600 lg:justify-self-end">
|
||||||
|
Ein Rundgang durch die sechs Bereiche mit echten App-Ansichten und Beispieldaten.
|
||||||
|
Deutsche Untertitel und markierte Hinweise erklären die Abläufe – ohne Ton und ohne Gesprächstermin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
playsInline
|
||||||
|
preload="none"
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
poster="/landing/video/kita-planer-poster.jpg"
|
||||||
|
aria-label="Kita-Planer: zweiminütiger Rundgang mit deutschen Untertiteln und visuellen Hinweisen"
|
||||||
|
className="aspect-video h-auto w-full rounded-2xl border border-slate-200 bg-[#f7f4ed] shadow-sm [&::cue]:text-[clamp(12px,1.8vw,24px)]"
|
||||||
|
>
|
||||||
|
<source src="/landing/video/kita-planer-rundgang-web.mp4" type="video/mp4" />
|
||||||
|
<track src="/landing/video/kita-planer-untertitel.vtt" kind="captions" srcLang="de" label="Deutsch" default />
|
||||||
|
<track src="/landing/video/kita-planer-kapitel.vtt" kind="chapters" srcLang="de" label="Kapitel" />
|
||||||
|
Dein Browser unterstützt die Videoanzeige nicht. Das Video kannst du über den Link unten herunterladen.
|
||||||
|
</video>
|
||||||
|
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 text-xs leading-6 text-slate-500">
|
||||||
|
<p>2:00 Min. · Ohne Ton · Deutsche Untertitel</p>
|
||||||
|
<div className="flex flex-wrap gap-x-5 gap-y-1">
|
||||||
|
<a href="/landing/video/kita-planer-rundgang.mp4" download className="font-medium text-emerald-800 underline-offset-4 hover:underline">Video herunterladen</a>
|
||||||
|
<a href="/landing/video/kita-planer-untertitel.vtt" download className="text-emerald-800 underline-offset-4 hover:underline">Untertitel als Datei</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<details className="mt-5 rounded-xl border border-slate-200 bg-white px-5 py-4 text-sm text-slate-600">
|
||||||
|
<summary className="cursor-pointer font-medium text-slate-800">Videoinhalt als Text lesen</summary>
|
||||||
|
<div className="mt-5 grid gap-5 md:grid-cols-2">
|
||||||
|
{storyboard.chapters.map((chapter, index) => (
|
||||||
|
<div key={chapter.title}>
|
||||||
|
<h3 className="font-semibold text-slate-800">{chapter.title}</h3>
|
||||||
|
<div className="mt-2 space-y-2 leading-6">
|
||||||
|
{storyboard.scenes.filter((scene) => scene.chapter === index).map((scene) => <p key={scene.title.join(" ")}>{scene.subtitle.join(" ")}</p>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
// Update when the public product copy changes, never on each request or build.
|
// Update when the public product copy changes, never on each request or build.
|
||||||
export const marketingLastModified = "2026-10-13";
|
export const marketingLastModified = "2026-10-14";
|
||||||
export const marketingTitle =
|
export const marketingTitle =
|
||||||
"Kita-Planer für Elterninitiativen und Kita-Vereine";
|
"Kita-Planer für Elterninitiativen und Kita-Vereine";
|
||||||
export const marketingDescription =
|
export const marketingDescription =
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ test('Social-Crawler erhält Canonical und Vorschaudaten direkt im HTML-Head', a
|
|||||||
test('Ausgelieferte Sitemap enthält ausschließlich erreichbare kanonische öffentliche Seiten', async () => {
|
test('Ausgelieferte Sitemap enthält ausschließlich erreichbare kanonische öffentliche Seiten', async () => {
|
||||||
const response = await request('/sitemap.xml'); assert.equal(response.status, 200); const xml = await response.text();
|
const response = await request('/sitemap.xml'); assert.equal(response.status, 200); const xml = await response.text();
|
||||||
const urls = [...xml.matchAll(/<loc>(.*?)<\/loc>/g)].map(match => new URL(match[1]).href).sort(); assert.deepEqual(urls, paths.map(path => new URL(path, site).href).sort());
|
const urls = [...xml.matchAll(/<loc>(.*?)<\/loc>/g)].map(match => new URL(match[1]).href).sort(); assert.deepEqual(urls, paths.map(path => new URL(path, site).href).sort());
|
||||||
assert.ok(xml.includes('<lastmod>2026-10-13</lastmod>')); assert.ok(!xml.includes('T00:'));
|
assert.ok(xml.includes('<lastmod>2026-10-14</lastmod>')); assert.ok(!xml.includes('T00:'));
|
||||||
});
|
});
|
||||||
test('Robots verwenden dieselbe Domain und halten Loginformulare zum Lesen von noindex offen', async () => {
|
test('Robots verwenden dieselbe Domain und halten Loginformulare zum Lesen von noindex offen', async () => {
|
||||||
const response = await request('/robots.txt'); assert.equal(response.status, 200); const text = await response.text();
|
const response = await request('/robots.txt'); assert.equal(response.status, 200); const text = await response.text();
|
||||||
|
|||||||
Reference in New Issue
Block a user