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:
parent
446b6cd9e7
commit
790eb7414e
17 changed files with 807 additions and 56 deletions
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
37
public/site/plugins/virtual-print-page/index.php
Normal file
37
public/site/plugins/virtual-print-page/index.php
Normal 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()
|
||||
]
|
||||
]);
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
|
@ -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">
|
||||
17
public/site/templates/print.php
Normal file
17
public/site/templates/print.php
Normal 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') ?>
|
||||
202
public/site/templates/recit.json.php
Normal file
202
public/site/templates/recit.json.php
Normal 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);
|
||||
33
public/site/templates/recit.php
Normal file
33
public/site/templates/recit.php
Normal 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') ?>
|
||||
Loading…
Add table
Add a link
Reference in a new issue