feat: integrate Kirby CMS data with Vue print editor

- Add JSON content representation template (recit.json.php)
- Create virtual /print page plugin for recit pages
- Add recit.php base template for content representation
- Create Pinia store for recit data management
- Add block components (text, heading, image, list, quote, video, map)
- Update PagedJsWrapper for dynamic content rendering with data-page-type
- Modify header.php to pass recit JSON URL via data attribute
- Update App.vue to load recit data on mount

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
isUnknown 2025-12-08 18:01:01 +01:00
parent 446b6cd9e7
commit 790eb7414e
17 changed files with 807 additions and 56 deletions

View file

@ -7,22 +7,25 @@ columns:
fields:
type: fields
fields:
author:
blueprint:
type: headline
label: Page "{{ page.intendedTemplate }}"
author:
label: Auteur·ice(s)
type: text
width: 1/2
cover:
cover:
label: Image de couverture
type: files
multiple: false
width: 1/2
introduction:
introduction:
label: Introduction
type: writer
pages:
label: Pages
type: pages
template:
template:
- carte
- geoformat
sidebar:
@ -31,6 +34,3 @@ columns:
files:
label: Fichiers
type: files

View file

@ -0,0 +1,37 @@
<?php
/**
* Virtual Print Page Plugin
*
* Crée une page virtuelle /print pour chaque récit
* Permet d'accéder à l'éditeur d'impression via /projet/recit/print
*/
use Kirby\Cms\Page;
use Kirby\Uuid\Uuid;
Kirby::plugin('geoproject/virtual-print-page', [
'routes' => [
[
'pattern' => '(:all)/print',
'action' => function ($parentPath) {
// Trouver la page parente (le récit)
$parent = page($parentPath);
if (!$parent || $parent->intendedTemplate()->name() !== 'recit') {
return $this->next();
}
// Créer la page virtuelle avec Page::factory()
return Page::factory([
'slug' => 'print',
'template' => 'print',
'parent' => $parent,
'content' => [
'title' => 'Impression - ' . $parent->title()->value(),
'uuid' => Uuid::generate()
]
]);
}
]
]
]);

View file

@ -26,5 +26,5 @@
<?php endif ?>
</head>
<body data-template="<?= $page->template() ?>">
<body data-template="<?= $page->template() ?>"<?php if (isset($recitJsonUrl)): ?> data-recit-url="<?= $recitJsonUrl ?>"<?php endif ?>>
<div id="app">

View file

@ -0,0 +1,17 @@
<?php
/**
* Template pour l'éditeur d'impression Vue.js
* Route: /projet/recit/print
*
* Ce template charge l'app Vue et lui passe l'URL JSON du récit parent
*/
// Récupérer le récit parent
$recit = $page->parent();
// Construire l'URL JSON du récit
$recitJsonUrl = $recit->url() . '.json';
?>
<?php snippet('header', ['recitJsonUrl' => $recitJsonUrl]) ?>
<?php snippet('footer') ?>

View file

@ -0,0 +1,202 @@
<?php
/**
* Template JSON pour exposer les données d'un récit
* Accessible via /projet/recit.json ou /projet/recit?format=json
*/
header('Content-Type: application/json; charset=utf-8');
/**
* Résout les références de fichiers Kirby en URLs absolues
*/
function resolveFileUrl($field, $page) {
if (!$field || $field->isEmpty()) {
return null;
}
$file = $field->toFile();
if ($file) {
return $file->url();
}
return null;
}
/**
* Résout les images dans le contenu HTML
*/
function resolveImagesInHtml($html, $page) {
if (!$html) return $html;
// Remplacer les références file:// par les URLs réelles
return preg_replace_callback(
'/file:\/\/([a-z0-9]+)/i',
function($matches) use ($page) {
$uuid = $matches[1];
$file = $page->file("file://{$uuid}");
if (!$file) {
// Chercher dans les fichiers du site
$file = site()->file("file://{$uuid}");
}
return $file ? $file->url() : $matches[0];
},
$html
);
}
/**
* Parse les blocks Kirby en structure JSON
*/
function parseBlocks($blocksField, $page) {
if (!$blocksField || $blocksField->isEmpty()) {
return [];
}
$blocks = [];
foreach ($blocksField->toBlocks() as $block) {
$blockData = [
'id' => $block->id(),
'type' => $block->type(),
'isHidden' => $block->isHidden(),
];
switch ($block->type()) {
case 'text':
$blockData['content'] = [
'text' => resolveImagesInHtml($block->text()->value(), $page)
];
break;
case 'heading':
$blockData['content'] = [
'level' => $block->level()->value() ?? 'h2',
'text' => $block->text()->value()
];
break;
case 'image':
$image = $block->image()->toFile();
$blockData['content'] = [
'url' => $image ? $image->url() : null,
'alt' => $block->alt()->value() ?? '',
'caption' => $block->caption()->value() ?? '',
'width' => $block->width()->value() ?? '100%',
'position' => $block->position()->value() ?? 'auto'
];
break;
case 'list':
$blockData['content'] = [
'text' => $block->text()->value()
];
break;
case 'quote':
$blockData['content'] = [
'text' => $block->text()->value(),
'citation' => $block->citation()->value() ?? ''
];
break;
case 'video':
$blockData['content'] = [
'url' => $block->url()->value(),
'caption' => $block->caption()->value() ?? ''
];
break;
case 'map':
$blockData['content'] = [
'map' => $block->map()->value()
];
break;
default:
$blockData['content'] = $block->content()->toArray();
}
$blocks[] = $blockData;
}
return $blocks;
}
/**
* Parse un chapitre
*/
function parseChapitre($chapitre) {
return [
'id' => $chapitre->id(),
'uuid' => $chapitre->uuid()->toString(),
'template' => 'chapitre',
'title' => $chapitre->title()->value(),
'slug' => $chapitre->slug(),
'blocks' => parseBlocks($chapitre->text(), $chapitre)
];
}
/**
* Parse une carte
*/
function parseCarte($carte) {
return [
'id' => $carte->id(),
'uuid' => $carte->uuid()->toString(),
'template' => 'carte',
'title' => $carte->title()->value(),
'slug' => $carte->slug(),
'tags' => $carte->tags()->isNotEmpty() ? $carte->tags()->split() : [],
'text' => resolveImagesInHtml($carte->text()->value(), $carte)
];
}
/**
* Parse un geoformat
*/
function parseGeoformat($geoformat) {
$chapitres = [];
foreach ($geoformat->children()->listed() as $child) {
if ($child->intendedTemplate()->name() === 'chapitre') {
$chapitres[] = parseChapitre($child);
}
}
return [
'id' => $geoformat->id(),
'uuid' => $geoformat->uuid()->toString(),
'template' => 'geoformat',
'title' => $geoformat->title()->value(),
'slug' => $geoformat->slug(),
'subtitle' => $geoformat->subtitle()->value() ?? '',
'tags' => $geoformat->tags()->isNotEmpty() ? $geoformat->tags()->split() : [],
'cover' => resolveFileUrl($geoformat->cover(), $geoformat),
'text' => resolveImagesInHtml($geoformat->text()->value(), $geoformat),
'children' => $chapitres
];
}
// Construction de la réponse JSON
$data = [
'id' => $page->id(),
'uuid' => $page->uuid()->toString(),
'template' => 'recit',
'title' => $page->title()->value(),
'slug' => $page->slug(),
'author' => $page->author()->value() ?? '',
'cover' => resolveFileUrl($page->cover(), $page),
'introduction' => resolveImagesInHtml($page->introduction()->value(), $page),
'children' => []
];
// Parser les enfants (geoformats et cartes)
foreach ($page->children()->listed() as $child) {
$template = $child->intendedTemplate()->name();
if ($template === 'geoformat') {
$data['children'][] = parseGeoformat($child);
} elseif ($template === 'carte') {
$data['children'][] = parseCarte($child);
}
}
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

View file

@ -0,0 +1,33 @@
<?php
/**
* Template pour afficher un récit
* Ce template est requis pour que recit.json.php fonctionne
*/
?>
<?php snippet('header') ?>
<article class="recit">
<h1><?= $page->title() ?></h1>
<?php if ($page->author()->isNotEmpty()): ?>
<p class="author"><?= $page->author() ?></p>
<?php endif ?>
<?php if ($page->cover()->isNotEmpty()): ?>
<figure class="cover">
<?php if ($cover = $page->cover()->toFile()): ?>
<img src="<?= $cover->url() ?>" alt="">
<?php endif ?>
</figure>
<?php endif ?>
<?php if ($page->introduction()->isNotEmpty()): ?>
<div class="introduction">
<?= $page->introduction() ?>
</div>
<?php endif ?>
<p><a href="<?= $page->url() ?>/print">Ouvrir l'éditeur d'impression</a></p>
</article>
<?php snippet('footer') ?>