diff --git a/composer.json b/composer.json
index e877de6..b253f1c 100644
--- a/composer.json
+++ b/composer.json
@@ -22,7 +22,10 @@
},
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
- "getkirby/cms": "^5.2"
+ "getkirby/cms": "^5.2",
+ "belugadigital/kirby-navigation": "^5.0",
+ "tobimori/kirby-seo": "^2.0.0-beta.4",
+ "scottboms/promote-button": "^1.1"
},
"config": {
"allow-plugins": {
diff --git a/composer.lock b/composer.lock
index 9d864ad..0bce64d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,70 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "da1c3a8eb3e9e3b252ad405f32a3f585",
+ "content-hash": "21be036dcf5cc9ba09fe1c94fe04608d",
"packages": [
+ {
+ "name": "belugadigital/kirby-navigation",
+ "version": "5.0.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/chrisbeluga/kirby-navigation.git",
+ "reference": "7ceaca30b37fd2c8ca664b1c046637cabd7f9802"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/chrisbeluga/kirby-navigation/zipball/7ceaca30b37fd2c8ca664b1c046637cabd7f9802",
+ "reference": "7ceaca30b37fd2c8ca664b1c046637cabd7f9802",
+ "shasum": ""
+ },
+ "require": {
+ "getkirby/cms": "^4.0 || ^5.0",
+ "getkirby/composer-installer": "^1.2",
+ "php": ">=8.1.0 <8.6.0"
+ },
+ "type": "kirby-plugin",
+ "extra": {
+ "installer-name": "kirby-navigation"
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Chris Martin",
+ "email": "chris@builtbybeluga.com",
+ "homepage": "https://builtbybeluga.com/"
+ },
+ {
+ "name": "Ahmet Bora",
+ "email": "ahmet@getkirby.com",
+ "homepage": "https://owebstudio.com/"
+ },
+ {
+ "name": "Gabor Horvath"
+ },
+ {
+ "name": "Immo Seebörger",
+ "homepage": "https://diesachbearbeiter.de/"
+ }
+ ],
+ "description": "Kirby 5 field for hierarchical menus with drag & drop level indentation.",
+ "homepage": "https://github.com/chrisbeluga/kirby-navigation",
+ "keywords": [
+ "field",
+ "kirby",
+ "kirby-cms",
+ "kirby-plugin",
+ "menu",
+ "navigation"
+ ],
+ "support": {
+ "issues": "https://github.com/chrisbeluga/kirby-navigation/issues",
+ "source": "https://github.com/chrisbeluga/kirby-navigation/tree/5.0.11"
+ },
+ "time": "2026-04-29T08:06:27+00:00"
+ },
{
"name": "christian-riesen/base32",
"version": "1.6.0",
@@ -721,6 +783,56 @@
},
"time": "2024-09-11T13:17:53+00:00"
},
+ {
+ "name": "scottboms/promote-button",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/scottboms/kirby-promote-button.git",
+ "reference": "5143cd29ea211e286483c31eda2e47238ae81111"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/scottboms/kirby-promote-button/zipball/5143cd29ea211e286483c31eda2e47238ae81111",
+ "reference": "5143cd29ea211e286483c31eda2e47238ae81111",
+ "shasum": ""
+ },
+ "require": {
+ "getkirby/cms": "^5.0",
+ "getkirby/composer-installer": "^1.1",
+ "php": ">8.1.0 <8.4.0"
+ },
+ "type": "kirby-plugin",
+ "extra": {
+ "installer-name": "promote-button"
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Scott Boms",
+ "email": "plugins@scottboms.com",
+ "homepage": "https://scottboms.com"
+ }
+ ],
+ "description": "Promote Panel Button for Kirby.",
+ "homepage": "https://github.com/scottboms/kirby-promote-button",
+ "keywords": [
+ "kirby",
+ "kirby-cms",
+ "kirby-plugin",
+ "kirby5",
+ "kirby5-plugin"
+ ],
+ "support": {
+ "docs": "https://github.com/scottboms/kirby-promote-button/blob/main/README.md",
+ "issues": "https://github.com/scottboms/kirby-promote-button/issues",
+ "source": "https://github.com/scottboms/kirby-promote-button"
+ },
+ "time": "2025-12-24T07:14:57+00:00"
+ },
{
"name": "symfony/deprecation-contracts",
"version": "v3.7.0",
@@ -1207,12 +1319,82 @@
}
],
"time": "2026-03-24T13:12:05+00:00"
+ },
+ {
+ "name": "tobimori/kirby-seo",
+ "version": "2.0.0-beta.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tobimori/kirby-seo.git",
+ "reference": "7a364eaccefc69cf82a164f8eeefd2fa50869985"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/tobimori/kirby-seo/zipball/7a364eaccefc69cf82a164f8eeefd2fa50869985",
+ "reference": "7a364eaccefc69cf82a164f8eeefd2fa50869985",
+ "shasum": ""
+ },
+ "require": {
+ "getkirby/composer-installer": "^1.2.1",
+ "php": ">=8.3.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.48",
+ "getkirby/cli": "^1.8.0",
+ "getkirby/cms": "^5.0.0",
+ "spatie/schema-org": "^3.23",
+ "tobimori/kirby-queues": "^1.0.0-beta.1"
+ },
+ "suggest": {
+ "ext-imagick": "Rasterize non-resizable images (SVG, etc.) for AI alt text generation",
+ "getkirby/cli": "Enable background processing support",
+ "spatie/schema-org": "Enable the Schema.org support",
+ "tobimori/kirby-queues": "Enable background processing support"
+ },
+ "type": "kirby-plugin",
+ "extra": {
+ "kirby-cms-path": false
+ },
+ "autoload": {
+ "psr-4": {
+ "tobimori\\Seo\\": "classes"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "proprietary"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Möritz",
+ "email": "tobias@moeritz.io"
+ }
+ ],
+ "description": "The default choice for SEO on Kirby: Implement technical SEO & Meta best practices with ease and provide an easy-to-use editor experience",
+ "homepage": "https://github.com/tobimori/kirby-seo#readme",
+ "support": {
+ "issues": "https://github.com/tobimori/kirby-seo/issues",
+ "source": "https://github.com/tobimori/kirby-seo/tree/2.0.0-beta.4"
+ },
+ "funding": [
+ {
+ "url": "https://plugins.andkindness.com/seo/preorder",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/tobimori",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-16T15:38:48+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {
+ "tobimori/kirby-seo": 10
+ },
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
diff --git a/content/archives/archives.txt b/content/archives/archives.txt
new file mode 100644
index 0000000..f15f1cd
--- /dev/null
+++ b/content/archives/archives.txt
@@ -0,0 +1,5 @@
+Title: Archives
+
+----
+
+Uuid: txrmlqm6zlaxszpv
\ No newline at end of file
diff --git a/content/archives/by-year/by-year.txt b/content/archives/by-year/by-year.txt
new file mode 100644
index 0000000..04b95e9
--- /dev/null
+++ b/content/archives/by-year/by-year.txt
@@ -0,0 +1,5 @@
+Title: Par année universitaire
+
+----
+
+Uuid: dthnb8i5jun6udbd
\ No newline at end of file
diff --git a/content/career/1_alumni/alumni.txt b/content/career/1_alumni/alumni.txt
new file mode 100644
index 0000000..8ecd775
--- /dev/null
+++ b/content/career/1_alumni/alumni.txt
@@ -0,0 +1,5 @@
+Title: Alumni · Galerie des diplômés
+
+----
+
+Uuid: uslnjr1kcrh0fksh
\ No newline at end of file
diff --git a/content/career/2_resources/resources.txt b/content/career/2_resources/resources.txt
new file mode 100644
index 0000000..767bf20
--- /dev/null
+++ b/content/career/2_resources/resources.txt
@@ -0,0 +1,5 @@
+Title: Ressources
+
+----
+
+Uuid: mtho0strg9viiuxh
\ No newline at end of file
diff --git a/content/career/3_continuing-education/continuing-education.txt b/content/career/3_continuing-education/continuing-education.txt
new file mode 100644
index 0000000..a264134
--- /dev/null
+++ b/content/career/3_continuing-education/continuing-education.txt
@@ -0,0 +1,5 @@
+Title: VAE · Formation continue
+
+----
+
+Uuid: 3ke7tgzd4zoywg1f
\ No newline at end of file
diff --git a/content/career/career.txt b/content/career/career.txt
new file mode 100644
index 0000000..d0f79a7
--- /dev/null
+++ b/content/career/career.txt
@@ -0,0 +1,5 @@
+Title: Insertion professionnelle
+
+----
+
+Uuid: nnt37rdwbcxzvqbs
\ No newline at end of file
diff --git a/content/higher-education/1_cursus-art/cursus-art.txt b/content/higher-education/1_cursus-art/cursus-art.txt
new file mode 100644
index 0000000..a153756
--- /dev/null
+++ b/content/higher-education/1_cursus-art/cursus-art.txt
@@ -0,0 +1,5 @@
+Title: Art · DNA · DNSEP
+
+----
+
+Uuid: ct3w51s4y6c2a1oo
\ No newline at end of file
diff --git a/content/higher-education/2_cursus-design/cursus-design.txt b/content/higher-education/2_cursus-design/cursus-design.txt
new file mode 100644
index 0000000..7c06833
--- /dev/null
+++ b/content/higher-education/2_cursus-design/cursus-design.txt
@@ -0,0 +1,5 @@
+Title: Design · DNA · DNSEP
+
+----
+
+Uuid: vezdljcloi7uqri1
\ No newline at end of file
diff --git a/content/higher-education/3_admissions/admissions.txt b/content/higher-education/3_admissions/admissions.txt
new file mode 100644
index 0000000..727902b
--- /dev/null
+++ b/content/higher-education/3_admissions/admissions.txt
@@ -0,0 +1,5 @@
+Title: Inscriptions / concours
+
+----
+
+Uuid: 3xi7toojd2vk7wbj
\ No newline at end of file
diff --git a/content/higher-education/higher-education.txt b/content/higher-education/higher-education.txt
new file mode 100644
index 0000000..5b821d0
--- /dev/null
+++ b/content/higher-education/higher-education.txt
@@ -0,0 +1,5 @@
+Title: Enseignement supérieur
+
+----
+
+Uuid: 2jl6go9jucceh4ae
\ No newline at end of file
diff --git a/content/home/2_agenda/agenda.txt b/content/home/2_agenda/agenda.txt
new file mode 100644
index 0000000..c207cb5
--- /dev/null
+++ b/content/home/2_agenda/agenda.txt
@@ -0,0 +1,5 @@
+Title: Agenda
+
+----
+
+Uuid: j6tdqcfadwdpuwzx
\ No newline at end of file
diff --git a/content/home/3_starred-events/starred-events.txt b/content/home/3_starred-events/starred-events.txt
new file mode 100644
index 0000000..e94a358
--- /dev/null
+++ b/content/home/3_starred-events/starred-events.txt
@@ -0,0 +1,5 @@
+Title: Événements phares
+
+----
+
+Uuid: 5bflrej3tqifinte
\ No newline at end of file
diff --git a/content/international/international.txt b/content/international/international.txt
new file mode 100644
index 0000000..c670192
--- /dev/null
+++ b/content/international/international.txt
@@ -0,0 +1,5 @@
+Title: International
+
+----
+
+Uuid: ulmylokycamwwg5f
\ No newline at end of file
diff --git a/content/news/1_jean-marc-bullet-designer/actu_jean_marc_bullet.jpg b/content/news/1_jean-marc-bullet-designer/actu_jean_marc_bullet.jpg
new file mode 100644
index 0000000..3a0cf4b
Binary files /dev/null and b/content/news/1_jean-marc-bullet-designer/actu_jean_marc_bullet.jpg differ
diff --git a/content/news/1_jean-marc-bullet-designer/actu_jean_marc_bullet.jpg.txt b/content/news/1_jean-marc-bullet-designer/actu_jean_marc_bullet.jpg.txt
new file mode 100644
index 0000000..9f78756
--- /dev/null
+++ b/content/news/1_jean-marc-bullet-designer/actu_jean_marc_bullet.jpg.txt
@@ -0,0 +1 @@
+Uuid: hjm3ouicarfkieqx
\ No newline at end of file
diff --git a/content/news/1_jean-marc-bullet-designer/news-item.txt b/content/news/1_jean-marc-bullet-designer/news-item.txt
new file mode 100644
index 0000000..97d85de
--- /dev/null
+++ b/content/news/1_jean-marc-bullet-designer/news-item.txt
@@ -0,0 +1,85 @@
+Title: Jean-Marc Bullet, designer
+
+----
+
+Cover: - file://hjm3ouicarfkieqx
+
+----
+
+Date: 2026-05-06
+
+----
+
+Category: visio
+
+----
+
+Infos:
14h à 15h30, salle 245 [VISIO-CONFÉRENCE] avec Jean-Marc Bullet, designer Parcours Salle des fêtes, cycle 1, Design
+
+----
+
+Presentation:
Jean-Marc Bullet est un artiste designer basé en Martinique, dont la pratique se situe à l’intersection de l’innovation sociale et de la préservation de l’environnement. Diplômé de la Haute École des Arts du Rhin (HEAR) de Strasbourg en 2005, puis de l’ENSCI-Les Ateliers à Paris en 2007, il utilise le design comme un levier pour créer du lien et repenser nos modes de vie.
Son travail explore particulièrement la manière dont le design peut renforcer les liens sociaux et culturels au sein d’un territoire, tout en interrogeant nos relations écologiques dans un contexte post-colonial. Plus que la simple recherche de solutions techniques, Jean-Marc Bullet conçoit des dispositifs destinés à engager le dialogue avec les habitants et à faire émerger des projets collaboratifs.
Son parcours international l’a mené en Chine, où il a travaillé dans les industries du marbre et de l’énergie solaire pour réduire l’empreinte écologique des entreprises. Cette expertise environnementale a été récompensée en 2019 par un prix de design allemand pour son projet de composteur, Nestot.
Récemment, ses projets ont acquis une visibilité majeure : • En 2021, il est lauréat du programme « Mondes Nouveaux » pour son projet Presqu’une île, une expérience sensorielle visant à sensibiliser le public aux enjeux écologiques et sociaux de la mangrove.• La même année, il collabore avec l’éditeur américain CB2 pour créer une ligne de mobilier inspirée par l’histoire de la diaspora caribéenne.• Plus récemment, il a été résident à la Villa Albertine à New York, où il a capté la voix de designers africains américains et africains caraibéens sur l’influence de l’histoire et des origines sur la pratique du design dans la societe americaine.
Depuis 2022, il a co-fondé Heritage des Iles qui favorise l’accès de la diaspora antillaise à son matrimoine culturel en recréant le rituel caribéen de l’infusion. Cela comprend des infusions fonctionnels, des produits au cacao, et des accessoires de fabrication locale et artisanale.
+
+```
+
+As you can see, $item->isOpen()->value() can be used in this foreach() to check whether the given menu item is the current page, and $item->isChildOpen()->value() can be used to check whether any of the child menu items is the current page.
+
+## Nesting limit
+
+Nesting limit is set to 10 by default. To adjust the maximum number of levels, use the "levels" option in the blueprint.
+
+## Multi-language support
+
+The plugin supports multiple languages in two ways.
+- In "normal" mode, the navigation field functions like any other content field: you need to add the necessary links to the field for each language, and set the link text and link title for each language (translating). For example, if you have 3 languages and a navigation field with 5 links, you will need to add the 5 links 3 times.
+- In "multilang" mode, add the "multilang: true" option to the field blueprint. This allows you to add the necessary links only once for all languages, and edit the link text and link title separately for each language. For example, if you have 3 languages and a navigation field with 5 links, you will only need to add the 5 links once, and they will be shared across all languages. You can still translate the link text and link title, if needed.
+
+This means that if you simply add 5 Kirby pages to the field in multilang mode and don't edit the links, the link text will be displayed in the language of the current page, and will link to the corresponding page URL in that language.
+
+The plugin allows you to add "Kirby Pages" and "Custom Links" to the navigation field. For "Kirby Pages", the page title will be the default value of the link text, if no custom link text is entered. This means that if you simply add 5 Kirby pages to the field in multilang mode and don't edit the links, you will see the language-specific page title and page URL in the generated markup.
+
+## What's new in version 4.0?
+
+Changes worth mentioning:
+- It works with Kirby 4
+- New feature: Multi-language support, as described earlier
+- New feature: The plugin now uses permanent page IDs to identify pages, instead of using the 'id' (slug) as identification. This allows pages to be renamed or moved without breaking the page links.
+- Data: the structure of the field content has been changed to support permanent page IDs and multiple languages. However, the 4.0 version of the plugin is backwards compatible with the field content saved by the 3.7 version of the plugin.
+- UI: The "edit" button has been moved out of the options dropdown menus.
+- UI: Better icons are now used for editing the links.
+- UI: The 'id' and 'url' values of 'Kirby page' links are no longer editable.
+- Markup: the current language and the actual values of the page title and page URL are taken into account when generating the markup for the field in the template.
+- Markup: link text and link attributes are properly escaped to prevent potential issues
+
+## What's new in version 4.1?
+
+New features:
+- The 'class' and 'target' values of links are now editable
+- The 'anchor' values of 'Kirby page' links are now editable
+- The 'title' textfield can be hidden, if you do not need it
+- The 'popup' toggle can be hidden, if you do not need it
+
+Different sites have different needs, so the editable fields are configurable via /site/config/config.php.
+
+Here are the available options that you can use in your config.php, and their default values:
+
+```php
+return [
+ 'chrisbeluga.navigation' => [
+ 'edit_title' => true,
+ 'edit_popup' => true,
+ 'edit_target' => false,
+ 'edit_class' => false,
+ 'edit_anchor' => false,
+ ],
+];
+```
+
+For example, if you want to customize the 'target' value of your links, then set 'chrisbeluga.navigation.edit_target' to TRUE. This will replace the simple 'Popup' toggle with a 'Target' textfield, allowing you to set a link target, such as '_parent' or '_top'.
+
+If you want to add an anchor value to your 'Kirby page' links, for example to have an URL such as /en/contact#locations, set 'chrisbeluga.navigation.edit_anchor' to TRUE. You can enter 'locations' as anchor, and '#locations' will be appended to the page URL of the link.
+
+If you use the recommended way to output the navigation markup from your template (such as $site->navigation()->toNavigationMarkup() in case of a field called 'navigation'), then any target, class and anchor values will be included automatically in the generated markup.
+
+## What's new in version 5.0?
+
+Works with Kirby 5.0
+
+## Contributing
+
+Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
+
+## License
+
+[MIT](https://choosealicense.com/licenses/mit/)
diff --git a/site/plugins/kirby-navigation/composer.json b/site/plugins/kirby-navigation/composer.json
new file mode 100644
index 0000000..04b1a21
--- /dev/null
+++ b/site/plugins/kirby-navigation/composer.json
@@ -0,0 +1,49 @@
+{
+ "name": "chrisbeluga/kirby-navigation",
+ "description": "Kirby 5 field for hierarchical menus with drag & drop level indentation.",
+ "type": "kirby-plugin",
+ "keywords": [
+ "kirby",
+ "kirby-cms",
+ "kirby-plugin",
+ "navigation",
+ "menu",
+ "field"
+ ],
+ "version": "5.0.11",
+ "license": "MIT",
+ "homepage": "https://github.com/chrisbeluga/kirby-navigation",
+ "authors": [
+ {
+ "name": "Chris Martin",
+ "email": "chris@builtbybeluga.com",
+ "homepage": "https://builtbybeluga.com/"
+ },
+ {
+ "name": "Ahmet Bora",
+ "email": "ahmet@getkirby.com",
+ "homepage": "https://owebstudio.com/"
+ },
+ {
+ "name": "Gabor Horvath"
+ },
+ {
+ "name": "Immo Seebörger",
+ "homepage": "https://diesachbearbeiter.de/"
+ }
+ ],
+ "require": {
+ "php": ">=8.1.0 <8.6.0",
+ "getkirby/cms": "^4.0 || ^5.0",
+ "getkirby/composer-installer": "^1.2"
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "allow-plugins": {
+ "getkirby/composer-installer": true
+ }
+ },
+ "extra": {
+ "installer-name": "kirby-navigation"
+ }
+}
diff --git a/site/plugins/kirby-navigation/config/api.php b/site/plugins/kirby-navigation/config/api.php
new file mode 100644
index 0000000..1d69837
--- /dev/null
+++ b/site/plugins/kirby-navigation/config/api.php
@@ -0,0 +1,83 @@
+ 'listings/([a-zA-Z0-9-]+)/(:all)',
+ 'method' => 'GET',
+ 'action' => function ($language_code, $path) {
+ $content = [];
+ $breadcrumbs = [];
+ $getData = $path !== 'site' ? true : false;
+ $data = $getData ? page($path) : site();
+
+ $multilang = $this->kirby()->languages()->isNotEmpty();
+ if ($multilang) {
+ if ($language_code=='default') {
+ // default language, use item title and url without translation
+ $multilang=false;
+ }
+ elseif (!$this->kirby()->languages()->find($language_code)) {
+ // invalid language, do nothing, just return an empty array
+ $data=null;
+ }
+ }
+ if (($data != null) && $data->hasChildren()) {
+ if ($getData) {
+ foreach ($data->children()->first()->parents()->flip() as $parent) {
+ if ($multilang && ($parent->content($language_code)->title()->value() != null)) {
+ $title=$parent->content($language_code)->title()->value();
+ }
+ else {
+ $title = $parent->title()->value();
+ }
+ array_push($breadcrumbs,[
+ 'id' => $parent->id(),
+ 'title' => $title,
+ ]);
+ }
+ }
+
+ foreach ($data->children() as $item) {
+ $title = $item->title()->value();
+ if ($multilang && ($item->content($language_code)->title()->value() != null)) {
+ $title=$item->content($language_code)->title()->value();
+ }
+ array_push($content, [
+ // Values for page identification
+ 'type' => 'page',
+ 'uuid_uri' => Uuids::enabled() ? $item->uuid()->toString() : '',
+ 'id' => $item->id(),
+ // Language-specific values that may change due to editing
+ $language_code . '_page_url' => $multilang ? $item->url($language_code) : $item->url(),
+ $language_code . '_page_title' => $title,
+ // Default values of page links that prop.php also provides
+ $language_code . '_link_text' => '',
+ $language_code . '_link_title' => '',
+ 'children' => [],
+ 'target' => '',
+ // Temporary helper values that will not be saved
+ 'count' => $item->index()->count(),
+ ]);
+ }
+ }
+ return [
+ 'content' => $content,
+ 'breadcrumbs' => $breadcrumbs,
+ ];
+ }
+ ],
+ ];
+};
diff --git a/site/plugins/kirby-navigation/config/methods.php b/site/plugins/kirby-navigation/config/methods.php
new file mode 100644
index 0000000..9a3d39c
--- /dev/null
+++ b/site/plugins/kirby-navigation/config/methods.php
@@ -0,0 +1,123 @@
+toArray() in case of the navigation field.
+ 'toNavigationArray' => function ($field) {
+ // Refresh items to get the current multilang URL and page titles
+ require __DIR__ . '/../includes/refresh_item.inc.php';
+ $items=$refresh_items($field->yaml(), $field->key(), $field->model());
+ $lang_code=kirby()->languages()->isNotEmpty() ? kirby()->language()->code() : 'default';
+
+ // Use anonymous recursive function to process child items
+ $process_item = function($item, $depth=1) use (&$process_item, $lang_code) {
+ $item['depth']=$depth;
+ // To help markup generation, set 'url', 'text', 'title'
+ // and the optional 'anchor' according to the current language
+ if ($item['type']=='page') {
+ $item['url']=$item[$lang_code . '_page_url'];
+ $item['text']=$item[$lang_code . '_link_text'];
+ $item['title']=$item[$lang_code . '_link_title'];
+ if (isset($item[$lang_code . '_link_anchor'])) {
+ $item['anchor']='#' . $item[$lang_code . '_link_anchor'];
+ }
+ else {
+ $item['anchor']='';
+ }
+ if ($item['text']=='') {
+ $item['text']=$item[$lang_code . '_page_title'];
+ }
+ if ($item['title']=='') {
+ $item['title']=$item[$lang_code . '_page_title'];
+ }
+ }
+ elseif ($item['type']=='custom') {
+ $item['text']=$item[$lang_code . '_link_text'];
+ $item['title']=$item[$lang_code . '_link_title'];
+ }
+ else {
+ $item['error']=TRUE;
+ return $item;
+ }
+ if ($item['title']==$item['text']) {
+ // No need to have a title
+ $item['title']='';
+ }
+ // Values used by previous plugin version
+ $item['isOpen']=kirby()->url('current') === $item['url'];
+ // To help markup generation and customization, put all attributes into an array
+ $item['attributes']=[];
+ if ($item['title']!='') {
+ $item['attributes']['title']=$item['title'];
+ }
+ if (isset($item['class']) && ($item['class']!=='')) {
+ $item['attributes']['class']=$item['class'];
+ }
+ if (isset($item['target']) && ($item['target']!='')) {
+ $item['attributes']['target']=$item['target'];
+ }
+ if ($item['url'] === kirby()->url('current')) {
+ $item['attributes']['aria-current']='page';
+ }
+ // process child items as well, if any
+ $item['isChildOpen']=FALSE;
+ if (!empty($item['children'])) {
+ foreach (array_keys($item['children']) as $key) {
+ $item['children'][$key]=$process_item($item['children'][$key], $depth+1);
+ // keep track whether any child item is active
+ if (!empty($item['children'][$key]['isOpen']) || !empty($item['children'][$key]['isChildOpen'])) {
+ $item['isChildOpen']=TRUE;
+ }
+ }
+ }
+ return $item;
+ };
+ if (is_array($items) && $items) {
+ foreach (array_keys($items) as $key) {
+ $items[$key]=$process_item($items[$key]);
+ }
+ }
+ return $items;
+ },
+ // This method is the preferred way to output the markup of a
+ // navigation field from your template files.
+ 'toNavigationMarkup' => function ($field) {
+ // Refresh items to get the current multilang URL and page titles
+ // and set item values needed for markup generation
+ $items=$field->toNavigationArray();
+ // Generate HTML
+ return snippet('navigation', [
+ 'children' => $items
+ ]);
+ },
+ // This method is provided only for compatibility reasons,
+ // to help the users of older plugin versions
+ 'toNavigationStructure' => function ($field) {
+ // Use anonymous recursive function to process child items
+ $process_item = function($key, $item) use (&$process_item) {
+ // Preserve any original 'id' value as 'link_id'
+ $item['link_id']=$item['id'] ?? '';
+ // Overwrite the 'id' (slug) value in link items,
+ // because the Structure class also uses it as index
+ $item['id']=$key;
+ // process child items as well, if any
+ if (!empty($item['children'])) {
+ foreach (array_keys($item['children']) as $child_key) {
+ $item['children'][$child_key]=$process_item($child_key, $item['children'][$child_key]);
+ }
+ }
+ return $item;
+ };
+ // Refresh items to get the current multilang URL and page titles
+ // and set item values needed for markup generation
+ $items=$field->toNavigationArray();
+ if (is_array($items) && $items) {
+ foreach ($items as $key => $item) {
+ $items[$key]=$process_item($key, $item);
+ }
+ }
+ $modified_field=$field->value(Yaml::encode($items));
+ return $modified_field->toStructure();
+ },
+];
diff --git a/site/plugins/kirby-navigation/config/props.php b/site/plugins/kirby-navigation/config/props.php
new file mode 100644
index 0000000..bc94e25
--- /dev/null
+++ b/site/plugins/kirby-navigation/config/props.php
@@ -0,0 +1,60 @@
+ function ($value = []) {
+ $items=Yaml::decode($value);
+
+ // Check whether there is an item with type 'save',
+ // which indicates that the data was just submitted, and
+ // in this case no changes should be done.
+ // This 'save' item is always added by props.php
+ // This 'save' item is always removed by the save.php
+ // This 'save' trick is needed, because the props.php is not only
+ // executed when a field is loaded, but also when it is saved, and
+ // otherwise it would not be possible to distinguish between the two.
+ $save_in_progress=FALSE;
+ if (is_array($items) && $items) {
+ foreach ($items as $item) {
+ if (isset($item['type']) && ($item['type']==='save')) {
+ // saving is in progress, do nothing.
+ $save_in_progress=TRUE;
+ break;
+ }
+ }
+ }
+ if (!$save_in_progress) {
+ // Refresh items to get the current multilang URL and page titles
+ require __DIR__ . '/../includes/refresh_item.inc.php';
+ $items=$refresh_items($items, $this->name(), $this->model());
+ // Add a 'save' item. When the 'props' values are sent to Panel,
+ // the Field.vue will hide this item, but it will send it back,
+ // and then the props.php will detect it, and save.php will ignore it.
+ // Use a constant uuid value for this item, to avoid diff problems
+ $items[]=['type' => 'save', 'uuid' => 'save', 'children' => []];
+ }
+ return $items;
+ },
+ // Notify the Vue code which textfields should be editable by default.
+ // Notes:
+ // - The 'Popup' will be automatically hidden, if 'Target' is visible
+ // - If a certain value is set in the field, then Vue will show
+ // the editable textfield even if the corresponding option is false
+ // - These options can be set in /site/config/config.php
+ 'edit_title' => function() {
+ return (bool) option('chrisbeluga.navigation.edit_title', true);
+ },
+ 'edit_popup' => function() {
+ return (bool) option('chrisbeluga.navigation.edit_popup', true);
+ },
+ 'edit_target' => function() {
+ return (bool) option('chrisbeluga.navigation.edit_target', false);
+ },
+ 'edit_class' => function() {
+ return (bool) option('chrisbeluga.navigation.edit_class', false);
+ },
+ 'edit_anchor' => function() {
+ return (bool) option('chrisbeluga.navigation.edit_anchor', false);
+ }
+];
diff --git a/site/plugins/kirby-navigation/config/save.php b/site/plugins/kirby-navigation/config/save.php
new file mode 100644
index 0000000..e13aaf4
--- /dev/null
+++ b/site/plugins/kirby-navigation/config/save.php
@@ -0,0 +1,78 @@
+model()->blueprint()->field($this->name());
+ if (!empty($blueprint_field) && !empty($blueprint_field['multilang'])) {
+ // If 'multilang' is set in the blueprint, then data is stored
+ // in external yaml file, so that it can be shared between languages
+ $filepathTMP = tempnam(sys_get_temp_dir(), 'navigation');
+ if (file_put_contents($filepathTMP, Yaml::encode($items))) {
+ if (rename($filepathTMP, $this->model()->root() . '/kirby-navigation---' . $this->name() . '.yml')) {
+ // The data was successfully saved to the external file,
+ // so change the field data to a simple flag that will tell
+ // the load function to load the external file.
+ $items=['multilang' => TRUE];
+ }
+ else {
+ }
+ }
+ else {
+ }
+ @unlink($filepathTMP);
+ }
+ return $items;
+};
diff --git a/site/plugins/kirby-navigation/includes/refresh_item.inc.php b/site/plugins/kirby-navigation/includes/refresh_item.inc.php
new file mode 100644
index 0000000..d44c305
--- /dev/null
+++ b/site/plugins/kirby-navigation/includes/refresh_item.inc.php
@@ -0,0 +1,252 @@
+languages()->isNotEmpty()) {
+ $language_code=kirby()->language()->code();
+ $default_language_code=kirby()->defaultLanguage()->code();
+ }
+ else {
+ $language_code='default';
+ $default_language_code='default';
+ }
+ // Upgrade from old data, to remain compatible with old plugin versions
+ if (!isset($item['type'])) {
+ // Add a 'type' value, that did not exist in the old plugin version
+ // Previous plugin versions stored Kirby links with 'id' values,
+ // and stored Custom links without that
+ $item['type'] = isset($item['id']) ? 'page' : 'custom';
+ if ($item['type']=='page') {
+ // Previous plugin versions used 'text' and 'title' values.
+ // The new version uses 'LANG_link_text', 'LANG_link_title',
+ // and 'LANG_page_title', where 'LANG' is the language code.
+ // The new version outputs the 'LANG_page_title' as link HTML,
+ // if the 'LANG_link_text' value is empty.
+ if (isset($item['text'])) {
+ $item[$default_language_code . '_link_text']=$item['text'];
+ unset($item['text']);
+ }
+ if (isset($item['title'])) {
+ $item[$default_language_code . '_link_title']=$item['title'];
+ unset($item['title']);
+ }
+ // Previous plugin versions used 'url' value.
+ // The new version uses 'LANG_page_url',
+ // where 'LANG' is the language code.
+ if (isset($item['url'])) {
+ $item[$default_language_code . '_page_url']=$item['url'];
+ unset($item['url']);
+ }
+ }
+ elseif ($item['type']=='custom') {
+ // Previous plugin versions used 'text' and 'title' values.
+ // The new version uses 'LANG_link_text', 'LANG_link_title',
+ // where 'LANG' is the language code.
+ if (isset($item['text'])) {
+ $item[$default_language_code . '_link_text']=$item['text'];
+ unset($item['text']);
+ }
+ if (isset($item['title'])) {
+ $item[$default_language_code . '_link_title']=$item['title'];
+ unset($item['title']);
+ }
+ }
+ }
+ if ($item['type']=='page') {
+ // Fetch page by the permanent 'uuid_uri', if possible, otherwise by 'id'
+ $page=null;
+ if (!empty($item['uuid_uri']) && Uuids::enabled()) {
+ if ($page=kirby()->page($item['uuid_uri'])) {
+ // Refresh the 'id' to handle any changes
+ $item['id'] = $page->id();
+ }
+ }
+ if (!$page) {
+ if ($page=kirby()->page($item['id'])) {
+ if (empty($item['uuid_uri']) && Uuids::enabled()) {
+ // Add a 'uuid_uri' value, that did not exist in the old plugin version
+ $item['uuid_uri'] = $page->uuid()->toString();
+ }
+ }
+ }
+ if ($page) {
+ // Refresh the 'url' and 'page_title' (in the current language)
+ // to handle any changes
+ // Remember, that a navigation field built on e.g. localhost
+ // should work perfectly when copied to the public site.
+ $item[$language_code . '_page_url'] = $multilang ? $page->url($language_code) : $page->url();
+ $item[$language_code . '_page_title'] = $multilang ? ($page->content($language_code)->title()->value() ?? ''): ($page->content()->title()->value() ?? '');
+ }
+ else {
+ // This page no longer exists.
+ // Set 'error' so that the item icon in panel will be 'question'
+ $item['error']=TRUE;
+
+ // Adjust the old url to the current site url using the latest known 'id' value
+ if ($multilang) {
+ kirby()->currentLanguage()->url() . '/' . $item['id'];
+ }
+ else {
+ $item[$language_code . '_page_url']=Uri::current(['path' => $item['id'], 'query' => '',])->toString();
+ }
+ }
+ // if no translation exists yet, add default values
+ if (!isset($item[$language_code . '_link_text'])) {
+ $item[$language_code . '_link_text']='';
+ }
+ if (!isset($item[$language_code . '_link_title'])) {
+ $item[$language_code . '_link_title']='';
+ }
+ if (option('chrisbeluga.navigation.edit_anchor')) {
+ if (!isset($item[$language_code . '_link_anchor'])) {
+ $item[$language_code . '_link_anchor']='';
+ }
+ }
+ }
+ elseif ($item['type']=='custom') {
+ // Validate the URL
+ if (empty($item['url']) || !V::url($item['url'])) {
+ $item['error'] = TRUE;
+ }
+ // if no translation exists yet, add default values
+ if (!isset($item[$language_code . '_link_text'])) {
+ $item[$language_code . '_link_text']='';
+ }
+ if (!isset($item[$language_code . '_link_title'])) {
+ $item[$language_code . '_link_title']='';
+ }
+ }
+ // Handle the popup -> target transition to remain compatible.
+ // Previous plugin versions used the 'popup' boolean value.
+ // The new version uses the 'target' string value.
+ // The new version uses the 'Popup' toggle UI by default,
+ // but provides the edit_target option to switch to 'Target' string UI.
+ if (isset($item['popup']) && !isset($item['target'])) {
+ $item['target']=$item['popup'] ? '_blank' : '';
+ }
+ unset($item['popup']);
+
+ // refresh child items, if any
+ if (!empty($item['children'])) {
+ foreach (array_keys($item['children']) as $key) {
+ $item['children'][$key]=$refresh_item($item['children'][$key]);
+ }
+ }
+ return $item;
+};
+
+$refresh_items = function($items, $field_name, $field_model) use ($refresh_item) {
+ // If 'multilang' is set in the field data, then the real data is stored
+ // in external yaml file, so that it can be shared between languages
+ // It is better to be prepared that the 'multilang' field option is
+ // enabled/disabled while
+ // - there is field data in primary or secondary languages
+ // - there is ['multilang' => TRUE] data in primary field data,
+ // but there is real field data in secondary language
+ // - there is real field data in primary language, but there is
+ // ['multilang' => TRUE] data in secondary language
+ // - the blueprint contains 'multilang: true', the primary language
+ // data is still stored in the page content file, not in external file
+ // (because it was not saved yet), but the field is edited in the
+ // secondary language in Panel.
+ // - and all kinds of similar cases.
+ //
+ $multilang_site=kirby()->languages()->isNotEmpty();
+ $multilang_blueprint=FALSE;
+ $multilang_field=FALSE;
+ $multilang_load=FALSE;
+ // Get the 'multilang' option of the field blueprint
+ $field_blueprint=$field_model->blueprint()->field($field_name);
+ if (!empty($field_blueprint) && !empty($field_blueprint['multilang'])) {
+ $multilang_blueprint=TRUE;
+ }
+ if (!empty($items['multilang'])) {
+ $multilang_field=TRUE;
+ }
+
+ if (!$multilang_blueprint && !$multilang_field) {
+ // normal operation: use $items
+ }
+ elseif ($multilang_blueprint && $multilang_field) {
+ // multilang operation with previous save: load from external file
+ $multilang_load=TRUE;
+ }
+ elseif ($multilang_blueprint && !$multilang_field) {
+ // multilang operation without previous save:
+ // - if site is multilang, and this is primary language: use $items
+ // - if site is multilang, and this is secondary language:
+ // load data from the primary language
+ // - if site has no languages: use $items
+ if (kirby()->multilang()) {
+ if (kirby()->language()->isDefault()) {
+ // good, this is easy!
+ }
+ else {
+ // The field data should be loaded from the primary language.
+ $model=$field_model;
+ $defaultContentTranslation=$model->translation(kirby()->defaultLanguage()->code());
+ $defaultContent=$defaultContentTranslation->content();
+ $fieldContent=$defaultContent[$field_name];
+ $items=Yaml::decode($fieldContent);
+
+ // Furthermore, the primary data may be ['multilang' => TRUE],
+ // instead of the real field data. Load the external file then.
+ if (is_array($items) && !empty($items['multilang'])) {
+ $multilang_load=TRUE;
+ }
+ }
+ }
+ else {
+ // good, this is easy!
+ }
+ }
+ else {
+ // not multilang operation, but data happens to be saved externally,
+ // so load it from there
+ $multilang_load=TRUE;
+ }
+ if ($multilang_load) {
+ $filepath=$field_model->root() . '/kirby-navigation---' . $field_name . '.yml';
+ if (!file_exists($filepath)) {
+ throw new Exception('Failed to load the navigation field data.' );
+ }
+ $contents=file_get_contents($filepath);
+ if ($contents===FALSE) {
+ throw new Exception('Failed to load the navigation field data.');
+ }
+ $items=Yaml::decode($contents);
+ if (!is_array($items)) {
+ $items=[];
+ }
+ }
+
+ if (is_array($items) && $items) {
+ foreach (array_keys($items) as $key) {
+ $items[$key]=$refresh_item($items[$key]);
+ }
+ }
+ return $items;
+};
+
diff --git a/site/plugins/kirby-navigation/index.css b/site/plugins/kirby-navigation/index.css
new file mode 100644
index 0000000..fe29716
--- /dev/null
+++ b/site/plugins/kirby-navigation/index.css
@@ -0,0 +1 @@
+.k-form-input[data-v-e9c938c8]{width:100%;display:flex;position:relative;margin-bottom:2px}.k-form-input .k-form-inner[data-v-e9c938c8]{width:100%;display:flex;position:relative;align-items:center;justify-content:space-between;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.25rem;padding:.5rem .75rem}.k-form-input .k-menu-text[data-v-e9c938c8]{overflow:hidden;text-overflow:ellipsis}.k-form-input .k-form-actions[data-v-e9c938c8]{display:flex;align-items:center}.k-form-input .k-form-actions button[data-v-e9c938c8]{margin-left:20px}.k-form-input[data-v-34431234]{width:100%;display:flex;position:relative;margin-bottom:2px}.k-form-input .k-form-inner[data-v-34431234]{width:100%;display:flex;position:relative;flex-direction:column}.k-form-input .k-form-inner .k-form-config[data-v-34431234]{width:100%;border-top:0;display:flex;border:1px solid #ccc;flex-direction:column;margin-top:5px}.k-form-input .k-form-inner .k-form-config .k-form-group[data-v-34431234]{flex-grow:1;padding:1rem;display:flex;flex-direction:column}.k-form-input .k-form-inner .k-form-config .k-form-footer[data-v-34431234]{width:100%;display:flex;margin-top:2rem;align-items:center;padding:.6rem 1rem;border-top:1px solid #ccc;justify-content:space-between}.k-form-input .k-form-actions[data-v-34431234]{display:flex;flex-direction:column}.k-form-input .k-form-actions .input-handle[data-v-34431234]{padding:0 .4rem 1rem}.k-pages-dialog-navbar[data-v-571b2066]{display:flex;align-items:center;justify-content:center;margin-bottom:.5rem;padding-right:38px}.k-pages-dialog-navbar .k-button[data-v-571b2066]{width:38px}.k-pages-dialog-navbar .k-button[disabled][data-v-571b2066]{opacity:0}.k-pages-dialog-navbar .k-headline[data-v-571b2066]{flex-grow:1;text-align:center}.k-pages-dialog .k-list-item[data-v-571b2066]{cursor:pointer}.k-pages-dialog .k-list-item .k-button[data-theme=disabled][data-v-571b2066],.k-pages-dialog .k-list-item .k-button[disabled][data-v-571b2066]{opacity:.25}.k-pages-dialog .k-list-item .k-button[data-theme=disabled][data-v-571b2066]:hover{opacity:1}.navigation-field .k-field-depth{text-align:right}.navigation-field .k-field-header .k-dropdown-content{margin-top:10px}.navigation-field .k-field-header .k-dropdown-item{width:180px;height:auto;padding:8px;margin-bottom:8px}.navigation-field .k-field-header .k-dropdown-item .k-button-text{opacity:1;white-space:normal;text-align:left}.navigation-field .k-field-header .k-dropdown-item .k-button-text .k-menu-title{opacity:1;width:100%;display:block;margin-bottom:8px}.navigation-field .k-field-header .k-dropdown-item .k-button-text .k-menu-subtitle{opacity:.75;font-size:.675rem;line-height:.875rem}.navigation-field .nestable-handle{width:100%;display:block}.navigation-field .nestable-item-content{width:100%;display:flex;flex-wrap:nowrap;position:relative;align-items:center}.navigation-field .nestable-item-content:hover .nestable-handle .k-button{opacity:1;transition:all .3s ease-in-out}.navigation-field .nestable-handle{width:auto;height:auto;display:flex;flex-wrap:nowrap;position:relative;align-items:flex-start;margin-top:7px}.navigation-field .nestable-handle .k-button{opacity:.2;cursor:move;transition:all .3s ease-in-out}.navigation-field .nestable{position:relative}.navigation-field .nestable .k-column{margin-top:8px;margin-right:8px}.navigation-field .nestable .nestable-list{margin:0;padding:0 0 0 26px;list-style-type:none}.navigation-field .nestable>.nestable-list{padding:0}.navigation-field .nestable-item:first-child,.navigation-field .nestable-item-copy:first-child{margin-top:0}.navigation-field .nestable-item{position:relative}.navigation-field .nestable-item.is-dragging .nestable-list{pointer-events:none}.navigation-field .nestable-item.is-dragging *{opacity:0}.navigation-field .nestable-item.is-dragging:before{content:"";position:absolute;top:0;left:26px;right:0;bottom:0;transition:all .3s ease-in-out}.navigation-field .nestable-drag-layer{position:fixed;top:0;left:0;z-index:100;pointer-events:none}.navigation-field .nestable-rtl .nestable-drag-layer{left:auto;right:0}.navigation-field .nestable-handle{cursor:move}
diff --git a/site/plugins/kirby-navigation/index.js b/site/plugins/kirby-navigation/index.js
new file mode 100644
index 0000000..3d7cf06
--- /dev/null
+++ b/site/plugins/kirby-navigation/index.js
@@ -0,0 +1,6 @@
+!function(){"use strict";function t(t,e,n,i,o,r,a,s,l,u){"boolean"!=typeof a&&(l=s,s=a,a=!1);const c="function"==typeof n?n.options:n;let d;if(t&&t.render&&(c.render=t.render,c.staticRenderFns=t.staticRenderFns,c._compiled=!0,o&&(c.functional=!0)),i&&(c._scopeId=i),r?(d=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),e&&e.call(this,l(t)),t&&t._registeredComponents&&t._registeredComponents.add(r)},c._ssrRegister=d):e&&(d=a?function(t){e.call(this,u(t,this.$root.$options.shadowRoot))}:function(t){e.call(this,s(t))}),d)if(c.functional){const t=c.render;c.render=function(e,n){return d.call(n),t(e,n)}}else{const t=c.beforeCreate;c.beforeCreate=t?[].concat(t,d):[d]}return n}function e(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var n={exports:{}};!function(t,e){function n(t){return"object"!=typeof t||"toString"in t?t:Object.prototype.toString.call(t).slice(8,-1)}Object.defineProperty(e,"__esModule",{value:!0});var i="object"==typeof process&&!0;function o(t,e){if(!t){if(i)throw new Error("Invariant failed");throw new Error(e())}}e.invariant=o;var r=Object.prototype.hasOwnProperty,a=Array.prototype.splice,s=Object.prototype.toString;function l(t){return s.call(t).slice(8,-1)}var u=Object.assign||function(t,e){return c(e).forEach((function(n){r.call(e,n)&&(t[n]=e[n])})),t},c="function"==typeof Object.getOwnPropertySymbols?function(t){return Object.keys(t).concat(Object.getOwnPropertySymbols(t))}:function(t){return Object.keys(t)};function d(t){return Array.isArray(t)?u(t.constructor(t.length),t):"Map"===l(t)?new Map(t):"Set"===l(t)?new Set(t):t&&"object"==typeof t?u(Object.create(Object.getPrototypeOf(t)),t):t}var p=function(){function t(){this.commands=u({},h),this.update=this.update.bind(this),this.update.extend=this.extend=this.extend.bind(this),this.update.isEquals=function(t,e){return t===e},this.update.newContext=function(){return(new t).update}}return Object.defineProperty(t.prototype,"isEquals",{get:function(){return this.update.isEquals},set:function(t){this.update.isEquals=t},enumerable:!0,configurable:!0}),t.prototype.extend=function(t,e){this.commands[t]=e},t.prototype.update=function(t,e){var n=this,i="function"==typeof e?{$apply:e}:e;Array.isArray(t)&&Array.isArray(i)||o(!Array.isArray(i),(function(){return"update(): You provided an invalid spec to update(). The spec may not contain an array except as the value of $set, $push, $unshift, $splice or any custom command allowing an array value."})),o("object"==typeof i&&null!==i,(function(){return"update(): You provided an invalid spec to update(). The spec and every included key path must be plain objects containing one of the following commands: "+Object.keys(n.commands).join(", ")+"."}));var a=t;return c(i).forEach((function(e){if(r.call(n.commands,e)){var o=t===a;a=n.commands[e](i[e],a,i,t),o&&n.isEquals(a,t)&&(a=t)}else{var s="Map"===l(t)?n.update(t.get(e),i[e]):n.update(t[e],i[e]),u="Map"===l(a)?a.get(e):a[e];n.isEquals(s,u)&&(void 0!==s||r.call(t,e))||(a===t&&(a=d(t)),"Map"===l(a)?a.set(e,s):a[e]=s)}})),a},t}();e.Context=p;var h={$push:function(t,e,n){return m(e,n,"$push"),t.length?e.concat(t):e},$unshift:function(t,e,n){return m(e,n,"$unshift"),t.length?t.concat(e):e},$splice:function(t,e,i,r){return function(t,e){o(Array.isArray(t),(function(){return"Expected $splice target to be an array; got "+n(t)})),y(e.$splice)}(e,i),t.forEach((function(t){y(t),e===r&&t.length&&(e=d(r)),a.apply(e,t)})),e},$set:function(t,e,n){return function(t){o(1===Object.keys(t).length,(function(){return"Cannot have more than one key in an object with $set"}))}(n),t},$toggle:function(t,e){g(t,"$toggle");var n=t.length?d(e):e;return t.forEach((function(t){n[t]=!e[t]})),n},$unset:function(t,e,n,i){return g(t,"$unset"),t.forEach((function(t){Object.hasOwnProperty.call(e,t)&&(e===i&&(e=d(i)),delete e[t])})),e},$add:function(t,e,n,i){return v(e,"$add"),g(t,"$add"),"Map"===l(e)?t.forEach((function(t){var n=t[0],o=t[1];e===i&&e.get(n)!==o&&(e=d(i)),e.set(n,o)})):t.forEach((function(t){e!==i||e.has(t)||(e=d(i)),e.add(t)})),e},$remove:function(t,e,n,i){return v(e,"$remove"),g(t,"$remove"),t.forEach((function(t){e===i&&e.has(t)&&(e=d(i)),e.delete(t)})),e},$merge:function(t,e,i,r){var a,s;return a=e,o((s=t)&&"object"==typeof s,(function(){return"update(): $merge expects a spec of type 'object'; got "+n(s)})),o(a&&"object"==typeof a,(function(){return"update(): $merge expects a target of type 'object'; got "+n(a)})),c(t).forEach((function(n){t[n]!==e[n]&&(e===r&&(e=d(r)),e[n]=t[n])})),e},$apply:function(t,e){var i;return o("function"==typeof(i=t),(function(){return"update(): expected spec of $apply to be a function; got "+n(i)+"."})),t(e)}},f=new p;function m(t,e,i){o(Array.isArray(t),(function(){return"update(): expected target of "+n(i)+" to be an array; got "+n(t)+"."})),g(e[i],i)}function g(t,e){o(Array.isArray(t),(function(){return"update(): expected spec of "+n(e)+" to be an array; got "+n(t)+". Did you forget to wrap your parameter in an array?"}))}function y(t){o(Array.isArray(t),(function(){return"update(): expected spec of $splice to be an array of arrays; got "+n(t)+". Did you forget to wrap your parameters in an array?"}))}function v(t,e){var i=l(t);o("Map"===i||"Set"===i,(function(){return"update(): "+n(e)+" expects a target of type Set or Map; got "+n(i)}))}e.isEquals=f.update.isEquals,e.extend=f.extend,e.default=f.update,e.default.default=t.exports=u(e.default,e)}(n,n.exports);var i=e(n.exports);
+/*!
+ * vue-nestable v2.6.0
+ * (c) Ralph Huwiler
+ * Released under the MIT License.
+ */function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function s(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function l(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,i=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[i++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,a=!0,s=!1;return{s:function(){n=t[Symbol.iterator]()},n:function(){var t=n.next();return a=t.done,t},e:function(t){s=!0,r=t},f:function(){try{a||null==n.return||n.return()}finally{if(s)throw r}}}}var h={},f={methods:{registerNestable:function(t){var e=this._getByGroup(t.group);e.onDragStartListeners.push(t.onDragStart),e.onMouseEnterListeners.push(t.onMouseEnter),e.onMouseMoveListeners.push(t.onMouseMove)},notifyDragStart:function(t,e,n){var i,o=p(this._getByGroup(t).onDragStartListeners);try{for(o.s();!(i=o.n()).done;){(0,i.value)(e,n)}}catch(r){o.e(r)}finally{o.f()}},notifyMouseEnter:function(t,e,n,i){var o,r=p(this._getByGroup(t).onMouseEnterListeners);try{for(r.s();!(o=r.n()).done;){(0,o.value)(e,n,i)}}catch(a){r.e(a)}finally{r.f()}},notifyMouseMove:function(t,e){var n,i=p(this._getByGroup(t).onMouseMoveListeners);try{for(i.s();!(n=i.n()).done;){(0,n.value)(e)}}catch(o){i.e(o)}finally{i.f()}},_getByGroup:function(t){return h[t]||(h[t]={onDragStartListeners:[],onMouseEnterListeners:[],onMouseMoveListeners:[],onDragStart:[],dragItem:null}),h[t]}}},m={name:"NestableItem",mixins:[f],props:{item:{type:Object,required:!0,default:function(){return{}}},index:{type:Number,required:!1,default:null},isChild:{type:Boolean,required:!1,default:!1},isCopy:{type:Boolean,required:!1,default:!1},options:{type:Object,required:!0,default:function(){return{}}}},inject:["listId","group","keyProp"],data:function(){return{breakPoint:null,moveDown:!1}},computed:{isDragging:function(){var t=this.options.dragItem;return!this.isCopy&&t&&t[this.options.keyProp]===this.item[this.options.keyProp]},hasChildren:function(){return this.item[this.options.childrenProp]&&this.item[this.options.childrenProp].length>0},hasHandle:function(){return!!this.$scopedSlots.handler},normalizedClassProp:function(){var t=this.item[this.options.classProp];return t?Array.isArray(t)?t:("undefined"==typeof a||o(a),[t]):[]},itemClasses:function(){var t=this.isDragging?["is-dragging"]:[];return["nestable-item".concat(this.isCopy?"-copy":""),"nestable-item".concat(this.isCopy?"-copy":"","-").concat(this.item[this.options.keyProp])].concat(t,u(this.normalizedClassProp))}},methods:{onMouseEnter:function(t){if(this.options.dragItem){if(!t.movementY)return this.sendNotification(t);this.moveDown=t.movementY>0,this.breakPoint=t.target.getBoundingClientRect().height/2}},onMouseLeave:function(){this.breakPoint=null},onMouseMove:function(t){if(this.breakPoint){var e=t.offsetY-this.breakPoint;this.moveDown&&e-this.breakPoint/4||this.sendNotification(t)}},sendNotification:function(t){this.breakPoint=null;var e=this.item||this.$parent.item;this.notifyMouseEnter(this.group,t,this.listId,e)}}},g={methods:{getPathById:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.value,i=[];return n.every((function(n,o){if(n[e.keyProp]===t)i.push(o);else if(n[e.childrenProp]){var r=e.getPathById(t,n[e.childrenProp]);r.length&&(i=i.concat(o).concat(r))}return 0===i.length})),i},getItemByPath:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.value,i=null;return t.forEach((function(t){var o=i&&i[e.childrenProp]?i[e.childrenProp]:n;i=o[t]})),i},getItemDepth:function(t){var e=1;if(t[this.childrenProp]&&t[this.childrenProp].length>0){var n=t[this.childrenProp].map(this.getItemDepth);e+=Math.max.apply(Math,u(n))}return e},getSplicePath:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n={},i=e.numToRemove||0,o=e.itemsToInsert||[],a=t.length-1,s=n;return t.forEach((function(t,n){if(n===a)s.$splice=[[t,i].concat(u(o))];else{var l={};s[t]=r({},e.childrenProp,l),s=l}})),n},getRealNextPath:function(t,e){var n=t.length-1,i=e.length-1;if(t.lengtht[a]&&a===n?(o=!0,r-1):r}))}if(t.length===e.length&&e[i]>t[i]){var r=this.getItemByPath(e);if(r[this.childrenProp]&&r[this.childrenProp].length&&!this.isCollapsed(r))return e.slice(0,-1).concat(e[i]-1).concat(0)}return e}}},y=function t(e,n){return e.map((function(e){return l(l({},e),{},r({},n,e[n]?t(e[n],n):[]))}))},v=t({render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{class:["nestable","nestable-"+t.group,t.rtl?"nestable-rtl":""]},[n("ol",{staticClass:"nestable-list nestable-group"},[t.listIsEmpty?n("Placeholder",{attrs:{options:t.itemOptions}},[t._t("placeholder",[t._v("\n No content\n ")])],2):t._e(),t._v(" "),t._l(t.value,(function(e,i){return[n("NestableItem",{key:e[t.keyProp],attrs:{index:i,item:e,options:t.itemOptions},scopedSlots:t._u([t._l(Object.keys(t.$scopedSlots),(function(e){return{key:e,fn:function(n){return[t._t(e,null,null,n)]}}}))],null,!0)})]}))],2),t._v(" "),t.dragItem?[n("div",{staticClass:"nestable-drag-layer"},[n("ol",{staticClass:"nestable-list",style:t.listStyles},[n("NestableItem",{attrs:{item:t.dragItem,options:t.itemOptions,"is-copy":!0},scopedSlots:t._u([t._l(Object.keys(t.$scopedSlots),(function(e){return{key:e,fn:function(n){return[t._t(e,null,null,n)]}}}))],null,!0)})],1)])]:t._e()],2)},staticRenderFns:[]},undefined,{name:"VueNestable",components:{NestableItem:t({render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("li",{class:t.itemClasses},[n("div",{staticClass:"nestable-item-content",on:{mouseenter:t.onMouseEnter,mouseleave:t.onMouseLeave,mousemove:t.onMouseMove}},[t._t("default",null,{index:t.index,item:t.item,isChild:t.isChild})],2),t._v(" "),t.hasChildren?n("ol",{staticClass:"nestable-list"},[t._l(t.item[t.options.childrenProp],(function(e,i){return[n("NestableItem",{key:e[t.keyProp],attrs:{item:e,index:i,options:t.options,"is-copy":t.isCopy,"is-child":""},scopedSlots:t._u([t._l(Object.keys(t.$scopedSlots),(function(e){return{key:e,fn:function(n){return[t._t(e,null,null,n)]}}}))],null,!0)})]}))],2):t._e()])},staticRenderFns:[]},undefined,m,undefined,false,undefined,!1,void 0,void 0,void 0),Placeholder:t({render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("li",[n("div",{staticClass:"nestable-list-empty",on:{mouseenter:t.onMouseEnter}},[t._t("default")],2)])},staticRenderFns:[]},undefined,{name:"Placeholder",mixins:[f],props:{index:{type:Number,required:!1,default:null},options:{type:Object,required:!1,default:function(){return{}}}},inject:["listId","group"],computed:{isDragging:function(){return this.options.dragItem}},methods:{onMouseEnter:function(t){this.options.dragItem&&this.notifyMouseEnter(this.group,t,this.listId,null)}}},undefined,false,undefined,!1,void 0,void 0,void 0)},mixins:[g,f,{methods:{hook:function(t,e){if(!this.hooks[t])return!0;var n=this.hooks[t](e);return n||void 0===n}}}],props:{value:{type:Array,required:!0,default:function(){return[]}},threshold:{type:Number,required:!1,default:30},maxDepth:{type:Number,required:!1,default:10},keyProp:{type:String,required:!1,default:"id"},classProp:{type:String,required:!1,default:null},group:{type:[String,Number],required:!1,default:function(){return Math.random().toString(36).slice(2)}},childrenProp:{type:String,required:!1,default:"children"},collapsed:{type:Boolean,required:!1,default:!1},hooks:{type:Object,required:!1,default:function(){return{}}},rtl:{type:Boolean,required:!1,default:!1}},provide:function(){return{listId:this.listId,group:this.group,keyProp:this.keyProp,onDragEnd:this.onDragEnd}},data:function(){return{itemsOld:null,dragItem:null,mouse:{last:{x:0},shift:{x:0}},el:null,elCopyStyles:null,isDirty:!1,collapsedGroups:[],listId:Math.random().toString(36).slice(2)}},computed:{listIsEmpty:function(){return 0===this.value.length},itemOptions:function(){return{dragItem:this.dragItem,keyProp:this.keyProp,classProp:this.classProp,childrenProp:this.childrenProp}},listStyles:function(){var t=document.querySelector(".nestable-"+this.group+" .nestable-item-"+this.dragItem[this.keyProp]),e={};return t&&(e.width="".concat(t.clientWidth,"px")),this.elCopyStyles&&(e=l(l({},e),this.elCopyStyles)),e}},created:function(){var t=y(this.value,this.childrenProp);this.$emit("input",t),this.isDirty=!1,this.registerNestable(this)},beforeDestroy:function(){this.stopTrackMouse()},methods:{startTrackMouse:function(){document.addEventListener("mousemove",this.onMouseMove),document.addEventListener("mouseup",this.onDragEnd),document.addEventListener("touchend",this.onDragEnd),document.addEventListener("touchcancel",this.onDragEnd),document.addEventListener("keydown",this.onKeyDown)},stopTrackMouse:function(){document.removeEventListener("mousemove",this.onMouseMove),document.removeEventListener("mouseup",this.onDragEnd),document.removeEventListener("touchend",this.onDragEnd),document.removeEventListener("touchcancel",this.onDragEnd),document.removeEventListener("keydown",this.onKeyDown),this.elCopyStyles=null},onDragStart:function(t,e){var n,i,o=this;t&&(t.preventDefault(),t.stopPropagation()),this.el=(n=t.target,i=".nestable-item",n.closest(i)),this.startTrackMouse(),this.dragItem=e,this.itemsOld=this.value,this.$nextTick((function(){o.onMouseMove(t)}))},onDragEnd:function(t,e){t&&t.preventDefault(),this.stopTrackMouse(),this.el=null,e?this.dragRevert():this.dragApply()},onKeyDown:function(t){27===t.which&&this.onDragEnd(null,!0)},getXandYFromEvent:function(t){var e=t.clientX,n=t.clientY,i=t.targetTouches;if(i){var o=i[0];e=o.clientX,n=o.clientY;var r=new Event("mouseenter"),a=document.elementFromPoint(e,n),s=a&&(a.closest(".nestable-item-content")||a.closest(".nestable-list-empty"));s&&s.dispatchEvent(r)}return{clientX:e,clientY:n}},onMouseMove:function(t){t&&t.preventDefault();var e=this.getXandYFromEvent(t),n=e.clientX,i=e.clientY;0===this.mouse.last.x&&(this.mouse.last.x=n);var o={transform:"translate("+n+"px, "+i+"px)"},r=document.querySelector(".nestable-"+this.group+" .nestable-drag-layer");if(r){var a,s,u=r.getBoundingClientRect(),c=u.top,d=u.left,p=document.querySelector(".nestable-"+this.group+" .nestable-drag-layer > .nestable-list");if(this.elCopyStyles){if(this.elCopyStyles=l(l({},this.elCopyStyles),o),p)for(var h in o)Object.prototype.hasOwnProperty.call(o,h)&&(p.style[h]=o[h]);var f=this.rtl?this.mouse.last.x-n:n-this.mouse.last.x;f>=0&&this.mouse.shift.x>=0||f<=0&&this.mouse.shift.x<=0?this.mouse.shift.x+=f:this.mouse.shift.x=0,this.mouse.last.x=n,Math.abs(this.mouse.shift.x)>this.threshold&&(this.mouse.shift.x>0?this.tryIncreaseDepth(this.dragItem):this.tryDecreaseDepth(this.dragItem),this.mouse.shift.x=0)}else{var m=(a=this.el,s=a.getBoundingClientRect(),{top:Math.round(s.top),left:Math.round(s.left)});this.elCopyStyles=l({marginTop:"".concat(m.top-i-c,"px"),marginLeft:"".concat(m.left-n-d,"px")},o)}}},moveItem:function(t){var e=t.dragItem,n=t.pathFrom,o=t.pathTo,r=this.getRealNextPath(n,o),a=this.getSplicePath(n,{numToRemove:1,childrenProp:this.childrenProp}),s=this.getSplicePath(r,{numToRemove:0,itemsToInsert:[e],childrenProp:this.childrenProp});if(this.hook("beforeMove",{dragItem:e,pathFrom:n,pathTo:r})){var l=this.value;l=i(l,a),l=i(l,s),this.isDirty=!0,this.pathTo=r,this.$emit("input",l)}},tryIncreaseDepth:function(t){var e=this.getPathById(t[this.keyProp]),n=e[e.length-1],i=e.length+this.getItemDepth(t);if(n>0&&i<=this.maxDepth){var o=this.getItemByPath(e.slice(0,-1).concat(n-1));if(o[this.childrenProp]&&(!o[this.childrenProp].length||!this.isCollapsed(o))){var r=e.slice(0,-1).concat(n-1).concat(o[this.childrenProp].length);this.moveItem({dragItem:t,pathFrom:e,pathTo:r})}}},tryDecreaseDepth:function(t){var e=this.getPathById(t[this.keyProp]),n=e[e.length-1];if(e.length>1&&n+1===this.getItemByPath(e.slice(0,-1))[this.childrenProp].length){var i=e.slice(0,-1);i[i.length-1]+=1,this.moveItem({dragItem:t,pathFrom:e,pathTo:i})}},onMouseEnter:function(t,e,n){t&&(t.preventDefault(),t.stopPropagation());var i=this.dragItem;if(i&&(null===n||i[this.keyProp]!==n[this.keyProp])){var o,r=this.getPathById(i[this.keyProp]);if(e===this.listId||0!==r.length)if(o=null===n?r.length>0?[]:[0]:this.getPathById(n[this.keyProp]),!(this.getRealNextPath(r,o).length+(this.getItemDepth(i)-1)>this.maxDepth)){var a={};if(this.collapsed&&r.length>1){var s=this.getItemByPath(r.slice(0,-1));1===s[this.childrenProp].length&&(a=this.onToggleCollapse(s,!0))}this.moveItem({dragItem:i,pathFrom:r,pathTo:o},a)}}},isCollapsed:function(t){return!!(this.collapsedGroups.indexOf(t[this.keyProp])>-1^this.collapsed)},dragApply:function(){this.$emit("change",this.dragItem,{items:this.value,pathTo:this.pathTo}),this.pathTo=null,this.itemsOld=null,this.dragItem=null,this.isDirty=!1},dragRevert:function(){this.$emit("input",this.itemsOld),this.pathTo=null,this.itemsOld=null,this.dragItem=null,this.isDirty=!1}}},undefined,false,undefined,!1,void 0,void 0,void 0),_=t({render:function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"nestable-handle",attrs:{draggable:""},on:{dragstart:t.dragstart,touchstart:t.dragstart,touchend:t.touchend,touchmove:t.touchmove}},[t._t("default")],2)},staticRenderFns:[]},undefined,{name:"VueNestableHandle",mixins:[f],props:{item:{type:Object,required:!1,default:function(){return{}}}},inject:["group","onDragEnd"],methods:{dragstart:function(t){var e=this.item||this.$parent.item;this.notifyDragStart(this.group,t,e)},touchend:function(t){this.onDragEnd(t)},touchmove:function(t){this.notifyMouseMove(this.group,t)}}},undefined,false,undefined,!1,void 0,void 0,void 0),b={install:function(t,e){t.component("VueNestable",v),t.component("VueNestableHandle",_)}};function k(t,e,n,i,o,r,a,s){var l,u="function"==typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=n,u._compiled=!0),i&&(u.functional=!0),r&&(u._scopeId="data-v-"+r),a?(l=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),o&&o.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(t,e){return l.call(e),c(t,e)}}else{var d=u.beforeCreate;u.beforeCreate=d?[].concat(d,l):[l]}return{exports:t,options:u}}const x={};var $=k({props:{item:Object}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-form-input"},[n("div",{staticClass:"k-form-inner"},[t._t("text"),n("div",{staticClass:"k-form-actions"},[t._t("fetch"),t._t("add")],2)],2)])}),[],!1,P,"e9c938c8",null,null);function P(t){for(let e in x)this[e]=x[e]}var E=function(){return $.exports}();const S={props:{item:Object,fields:Object,navigation:Array,navigationdisabled:Boolean},data:()=>({active:!1}),methods:{item_action(t){"edit"===t.type&&(this.active=!this.active),"remove"===t.type&&this.$emit("action_remove",t),"duplicate"===t.type&&this.$emit("action_add",t.item)},langkey(t){var e;return(null!=(e=this.$panel.language.code)?e:"default")+"_"+t},computed_link_text(t){return"page"===t.type&&""===t[this.langkey("link_text")]?t[this.langkey("page_title")]:t[this.langkey("link_text")]}}},C={};var I=k(S,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-form-input"},[n("div",{staticClass:"k-form-actions"},[t._t("handle")],2),n("div",{staticClass:"k-form-inner"},[t.navigationdisabled?n("k-item",{attrs:{text:t.computed_link_text(t.item)}}):n("k-item",{attrs:{text:t.computed_link_text(t.item),buttons:[{icon:t.active?"collapse":t.item.error?"question":"custom"==t.item.type?"pen":"edit",click:function(e){return t.item_action({type:"edit"})}}],options:[{icon:"copy",text:t.$t("editor.menu.duplicate"),click:function(e){return t.item_action({type:"duplicate",item:t.item})}},{icon:"trash",text:t.$t("editor.menu.remove"),click:function(e){return t.item_action({type:"remove",needle:t.item.uuid,haystack:t.navigation})}}]}}),t.active?n("div",{ref:"config",staticClass:"k-form-config"},[n("div",{ref:"config",staticClass:"k-form-group"},[t._t("dropdown_fields")],2),n("div",{staticClass:"k-form-footer"},[n("span"),n("k-button",{attrs:{icon:"hidden"},on:{click:function(e){return t.item_action({type:"edit"})}}},[t._v(" "+t._s(t.$t("editor.menu.close"))+" ")])],1)]):t._e()],1)])}),[],!1,O,"34431234",null,null);function O(t){for(let e in C)this[e]=C[e]}var w=function(){return I.exports}();const M={};var D=k({props:{modal:Object},methods:{modal_close(){this.$emit("modal_close")},modal_submit(){this.$emit("modal_submit")}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-dialog",{staticClass:"k-pages-dialog",attrs:{size:"medium",visible:t.modal},on:{cancel:t.modal_close,submit:t.modal_submit}},[t._t("modal_header"),t._t("modal_body")],2)}),[],!1,j,"571b2066",null,null);function j(t){for(let e in M)this[e]=M[e]}const q={props:{help:String,value:Array,label:String,levels:Number,edit_title:Boolean,edit_popup:Boolean,edit_target:Boolean,edit_class:Boolean,edit_anchor:Boolean,disabled:Boolean,required:Boolean,endpoints:Object},components:{ListModal:E,ListDefault:w,ModalDefault:function(){return D.exports}()},data(){return{navigation:this.value||[],modal:{type:"",status:!1},query:{content:[],breadcrumbs:[]},item:{url:"",uuid_uri:"",text:"",target:""}}},watch:{navigation:{handler(){this.$emit("input",this.navigation)},deep:!0},panel_content_has_diff(){this.panel_content_has_diff||(this.navigation=this.value)}},methods:{modal_close(){this.modal={type:"",status:!1},this.$emit("close")},modal_open(t){this.modal={type:t,status:!0},panel.dialog.open(this)},modal_submit(){"custom"===this.modal.type&&(this.item.type="custom",this.action_add(this.item),this.item={url:"",uuid_uri:"",text:"",target:""}),this.modal={type:"",status:!1},this.$emit("close")},action_fetch(t){var e;let n=null!=(e=this.$panel.language.code)?e:"default";this.$api.get(this.endpoints.field+"/listings/"+n+"/"+t).then((t=>{this.query=t})).catch((t=>{this.query={content:[],breadcrumbs:[]},console.log(t)}))},action_remove(t){return this.navigation=t.haystack.filter((e=>e.uuid!==t.needle)).map((e=>(e.children&&e.children.length&&(e.children=this.action_remove({haystack:e.children,needle:t.needle})),e)))},action_add(t){var e;if("page"==t.type){let e={type:t.type,id:t.id,uuid_uri:t.uuid_uri,target:"",uuid:Math.random().toString(36).substring(2,15),children:[]};e[this.langkey("link_text")]=t[this.langkey("link_text")],e[this.langkey("page_url")]=t[this.langkey("page_url")],e[this.langkey("page_title")]=t[this.langkey("page_title")],this.navigation.push(e)}else if("custom"==t.type){let n={type:t.type,url:t.url,target:t.popup?"_blank":t.target,uuid:Math.random().toString(36).substring(2,15),children:[]};n[this.langkey("link_text")]=null!=(e=t[this.langkey("link_text")])?e:"",n[this.langkey("link_title")]="",this.navigation.push(n)}else console.warn("Invalid data.type value")},langkey(t){var e;return(null!=(e=this.$panel.language.code)?e:"default")+"_"+t},title_is_editable(t){return"string"==typeof t[this.langkey("link_title")]&&""!==t[this.langkey("link_title")]||this.edit_title},anchor_is_editable(t){return"string"==typeof t[this.langkey("link_anchor")]&&""!==t[this.langkey("link_anchor")]||this.edit_anchor},popup_is_editable(t){return("string"!=typeof t.target||""===t.target||"_blank"===t.target)&&(!this.edit_target&&this.edit_popup)},target_is_editable(t){return"string"==typeof t.target&&""!==t.target&&"_blank"!==t.target||this.edit_target},class_is_editable(t){return"string"==typeof t.class&&""!==t.class||this.edit_class}},computed:{computed_navigation(){return this.navigation},computed_levels(){return this.levels&&parseInt(this.levels)&&parseInt(this.levels)<1?parseInt(this.levels):10},computed_breadcrumbs(){return this.query.breadcrumbs.length>=2?this.query.breadcrumbs[this.query.breadcrumbs.length-2].id:"site"},panel_content_has_diff(){return this.$panel.content.hasDiff()}},mounted(){this.action_fetch("site")}},T={};var A=k(q,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",{staticClass:"k-form-field navigation-field",attrs:{help:t.help,label:t.label,levels:t.levels,disabled:t.disabled,required:t.required},scopedSlots:t._u([{key:"options",fn:function(){return[n("k-dropdown",[t.disabled?t._e():n("k-button",{attrs:{icon:"add"},on:{click:function(e){return t.$refs.menu.toggle()}}},[t._v(" "+t._s(t.$t("menu.link.add"))+" ")]),n("k-dropdown-content",{ref:"menu"},[n("k-dropdown-item",{on:{click:function(e){return t.modal_open("default")}}},[n("span",{staticClass:"k-menu-title"},[t._v(" "+t._s(t.$t("menu.link.title"))+" ")]),n("p",{staticClass:"k-menu-subtitle"},[t._v(" "+t._s(t.$t("menu.link.text"))+" ")])]),n("k-dropdown-item",{on:{click:function(e){return t.modal_open("custom")}}},[n("span",{staticClass:"k-menu-title"},[t._v(" "+t._s(t.$t("menu.custom.title"))+" ")]),n("p",{staticClass:"k-menu-subtitle"},[t._v(" "+t._s(t.$t("menu.custom.text"))+" ")])])],1)],1)]},proxy:!0},{key:"help",fn:function(){return[n("k-grid",[n("k-column",{attrs:{width:"1/2"}},[t.help?n("k-help",{staticClass:"k-field-help",domProps:{innerHTML:t._s(t.help)}}):t._e()],1),n("k-column",{attrs:{width:"1/2"}},[t.computed_levels<=5?n("k-help",{staticClass:"k-field-help k-field-depth"},[t._v(" "+t._s(t.$t("help.depth.text"))+" "),n("strong",[t._v(t._s(t.computed_levels))])]):t._e()],1)],1)]},proxy:!0}])},[t.navigation.length?n("vue-nestable",{attrs:{keyProp:"uuid",childrenProp:"children",maxDepth:t.computed_levels},scopedSlots:t._u([{key:"default",fn:function(e){var i=e.item;return e.index,"save"!=i.type?[n("listDefault",{attrs:{item:i,navigation:t.navigation,navigationdisabled:t.disabled},on:{action_add:t.action_add,action_remove:t.action_remove},scopedSlots:t._u([{key:"handle",fn:function(){return[n("VueNestableHandle",{attrs:{item:i}},[n("k-button",{staticClass:"input-handle",attrs:{icon:"sort",tooltip:t.$t("editor.menu.sort")}})],1)]},proxy:!0},{key:"dropdown_fields",fn:function(){return["page"==i.type?n("k-grid",[n("k-column",{attrs:{width:"1"}},[n("k-info-field",{attrs:{text:i[t.langkey("page_title")],icon:"page"}}),n("k-info-field",{attrs:{text:i[t.langkey("page_url")],icon:"url"}})],1),n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.text")},model:{value:i[t.langkey("link_text")],callback:function(e){t.$set(i,t.langkey("link_text"),e)},expression:"item[langkey('link_text')]"}})],1),t.title_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.title")},model:{value:i[t.langkey("link_title")],callback:function(e){t.$set(i,t.langkey("link_title"),e)},expression:"item[langkey('link_title')]"}})],1):t._e(),t.anchor_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.anchor")},model:{value:i[t.langkey("link_anchor")],callback:function(e){t.$set(i,t.langkey("link_anchor"),e)},expression:"item[langkey('link_anchor')]"}})],1):t._e(),t.class_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.class")},model:{value:i.class,callback:function(e){t.$set(i,"class",e)},expression:"item.class"}})],1):t._e(),t.target_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.target")+" (_blank, _self, _parent, _top, ...)"},model:{value:i.target,callback:function(e){t.$set(i,"target",e)},expression:"item.target"}})],1):t._e(),t.popup_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-toggle-field",{attrs:{value:"_blank"==i.target,label:t.$t("editor.label.popup")},on:{input:function(t){i.target=t?"_blank":""}}})],1):t._e()],1):"custom"==i.type?n("k-grid",[n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.text")},model:{value:i[t.langkey("link_text")],callback:function(e){t.$set(i,t.langkey("link_text"),e)},expression:"item[langkey('link_text')]"}})],1),t.title_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.title")},model:{value:i[t.langkey("link_title")],callback:function(e){t.$set(i,t.langkey("link_title"),e)},expression:"item[langkey('link_title')]"}})],1):t._e(),n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.url")},model:{value:i.url,callback:function(e){t.$set(i,"url",e)},expression:"item.url"}})],1),t.class_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.class")},model:{value:i.class,callback:function(e){t.$set(i,"class",e)},expression:"item.class"}})],1):t._e(),t.target_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-text-field",{attrs:{label:t.$t("editor.label.target")+" (_blank, _self, _parent, _top, ...)"},model:{value:i.target,callback:function(e){t.$set(i,"target",e)},expression:"item.target"}})],1):t._e(),t.popup_is_editable(i)?n("k-column",{attrs:{width:"1/2"}},[n("k-toggle-field",{attrs:{value:"_blank"==i.target,label:t.$t("editor.label.popup")},on:{input:function(t){i.target=t?"_blank":""}}})],1):t._e()],1):t._e()]},proxy:!0}],null,!0)})]:void 0}}],null,!0),model:{value:t.navigation,callback:function(e){t.navigation=e},expression:"navigation"}}):n("k-empty",{attrs:{icon:"page"}},[t._v(" "+t._s(t.$t("help.empty.text"))+" ")]),t.modal.status?n("modalDefault",{attrs:{modal:t.modal.status},on:{modal_close:t.modal_close,modal_submit:t.modal_submit},scopedSlots:t._u([{key:"modal_header",fn:function(){return[n("header",{staticClass:"k-pages-dialog-navbar"},["default"===t.modal.type?[t.query.breadcrumbs.length>0?n("k-button",{attrs:{icon:"angle-left"},on:{click:function(e){return t.action_fetch(t.computed_breadcrumbs)}}},[t._v(" "+t._s(t.$t("modal.link.breadcrumb"))+" ")]):t._e(),n("k-headline",[t._v(" "+t._s(t.$t("modal.link.title"))+" ")])]:t._e(),"custom"===t.modal.type?[n("k-headline",[t._v(" "+t._s(t.$t("modal.custom.title"))+" ")])]:t._e()],2)]},proxy:!0},{key:"modal_body",fn:function(){return["default"===t.modal.type?t._l(t.query.content,(function(e,i){return n("listModal",{key:e.uuid,attrs:{item:e},scopedSlots:t._u([{key:"text",fn:function(){return[n("span",{staticClass:"k-menu-text"},[t._v(t._s(e[t.langkey("page_title")]))])]},proxy:!0},{key:"fetch",fn:function(){return[e.count>0?n("k-button",{attrs:{icon:"angle-right"},on:{click:function(n){return t.action_fetch(e.id)}}}):t._e()]},proxy:!0},{key:"add",fn:function(){return[n("k-button",{attrs:{icon:"add"},on:{click:function(n){return t.action_add(e)}}})]},proxy:!0}],null,!0)})})):t._e(),"custom"===t.modal.type?[n("div",{staticClass:"k-fieldset"},[n("k-grid",[n("k-column",[n("k-text-field",{attrs:{label:t.$t("editor.label.text")},model:{value:t.item[t.langkey("link_text")],callback:function(e){t.$set(t.item,t.langkey("link_text"),e)},expression:"item[langkey('link_text')]"}})],1),n("k-column",[n("k-text-field",{attrs:{label:t.$t("editor.label.url")},model:{value:t.item.url,callback:function(e){t.$set(t.item,"url",e)},expression:"item.url"}})],1),t.popup_is_editable(t.item)?n("k-column",[n("k-toggle-field",{attrs:{value:"_blank"==t.item.target,label:t.$t("editor.label.popup")},on:{input:function(e){t.item.target=e?"_blank":""}}})],1):t._e(),t.target_is_editable(t.item)?n("k-column",[n("k-text-field",{attrs:{label:t.$t("editor.label.target")+" (_blank, _self, _parent, _top, ...)"},model:{value:t.item.target,callback:function(e){t.$set(t.item,"target",e)},expression:"item.target"}})],1):t._e()],1)],1)]:t._e()]},proxy:!0}],null,!1,697514243)}):t._e()],1)}),[],!1,B,null,null,null);function B(t){for(let e in T)this[e]=T[e]}var N=function(){return A.exports}();panel.plugin("beluga/navigation",{fields:{navigation:N},use:b})}();
diff --git a/site/plugins/kirby-navigation/index.php b/site/plugins/kirby-navigation/index.php
new file mode 100644
index 0000000..13d617e
--- /dev/null
+++ b/site/plugins/kirby-navigation/index.php
@@ -0,0 +1,29 @@
+ [
+ 'navigation' => [
+ 'api' => require_once __DIR__ . '/config/api.php',
+ 'props' => require_once __DIR__ . '/config/props.php',
+ 'save' => require_once __DIR__ . '/config/save.php',
+ ]
+ ],
+ 'translations' => [
+ 'de' => require_once __DIR__ . '/languages/de.php',
+ 'en' => require_once __DIR__ . '/languages/en.php',
+ 'fr' => require_once __DIR__ . '/languages/fr.php',
+ 'tr' => require_once __DIR__ . '/languages/tr.php',
+ ],
+ 'snippets' => [
+ 'navigation' => __DIR__ . '/snippets/navigation.php',
+ 'navigation_item' => __DIR__ . '/snippets/navigation_item.php',
+ ],
+ 'fieldMethods' => require_once __DIR__ . '/config/methods.php',
+ 'options' => [
+ 'edit_title' => TRUE,
+ 'edit_popup' => TRUE,
+ 'edit_target' => FALSE,
+ 'edit_class' => FALSE,
+ 'edit_anchor' => FALSE,
+ ],
+]);
diff --git a/site/plugins/kirby-navigation/languages/de.php b/site/plugins/kirby-navigation/languages/de.php
new file mode 100644
index 0000000..d8ec325
--- /dev/null
+++ b/site/plugins/kirby-navigation/languages/de.php
@@ -0,0 +1,26 @@
+ 'Hinzufügen',
+ 'menu.link.title' => 'Kirby Link',
+ 'menu.link.text' => 'Fügt dem Menü eine Kirby-Seite hinzu',
+ 'menu.custom.title' => 'Benutzerdefinierten Link',
+ 'menu.custom.text' => 'Fügt dem Menü einen benutzerdefinierten Link hinzu, nützlich für externe URLs usw.',
+ 'editor.label.id' => 'Id',
+ 'editor.label.url' => 'Url',
+ 'editor.label.text' => 'Text',
+ 'editor.label.title' => 'Titel',
+ 'editor.label.anchor' => 'Anker',
+ 'editor.label.class' => 'Klasse',
+ 'editor.label.target' => 'Ziel',
+ 'editor.label.popup' => 'Popup',
+ 'editor.menu.close' => 'Schließen',
+ 'editor.menu.edit' => 'Bearbeiten',
+ 'editor.menu.remove' => 'Löschen',
+ 'editor.menu.duplicate' => 'Duplikat',
+ 'modal.link.title' => 'Seiten hinzufügen',
+ 'modal.link.breadcrumb' => 'Zurück',
+ 'modal.custom.title' => 'Benutzerdefinierten Link hinzufügen',
+ 'help.depth.text' => 'Maximal zulässige Tiefe:',
+ 'help.empty.text' => 'Noch keine Menüpunkte'
+];
diff --git a/site/plugins/kirby-navigation/languages/en.php b/site/plugins/kirby-navigation/languages/en.php
new file mode 100644
index 0000000..eaefe06
--- /dev/null
+++ b/site/plugins/kirby-navigation/languages/en.php
@@ -0,0 +1,26 @@
+ 'Add',
+ 'menu.link.title' => 'Kirby Link',
+ 'menu.link.text' => 'Adds a Kirby page to the menu',
+ 'menu.custom.title' => 'Custom Link',
+ 'menu.custom.text' => 'Adds a custom link to the menu, useful for external urls etc',
+ 'editor.label.id' => 'Id',
+ 'editor.label.url' => 'Url',
+ 'editor.label.text' => 'Text',
+ 'editor.label.title' => 'Title',
+ 'editor.label.anchor' => 'Anchor',
+ 'editor.label.class' => 'Class',
+ 'editor.label.target' => 'Target',
+ 'editor.label.popup' => 'Popup',
+ 'editor.menu.close' => 'Close Item',
+ 'editor.menu.edit' => 'Edit Item',
+ 'editor.menu.remove' => 'Remove Item',
+ 'editor.menu.duplicate' => 'Duplicate Item',
+ 'modal.link.title' => 'Add Pages',
+ 'modal.link.breadcrumb' => 'Back',
+ 'modal.custom.title' => 'Add Custom Link',
+ 'help.depth.text' => 'Maximum allowed depth:',
+ 'help.empty.text' => 'No menu items yet'
+];
diff --git a/site/plugins/kirby-navigation/languages/fr.php b/site/plugins/kirby-navigation/languages/fr.php
new file mode 100644
index 0000000..c805ad5
--- /dev/null
+++ b/site/plugins/kirby-navigation/languages/fr.php
@@ -0,0 +1,26 @@
+ 'Ajouter',
+ 'menu.link.title' => 'Lien Kirby',
+ 'menu.link.text' => 'Ajouter une page Kirby à ce menu',
+ 'menu.custom.title' => 'Lien personnalisé',
+ 'menu.custom.text' => 'Ajouter un lien personnalisé à ce menu, utile pour les urls externes etc',
+ 'editor.label.id' => 'Id',
+ 'editor.label.url' => 'Url',
+ 'editor.label.text' => 'Texte',
+ 'editor.label.title' => 'Titre',
+ 'editor.label.anchor' => 'Ancre',
+ 'editor.label.class' => 'Classe',
+ 'editor.label.target' => 'Cible',
+ 'editor.label.popup' => 'Popup',
+ 'editor.menu.close' => 'Fermer l\'élement',
+ 'editor.menu.edit' => 'Éditer l\'élement',
+ 'editor.menu.remove' => 'Supprimer l\'élement',
+ 'editor.menu.duplicate' => 'Dupliquer l\'élement',
+ 'modal.link.title' => 'Ajouter des pages',
+ 'modal.link.breadcrumb' => 'Retour',
+ 'modal.custom.title' => 'Ajouter un lien personnalisé',
+ 'help.depth.text' => 'Profondeur maximale autorisée :',
+ 'help.empty.text' => 'Aucun élement de menu pour l\'instant'
+];
diff --git a/site/plugins/kirby-navigation/languages/tr.php b/site/plugins/kirby-navigation/languages/tr.php
new file mode 100644
index 0000000..18e0f45
--- /dev/null
+++ b/site/plugins/kirby-navigation/languages/tr.php
@@ -0,0 +1,26 @@
+ 'Ekle',
+ 'menu.link.title' => 'Sayfa Bağlantısı',
+ 'menu.link.text' => 'Menüye bir sayfa ekler',
+ 'menu.custom.title' => 'Özel Bağlantı',
+ 'menu.custom.text' => 'Menüye harici url\'ler vb. için yararlı olan özel bir bağlantı ekler',
+ 'editor.label.id' => 'Id',
+ 'editor.label.url' => 'Url',
+ 'editor.label.text' => 'Metin',
+ 'editor.label.title' => 'Başlık',
+ 'editor.label.anchor' => 'Çapa',
+ 'editor.label.class' => 'Sınıf',
+ 'editor.label.target' => 'Hedef',
+ 'editor.label.popup' => 'Yeni pencere',
+ 'editor.menu.close' => 'Kapat',
+ 'editor.menu.edit' => 'Düzenle',
+ 'editor.menu.remove' => 'Kaldır',
+ 'editor.menu.duplicate' => 'Çiftleme',
+ 'modal.link.title' => 'Sayfa Ekle',
+ 'modal.link.breadcrumb' => 'Geri',
+ 'modal.custom.title' => 'Özel Bağlantı Ekle',
+ 'help.depth.text' => 'İzin verilen maksimum derinlik:',
+ 'help.empty.text' => 'Henüz menü öğesi yok'
+];
diff --git a/site/plugins/kirby-navigation/navigation-demo-1.gif b/site/plugins/kirby-navigation/navigation-demo-1.gif
new file mode 100644
index 0000000..089e87d
Binary files /dev/null and b/site/plugins/kirby-navigation/navigation-demo-1.gif differ
diff --git a/site/plugins/kirby-navigation/package.json b/site/plugins/kirby-navigation/package.json
new file mode 100644
index 0000000..b6f1171
--- /dev/null
+++ b/site/plugins/kirby-navigation/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "kirby-navigation",
+ "type": "module",
+ "scripts": {
+ "dev": "npx -y kirbyup src/index.js --watch",
+ "build": "npx -y kirbyup src/index.js"
+ },
+ "dependencies": {
+ "kirbyup": "^0.14.1",
+ "vue-nestable": "^2.6.0",
+ "vue-runtime-helpers": "^1.1.2"
+ }
+}
diff --git a/site/plugins/kirby-navigation/snippets/navigation.php b/site/plugins/kirby-navigation/snippets/navigation.php
new file mode 100644
index 0000000..327d1ee
--- /dev/null
+++ b/site/plugins/kirby-navigation/snippets/navigation.php
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
diff --git a/site/plugins/kirby-navigation/src/components/Modal/Default.vue b/site/plugins/kirby-navigation/src/components/Modal/Default.vue
new file mode 100644
index 0000000..b503ada
--- /dev/null
+++ b/site/plugins/kirby-navigation/src/components/Modal/Default.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/site/plugins/kirby-navigation/src/index.js b/site/plugins/kirby-navigation/src/index.js
new file mode 100644
index 0000000..298873a
--- /dev/null
+++ b/site/plugins/kirby-navigation/src/index.js
@@ -0,0 +1,9 @@
+import VueNestable from "vue-nestable/dist/index.esm";
+import Field from './Field.vue'
+
+panel.plugin('beluga/navigation', {
+ fields: {
+ navigation: Field
+ },
+ use: VueNestable
+})
diff --git a/site/plugins/kirby-seo/LICENSE.md b/site/plugins/kirby-seo/LICENSE.md
new file mode 100644
index 0000000..ad5f823
--- /dev/null
+++ b/site/plugins/kirby-seo/LICENSE.md
@@ -0,0 +1,132 @@
+# Plugin License Agreement
+
+Source: https://plugins.andkindness.com/license-agreement
+
+While most of our plugins source code are publicly available, they are, unless specified otherwise, not free software. To use any plugin in production, you need to purchase a license.
+
+## Summary
+
+> This is a legally non-binding summary. Please review the full license text carefully before using the plugins.
+
+This is a legal agreement between you (the customer) and Love & Kindness GmbH for using their Kirby CMS plugins. By downloading or using the plugins, or by purchasing a license, you agree to these terms:
+
+### What You Can Do
+
+- **Install and use the plugin on one website or multi-language website per license purchase**
+ If you need a separate Kirby CMS license for your site, you'll most likely also need a separate plugin license
+- **Make copies of the plugin for backup or development purposes**
+- **Modify the source code for your own use**
+
+### What You Cannot Do
+
+- **Use the plugin on additional websites without buying additional licenses**
+- **Redistribute or resell the plugin or your modified versions**
+
+### Support & Updates
+
+- **Free updates for minor/patch releases, paid upgrades for major releases possible**
+- **Email support provided for active license holders**
+
+---
+
+This license is a legal agreement between **You** and **Love & Kindness GmbH, Beimoorstr. 20, 22081 Hamburg, Germany** (therein "**Our**"/"**We**"/"**Us**") for the use of any Kirby CMS plugins and resources (the "**Plugin**") created by Us and sold via Paddle.com. By downloading any Plugin files or resources or purchasing a license to the Plugin, you agree to be bound by the terms and conditions of this license.
+
+## Permitted Use
+
+This agreement grants a license for each purchase to install and use a single instance of the Plugin on a **specific website** limited by **its domain & subdomain**. If You use the cross-domain multi-language feature with the same `content` folder, these domains count as the same Website.
+
+Additional Plugin licenses must be purchased in order to install and use the Plugin on **additional websites**.
+
+The license is **non-exclusive** and **generally non-transferable**.
+
+A license is valid for all minor & patch updates of the Plugin (e.g. 1.0.x to 1.1.x). We reserve the right to charge an **upgrade fee for major updates** (e.g. 1.x.x to 2.x.x). Whether a release is a patch, minor, or major release is at Our sole discretion.
+
+## Development Usage
+
+You are permitted to install and use the Plugin on a personal computer (such as a desktop PC, notebook, or tablet) or a server, free of charge, for as long as necessary during the development stage.
+
+Any website that is used **purely for the purposes of development and client preview** is considered development usage. It must only be accessible by a restricted number of users. A website with **the intention to handle production data** is never considered development usage, no matter if the related website is publicly accessible or not.
+
+## Refund Policy
+
+We offer refunds on the Plugin within **14 days of purchase**. Contact support@andkindness.com for assistance.
+
+## Technical Support
+
+Technical support is available via email for active license owners.
+
+No support is provided for free plugins or plugins that are available both for free and paid usage. We do not provide phone support. No representations or guarantees are made regarding the response time in which support questions are answered, but we will do our very best to respond quickly.
+
+## All Rights Reserved
+
+Love & Kindness GmbH **owns all rights**, title and interest to the Plugin (including all intellectual property rights) and **reserves all rights to the Plugin** that are not expressly granted in this Agreement.
+
+## Restrictions
+
+### Making Copies
+
+You may make **copies of the Plugin** in any machine readable form solely for purposes of **deploying a website to a server, developing a website on a personal computer or server or as a backup**, provided that You reproduce the Plugin in its original form and with all proprietary notices on the copy.
+
+You may not reproduce the Plugin or its source code, in whole or in part, for **any other purpose**.
+
+### Modification of the Source Code
+
+You may **alter, modify or extend the source code** for Your own use. You may also **commission a third party** to perform those modifications for You.
+
+However You may not **alter or circumvent the licensing features**, including (but not limited to) the license validation or **resell, redistribute or transfer** the modified or derivative version.
+
+### Ownership and Intellectual Property
+
+The Plugin is copyrighted by Us. All rights not expressly granted to You are retained by Us, including intellectual property rights.
+
+### Disclaimer of Warranty
+
+THE PLUGIN IS OFFERED ON AN **"AS-IS" BASIS** AND **NO WARRANTY**, EITHER EXPRESSED OR IMPLIED, IS GIVEN. WE EXPRESSLY DISCLAIM ALL WARRANTIES OF ANY KIND, WHETHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. YOU ASSUME ALL RISK ASSOCIATED WITH THE QUALITY, PERFORMANCE, INSTALLATION AND USE OF THE PLUGIN INCLUDING, BUT NOT LIMITED TO, THE RISKS OF PROGRAM ERRORS, DAMAGE TO EQUIPMENT, LOSS OF DATA OR SOFTWARE PROGRAMS, OR UNAVAILABILITY OR INTERRUPTION OF OPERATIONS. **YOU ARE SOLELY RESPONSIBLE** FOR DETERMINING THE APPROPRIATENESS OF USE OF THE PLUGIN AND ASSUME ALL RISKS ASSOCIATED WITH ITS USE. THIS PARAGRAPH ALSO APPLIES TO YOU IF YOU ARE NOT THE LICENSEE (E.G. IF YOU USE THE PLUGIN WHILE SOMEONE ELSE IS THE LICENSEE).
+
+### Limitations of Liability
+
+YOU EXPRESSLY UNDERSTAND AND AGREE THAT **WE SHALL NOT BE LIABLE** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR EXEMPLARY DAMAGES, INCLUDING BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER INTANGIBLE LOSSES (EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES). SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, **SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU**. **IN NO EVENT WILL OUR TOTAL CUMULATIVE DAMAGES EXCEED** THE FEES YOU PAID TO US UNDER THIS AGREEMENT IN THE MOST RECENT TWELVE-MONTH PERIOD. THIS PARAGRAPH ALSO APPLIES TO YOU IF YOU ARE NOT THE LICENSEE (E.G. IF YOU USE THE PLUGIN WHILE SOMEONE ELSE IS THE LICENSEE).
+
+## Termination
+
+The License may be terminated by either party if terms are breached and not remedied within a specified period.
+
+## Governing Law
+
+Any legal disputes that arise from or relate to this Agreement shall be exclusively resolved in the courts located in Hamburg, Germany. Nonetheless, we reserve the right to initiate legal proceedings against you in the jurisdiction where your principal place of business is situated.
+
+Should you be domiciled in Germany, the stipulations of the first paragraph will be relevant only if you are a businessperson, a public law entity, or a special fund under public law.
+
+If your domicile is not in Germany but within another member state of the European Union, the provisions of the first paragraph will apply to you only if you do not qualify as a consumer as defined under Article 17 of Regulation (EU) No. 1215/2012. In such circumstances, you may bring a legal action against us either in the jurisdiction of our place of business or where you habitually reside. Conversely, we are entitled to sue you exclusively in the courts of the member state where you have your domicile.
+
+In the event that your residence is outside Germany and not within any European Union member state, the provisions of the first paragraph are fully applicable without any modification.
+
+## Severability Clause
+
+Should any provision of this Agreement be or become invalid, void or unenforceable, in whole or in part, at present or in the future, this shall not affect the validity of the remaining provisions of this Agreement. The same shall apply if a gap requiring supplementation arises after conclusion of this Agreement. The parties shall replace the invalid, void or unenforceable provision or gap requiring filling by a valid provision which in its legal or economic content takes account of the invalid, void provision and the overall content of the agreement. § 139 BGB (partial invalidity) is expressly waived.
+
+---
+
+Kirby SEO 1.x releases were previously licensed under the MIT License.
+
+MIT License
+
+Copyright (c) 2023-2024 Tobias Möritz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/site/plugins/kirby-seo/README.md b/site/plugins/kirby-seo/README.md
new file mode 100644
index 0000000..ac3574e
--- /dev/null
+++ b/site/plugins/kirby-seo/README.md
@@ -0,0 +1,52 @@
+
+
+
Kirby SEO
+
+ The default choice for SEO on Kirby: Implement technical SEO & Meta best practices with ease and provide an easy-to-use editor experience
+
+
+
+
+
+
\ No newline at end of file
diff --git a/site/plugins/kirby-seo/translations/cs.json b/site/plugins/kirby-seo/translations/cs.json
new file mode 100644
index 0000000..98e6f66
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/cs.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Struktura nadpisů",
+ "sections.headingStructure.errors.incorrectOrder": "Struktura nadpisů má nesprávné pořadí a je tedy chybná.",
+ "sections.headingStructure.errors.missingH1": "Struktura nadpisů neobsahuje H1 a je tedy chybná.",
+ "sections.headingStructure.errors.multipleH1": "Struktura nadpisů obsahuje více než jeden H1 a je tedy chybná.",
+ "sections.preview.title": "Náhled",
+ "sections.preview.titleWithPage": "Náhled (zobrazuje „{title}\")",
+ "sections.preview.viewPage": "Zobrazit stránku",
+ "sections.preview.showFor": "Ukaž mi",
+ "sections.preview.openDebugger": "Otevřít Sharing Debugger",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "Indexace povolena",
+ "fields.robots.indicator.any": "Částečný zákaz indexace",
+ "fields.robots.indicator.noindex": "Indexace zakázána",
+ "fields.robots.label": "Vyhledávače a crawlery",
+ "fields.robots.index.label": "Indexace",
+ "fields.robots.index.help": "Zda mohou vyhledávače tuto stránku indexovat.",
+ "fields.robots.follow.label": "Sledovat odkazy",
+ "fields.robots.follow.help": "Zda mohou vyhledávače sledovat odkazy na této stránce.",
+ "fields.robots.archive.label": "Archivace",
+ "fields.robots.archive.help": "Zda mohou vyhledávače tuto stránku archivovat.",
+ "fields.robots.imageindex.label": "Indexace obrázků",
+ "fields.robots.imageindex.help": "Zda mohou vyhledávače indexovat obrázky na této stránce.",
+ "fields.robots.snippet.label": "Tvorba snippetů",
+ "fields.robots.snippet.help": "Zda mohou vyhledávače vytvářet snippety z této stránky.",
+ "fields.robots.ai.label": "Trénování AI",
+ "fields.robots.ai.help": "Zda mohou poskytovatelé AI tuto stránku používat k trénování.",
+ "fields.robots.imageai.label": "Trénování AI na obrázcích",
+ "fields.robots.imageai.help": "Zda mohou poskytovatelé AI používat obrázky na této stránce k trénování.",
+ "tabs.seo": "Metadata a SEO",
+ "site.meta.headline": "Globální nastavení SEO",
+ "site.meta.headline.help": "Toto nastavení bude použito pro všechny stránky, které nemají vlastní metadata.\nMůžete jej přepsat pro každou stránku.",
+ "fields.metaTitleTemplate.label": "Šablona titulku stránky",
+ "fields.metaTitleTemplate.help": "Šablona pro použití u všech názvů stránek.",
+ "fields.metaDescription.label": "Popisek stránky (description)",
+ "fields.metaDescription.help": "Doporučená délka je maximálně 150 znaků. Používá se, pokud není zadán popisek stránky.",
+ "site.og.headline": "Globální nastavení Open Graph",
+ "site.og.headline.help": "Nastavte, jak se vaše webové stránky zobrazují při sdílení na sociálních sítích, jako je Facebook nebo Twitter.",
+ "fields.ogTitleTemplate.label": "Šablona Open Graph titulku",
+ "fields.ogDescription.label": "Open Graph popisek (description)",
+ "fields.ogSiteName.label": "Open Graph název webu (site name)",
+ "fields.ogImage.label": "Open Graph obrázek",
+ "fields.ogImage.help": "Doporučená velikost 1200x630 pixelů.",
+ "fields.ogImage.empty": "Nebyl vybrán žádný obrázek",
+ "fields.cropOgImage.label": "Oříznout OG obrázek na doporučenou velikost?",
+ "fields.cropOgImage.help": "Doporučená velikost je 1200x630px. Při aktivaci budou obrázky automaticky oříznuty na tuto velikost pro optimální zobrazení na sociálních sítích.",
+ "fields.socialMediaAccounts.label": "Účty na sociálních sítích",
+ "fields.socialMediaAccounts.help": "Adresy URL nebo @uzivatelska-jmena vašich účtů na sociálních sítích. Slouží k umístění odkazů na vaše účty do metadat.",
+ "page.meta.headline": "Nastavení SEO",
+ "page.og.headline": "Nastavení Open Graph",
+ "fields.titleOverwrite.label": "Titulek stránky (přepsat)",
+ "fields.inheritSettings.label": "Zdědit nastavení",
+ "fields.inheritSettings.help": "Vyberte, která nastavení by měla být zděděna podstránkami.\nTo může být užitečné například v případě, že všechny příspěvky blogu by měly mít vlastní šablonu titulku, která se liší od výchozího nastavení webu. Všechna nastavení lze stále na hlavní stránce přepsat.",
+ "fields.useTitleTemplate.label": "Použít šablonu titulku?",
+ "fields.useTitleTemplate.no": "Ne - pouze název stránky",
+ "fields.useTitleTemplate.yes": "Ano - použít šablonu",
+ "fields.useTitleTemplate.help": "Určuje, zda se má použít šablona titulku. Nebude zděděno.",
+ "writerNodes.template.title": "Titulek stránky",
+ "writerNodes.template.siteTitle": "Titulek webu",
+ "common.default": "Výchozí:",
+ "common.yes": "Ano",
+ "common.no": "Ne",
+ "sitemap.title": "Sitemap",
+ "sitemap.index": "Sitemap Index",
+ "sitemap.description": "Toto je mapa stránek vašeho webu, která informuje vyhledávače o stránkách, které lze indexovat.",
+ "sitemap.by": "od",
+ "sitemap.changefreq": "Frekvence změn",
+ "sitemap.lastUpdated": "Poslední aktualizace",
+ "sitemap.priority": "Priorita",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "Žádné záznamy",
+ "utmShare.button": "Sdílet",
+ "utmShare.parameters": "Parametry",
+ "utmShare.source.label": "Zdroj",
+ "utmShare.source.placeholder": "např. google, newsletter",
+ "utmShare.medium.label": "Médium",
+ "utmShare.medium.placeholder": "např. cpc, email, social",
+ "utmShare.campaign.label": "Kampaň",
+ "utmShare.campaign.placeholder": "např. jarni_vyprodej",
+ "utmShare.content.label": "Obsah",
+ "utmShare.content.placeholder": "např. logo_odkaz",
+ "utmShare.term.label": "Výraz",
+ "utmShare.term.placeholder": "např. běžecké boty",
+ "utmShare.ref.label": "Ref",
+ "utmShare.ref.placeholder": "např. getkirby.com",
+ "ai.action.generate": "Vygenerovat pomocí AI",
+ "ai.action.regenerate": "Vygenerovat znovu",
+ "ai.action.edit": "Upravit…",
+ "ai.action.customize": "Přizpůsobit generování AI…",
+ "ai.action.stop": "Zastavit",
+ "ai.error.request": "Požadavek na AI selhal. Zkus to prosím znovu.",
+ "ai.error.disabled": "Funkce AI jsou vypnuté.",
+ "ai.error.permission": "Nemáš oprávnění používat funkce AI.",
+ "ai.dialog.instructions.label": "Pokyny",
+ "ai.dialog.instructions.placeholder": "Jaké změny chceš v textu provést?",
+ "ai.dialog.edit.submit": "Použít změny",
+ "ai.dialog.custom.label": "Vlastní pokyny",
+ "ai.dialog.custom.placeholder": "Jaký typ obsahu chceš vygenerovat?",
+ "ai.dialog.custom.submit": "Vygenerovat",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Nastav přihlašovací údaje Google Cloud pro připojení Search Console. Instrukce k nastavení najdeš v dokumentaci.",
+ "sections.searchConsole.notConnected": "Připoj svůj účet Google, abys viděla vyhledávací data pro tuto stránku.",
+ "sections.searchConsole.selectProperty": "Vyber, kterou vlastnost Search Console chceš použít.",
+ "sections.searchConsole.selectPropertyButton": "Vybrat vlastnost",
+ "sections.searchConsole.selectPropertyLabel": "Vlastnost",
+ "sections.searchConsole.scDomain": "doména",
+ "sections.searchConsole.docs": "Dokumentace",
+ "sections.searchConsole.connect": "Připojit",
+ "sections.searchConsole.reconnect": "Připojit znovu",
+ "sections.searchConsole.noData": "Pro tuto stránku nejsou k dispozici žádná vyhledávací data.",
+ "sections.searchConsole.showMore": "Zobrazit vše",
+ "sections.searchConsole.sortBy": "Seřadit podle",
+ "sections.searchConsole.query": "Dotaz",
+ "sections.searchConsole.clicks": "Kliknutí",
+ "sections.searchConsole.impressions": "Zobrazení",
+ "sections.searchConsole.ctr": "CTR",
+ "sections.searchConsole.position": "Pozice",
+ "sections.searchConsole.openInGsc": "Otevřít v Search Console",
+ "altText.decorative.on": "Není potřeba popisek (pouze vizuální prvek)",
+ "altText.decorative.off": "Vyžaduje popisek",
+ "job.generateAltText": "Vygenerovat alternativní text",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/kirby-seo/translations/de.json b/site/plugins/kirby-seo/translations/de.json
new file mode 100644
index 0000000..a85e2b0
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/de.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Überschriftenstruktur",
+ "sections.headingStructure.errors.incorrectOrder": "Deine Überschriftenstruktur hat eine falsche Abfolge und ist ungültig.",
+ "sections.headingStructure.errors.missingH1": "Deine Überschriftenstruktur enthält keine H1 und ist ungültig.",
+ "sections.headingStructure.errors.multipleH1": "Deine Überschriftenstruktur enthält mehr als eine H1 und ist ungültig.",
+ "sections.preview.title": "Vorschau",
+ "sections.preview.titleWithPage": "Vorschau (zeigt \"{title}\")",
+ "sections.preview.viewPage": "Seite ansehen",
+ "sections.preview.showFor": "Zeige mir",
+ "sections.preview.openDebugger": "Sharing-Debugger öffnen",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "Indexierung erlaubt",
+ "fields.robots.indicator.any": "Indexierung teilw. verboten",
+ "fields.robots.indicator.noindex": "Indexierung verboten",
+ "fields.robots.label": "Richtlinien für Suchmaschinen & Crawler",
+ "fields.robots.index.label": "Indexierung",
+ "fields.robots.index.help": "Ob Suchmaschinen die Seite indexieren dürfen.",
+ "fields.robots.follow.label": "Links folgen",
+ "fields.robots.follow.help": "Ob Suchmaschinen Links auf dieser Seite folgen dürfen.",
+ "fields.robots.archive.label": "Archivierung",
+ "fields.robots.archive.help": "Ob Suchmaschinen zwischengespeicherte Versionen der Seite ausliefern dürfen.",
+ "fields.robots.imageindex.label": "Bilder-Indexierung",
+ "fields.robots.imageindex.help": "Ob Bilder dieser Seite in der Bildersuche angezeigt werden dürfen.",
+ "fields.robots.snippet.label": "Snippets",
+ "fields.robots.snippet.help": "Ob Suchmaschinen Textausschnitte aus der Seite anzeigen dürfen.",
+ "fields.robots.ai.label": "KI-Training",
+ "fields.robots.ai.help": "Ob KI-Anbieter diese Seite für das Training verwenden dürfen.",
+ "fields.robots.imageai.label": "KI-Bildtraining",
+ "fields.robots.imageai.help": "Ob KI-Anbieter Bilder auf dieser Seite für das Training verwenden dürfen.",
+ "tabs.seo": "Metadaten & SEO",
+ "site.meta.headline": "Globale SEO-Einstellungen",
+ "site.meta.headline.help": "Diese Einstellungen werden für alle Seiten verwendet, die keine eigenen Metadaten haben.\nDu kannst sie für jede Seite überschreiben.",
+ "fields.metaTitleTemplate.label": "Titel-Template",
+ "fields.metaTitleTemplate.help": "Eine Vorlage für alle Seitentitel.",
+ "fields.metaDescription.label": "Seitenbeschreibung",
+ "fields.metaDescription.help": "Empfohlene Länge von max. 150 Zeichen. Wird verwendet, falls keine Seitenbeschreibung angegeben ist.",
+ "site.og.headline": "Globale Open Graph-Einstellungen",
+ "site.og.headline.help": "Stelle ein, wie deine Website erscheint, wenn sie auf sozialen Netzwerken wie Facebook oder Twitter geteilt wird.",
+ "fields.ogTitleTemplate.label": "Open Graph-Titel-Template",
+ "fields.ogDescription.label": "Open Graph-Beschreibung",
+ "fields.ogSiteName.label": "Open Graph-Seitenname",
+ "fields.ogImage.label": "Open Graph-Bild",
+ "fields.ogImage.help": "Empfohlene Größe von 1200x630 Pixeln.",
+ "fields.ogImage.empty": "Kein Open Graph-Bild ausgewählt",
+ "fields.cropOgImage.label": "Auf empfohlene Größe zuschneiden?",
+ "fields.cropOgImage.help": "Empfohlene Größe ist 1200x630px. Wenn aktiviert, werden Bilder automatisch auf diese Größe zugeschnitten für optimale Anzeige in sozialen Medien.",
+ "fields.socialMediaAccounts.label": "Social Media-Accounts",
+ "fields.socialMediaAccounts.help": "URLs bzw. @-Handles zu deinen Social Media-Accounts. Werden verwendet, um Links zu deinen Accounts in den Metadaten zu setzen.",
+ "page.meta.headline": "SEO-Einstellungen",
+ "page.og.headline": "Open Graph-Einstellungen",
+ "fields.titleOverwrite.label": "Titel (Überschreiben)",
+ "fields.inheritSettings.label": "Einstellungen vererben",
+ "fields.inheritSettings.help": "Wähle aus, welche Einstellungen an Unterseiten vererbt werden sollen.\nDies kann z.B. hilfreich sein, wenn alle Beiträge eines Blogs ein eigenes Titel-Template haben sollen, welches vom Seiten-Standard abweicht. Alle Einstellungen lassen sich weiterhin in der Hauptseite überschreiben.",
+ "fields.useTitleTemplate.label": "Titel-Template verwenden?",
+ "fields.useTitleTemplate.no": "Nein - reiner Titel",
+ "fields.useTitleTemplate.yes": "Ja - mit Template",
+ "fields.useTitleTemplate.help": "Gibt an, ob das Titel-Template verwendet werden soll. Wird nicht vererbt.",
+ "writerNodes.template.title": "Seitentitel",
+ "writerNodes.template.siteTitle": "Website-Titel",
+ "common.default": "Standard:",
+ "common.yes": "Ja",
+ "common.no": "Nein",
+ "sitemap.title": "Sitemap",
+ "sitemap.index": "Sitemap-Index",
+ "sitemap.description": "Dies ist die Sitemap für deine Website, die Suchmaschinen über die Seiten auf deiner Website informiert, die indexiert werden können.",
+ "sitemap.by": "von",
+ "sitemap.changefreq": "Änderungsfrequenz",
+ "sitemap.lastUpdated": "Letzte Änderung",
+ "sitemap.priority": "Priorität",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "Keine Einträge",
+ "utmShare.button": "Teilen",
+ "utmShare.parameters": "Parameter",
+ "utmShare.source.label": "Quelle",
+ "utmShare.source.placeholder": "z. B. google, newsletter",
+ "utmShare.medium.label": "Medium",
+ "utmShare.medium.placeholder": "z. B. cpc, email, social",
+ "utmShare.campaign.label": "Kampagne",
+ "utmShare.campaign.placeholder": "z. B. fruehjahrsverkauf",
+ "utmShare.content.label": "Inhalt",
+ "utmShare.content.placeholder": "z. B. logo_link",
+ "utmShare.term.label": "Begriff",
+ "utmShare.term.placeholder": "z. B. laufschuhe",
+ "utmShare.ref.label": "Ref",
+ "utmShare.ref.placeholder": "z. B. getkirby.com",
+ "ai.action.generate": "Mit KI generieren",
+ "ai.action.regenerate": "Neu generieren",
+ "ai.action.edit": "Bearbeiten...",
+ "ai.action.customize": "KI-Generierung anpassen...",
+ "ai.action.stop": "Stoppen",
+ "ai.error.request": "Die KI-Anfrage ist fehlgeschlagen. Bitte versuche es erneut.",
+ "ai.error.disabled": "KI-Funktionen sind deaktiviert.",
+ "ai.error.permission": "Du hast keine Berechtigung, KI-Funktionen zu nutzen.",
+ "ai.dialog.instructions.label": "Anweisungen",
+ "ai.dialog.instructions.placeholder": "Welche Änderungen möchtest du am Text vornehmen?",
+ "ai.dialog.edit.submit": "Änderungen übernehmen",
+ "ai.dialog.custom.label": "Benutzerdefinierte Anweisungen",
+ "ai.dialog.custom.placeholder": "Welche Art von Inhalt möchtest du generieren?",
+ "ai.dialog.custom.submit": "Generieren",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Richte Google Cloud-Zugangsdaten ein, um die Search Console zu verbinden. Siehe die Dokumentation für Einrichtungsanweisungen.",
+ "sections.searchConsole.notConnected": "Verbinde dein Google-Konto, um Suchdaten für diese Seite zu sehen.",
+ "sections.searchConsole.selectProperty": "Wähle aus, welche Search Console-Property verwendet werden soll.",
+ "sections.searchConsole.selectPropertyButton": "Property auswählen",
+ "sections.searchConsole.selectPropertyLabel": "Property",
+ "sections.searchConsole.scDomain": "Domain",
+ "sections.searchConsole.docs": "Dokumentation",
+ "sections.searchConsole.connect": "Verbinden",
+ "sections.searchConsole.reconnect": "Erneut verbinden",
+ "sections.searchConsole.noData": "Keine Suchdaten für diese Seite verfügbar.",
+ "sections.searchConsole.showMore": "Alle anzeigen",
+ "sections.searchConsole.sortBy": "Sortieren nach",
+ "sections.searchConsole.query": "Suchanfrage",
+ "sections.searchConsole.clicks": "Klicks",
+ "sections.searchConsole.impressions": "Impressionen",
+ "sections.searchConsole.ctr": "CTR",
+ "sections.searchConsole.position": "Position",
+ "sections.searchConsole.openInGsc": "In Search Console öffnen",
+ "altText.decorative.on": "Keine Beschreibung nötig (rein dekorativ)",
+ "altText.decorative.off": "Beschreibung erforderlich",
+ "job.generateAltText": "Alt-Text generieren",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/kirby-seo/translations/en.json b/site/plugins/kirby-seo/translations/en.json
new file mode 100644
index 0000000..367461d
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/en.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Heading Structure",
+ "sections.headingStructure.errors.incorrectOrder": "Your heading structure has an incorrect order and is invalid.",
+ "sections.headingStructure.errors.missingH1": "Your heading structure does not contain an H1 and is invalid.",
+ "sections.headingStructure.errors.multipleH1": "Your heading structure contains more than one H1 and is invalid.",
+ "sections.preview.title": "Preview",
+ "sections.preview.titleWithPage": "Preview (shows \"{title}\")",
+ "sections.preview.viewPage": "View page",
+ "sections.preview.showFor": "Show me",
+ "sections.preview.openDebugger": "Open Sharing Debugger",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "Indexing allowed",
+ "fields.robots.indicator.any": "Indexing partly forbidden",
+ "fields.robots.indicator.noindex": "Indexing forbidden",
+ "fields.robots.label": "Robots Directives",
+ "fields.robots.index.label": "Indexing",
+ "fields.robots.index.help": "Whether search engines may index this page.",
+ "fields.robots.follow.label": "Follow Links",
+ "fields.robots.follow.help": "Whether search engines may follow links on this page.",
+ "fields.robots.archive.label": "Archive",
+ "fields.robots.archive.help": "Whether search engines may archive this page.",
+ "fields.robots.imageindex.label": "Image Indexing",
+ "fields.robots.imageindex.help": "Whether search engines may index images on this page.",
+ "fields.robots.snippet.label": "Snippets",
+ "fields.robots.snippet.help": "Whether search engines may show text snippets of this page.",
+ "fields.robots.ai.label": "AI Training",
+ "fields.robots.ai.help": "Whether AI providers may use this page for training.",
+ "fields.robots.imageai.label": "AI Image Training",
+ "fields.robots.imageai.help": "Whether AI providers may use images on this page for training.",
+ "tabs.seo": "Metadata & SEO",
+ "site.meta.headline": "Global SEO Settings",
+ "site.meta.headline.help": "These settings are used for all pages that do not have their own metadata.\nYou can override them for each page.",
+ "fields.metaTitleTemplate.label": "Title Template",
+ "fields.metaTitleTemplate.help": "A template to use for all page titles.",
+ "fields.metaDescription.label": "Page Description",
+ "fields.metaDescription.help": "Recommended length of 150 characters max. Used if no page description is specified.",
+ "site.og.headline": "Global Open Graph Settings",
+ "site.og.headline.help": "Set how your website appears when shared on social networks like Facebook or Twitter.",
+ "fields.ogTitleTemplate.label": "Open Graph Title Template",
+ "fields.ogDescription.label": "Open Graph Description",
+ "fields.ogSiteName.label": "Open Graph Site Name",
+ "fields.ogImage.label": "Open Graph Image",
+ "fields.ogImage.help": "Recommended size of 1200x630 pixels.",
+ "fields.ogImage.empty": "No Open Graph Image selected",
+ "fields.cropOgImage.label": "Crop OG Image to recommended size?",
+ "fields.cropOgImage.help": "Recommended size is 1200x630px. When enabled, images will be automatically cropped to this size for optimal display on social media.",
+ "fields.socialMediaAccounts.label": "Social Media Accounts",
+ "fields.socialMediaAccounts.help": "URLs or @handles to your social media accounts. Used to put links to your accounts in the metadata.",
+ "page.meta.headline": "SEO Settings",
+ "page.og.headline": "Open Graph Settings",
+ "fields.titleOverwrite.label": "Title (overwrite)",
+ "fields.inheritSettings.label": "Inherit Settings",
+ "fields.inheritSettings.help": "Select which settings should be inherited by subpages.\nThis can be helpful, for example, if all posts of a blog should have their own title template, which differs from the page default. All settings can still be overridden in the main page.",
+ "fields.useTitleTemplate.label": "Use title template?",
+ "fields.useTitleTemplate.no": "No - only title",
+ "fields.useTitleTemplate.yes": "Yes - with template",
+ "fields.useTitleTemplate.help": "Specifies whether the title template should be used. Will not be inherited.",
+ "writerNodes.template.title": "Page Title",
+ "writerNodes.template.siteTitle": "Site Title",
+ "common.default": "Default:",
+ "common.yes": "Yes",
+ "common.no": "No",
+ "sitemap.title": "Sitemap",
+ "sitemap.index": "Sitemap Index",
+ "sitemap.description": "This is the sitemap for your website that informs search engines about the pages on your website that can be indexed.",
+ "sitemap.by": "by",
+ "sitemap.changefreq": "Change Frequency",
+ "sitemap.lastUpdated": "Last Updated",
+ "sitemap.priority": "Priority",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "No entries",
+ "utmShare.button": "Share",
+ "utmShare.parameters": "Parameters",
+ "utmShare.source.label": "Source",
+ "utmShare.source.placeholder": "e.g. google, newsletter",
+ "utmShare.medium.label": "Medium",
+ "utmShare.medium.placeholder": "e.g. cpc, email, social",
+ "utmShare.campaign.label": "Campaign",
+ "utmShare.campaign.placeholder": "e.g. spring_sale",
+ "utmShare.content.label": "Content",
+ "utmShare.content.placeholder": "e.g. logo_link",
+ "utmShare.term.label": "Term",
+ "utmShare.term.placeholder": "e.g. running shoes",
+ "utmShare.ref.label": "Ref",
+ "utmShare.ref.placeholder": "e.g. getkirby.com",
+ "ai.action.generate": "Generate with AI",
+ "ai.action.regenerate": "Regenerate",
+ "ai.action.edit": "Edit…",
+ "ai.action.customize": "Customize AI generation…",
+ "ai.action.stop": "Stop",
+ "ai.error.request": "The AI request failed. Please try again.",
+ "ai.error.disabled": "AI features are disabled.",
+ "ai.error.permission": "You do not have the permission to use AI features.",
+ "ai.dialog.instructions.label": "Instructions",
+ "ai.dialog.instructions.placeholder": "What changes do you want to make to the text?",
+ "ai.dialog.edit.submit": "Apply Changes",
+ "ai.dialog.custom.label": "Custom Instructions",
+ "ai.dialog.custom.placeholder": "What kind of content do you want to generate?",
+ "ai.dialog.custom.submit": "Generate",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Set up Google Cloud credentials to connect Search Console. See the docs for setup instructions.",
+ "sections.searchConsole.notConnected": "Connect your Google account to see search data for this page.",
+ "sections.searchConsole.selectProperty": "Select which Search Console property to use.",
+ "sections.searchConsole.selectPropertyButton": "Select Property",
+ "sections.searchConsole.selectPropertyLabel": "Property",
+ "sections.searchConsole.scDomain": "domain",
+ "sections.searchConsole.docs": "Docs",
+ "sections.searchConsole.connect": "Connect",
+ "sections.searchConsole.reconnect": "Reconnect",
+ "sections.searchConsole.noData": "No search data available for this page.",
+ "sections.searchConsole.showMore": "Show all",
+ "sections.searchConsole.sortBy": "Sort by",
+ "sections.searchConsole.query": "Query",
+ "sections.searchConsole.clicks": "Clicks",
+ "sections.searchConsole.impressions": "Impressions",
+ "sections.searchConsole.ctr": "CTR",
+ "sections.searchConsole.position": "Position",
+ "sections.searchConsole.openInGsc": "Open in Search Console",
+ "altText.decorative.on": "No description needed (purely visual)",
+ "altText.decorative.off": "Needs description",
+ "job.generateAltText": "Generate Alt Text",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/kirby-seo/translations/fr.json b/site/plugins/kirby-seo/translations/fr.json
new file mode 100644
index 0000000..acb541c
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/fr.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Structure des titres",
+ "sections.headingStructure.errors.incorrectOrder": "Votre structure de titres a un ordre incorrect et est invalide.",
+ "sections.headingStructure.errors.missingH1": "Votre structure de titres ne contient pas de balise H1 et est invalide.",
+ "sections.headingStructure.errors.multipleH1": "Votre structure de titres contient plus d'une balise H1 et est invalide.",
+ "sections.preview.title": "Aperçu",
+ "sections.preview.titleWithPage": "Aperçu (affiche « {title} »)",
+ "sections.preview.viewPage": "Voir la page",
+ "sections.preview.showFor": "Montre-moi",
+ "sections.preview.openDebugger": "Ouvrir le débogueur de partage",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "Indexation autorisée",
+ "fields.robots.indicator.any": "Indexation partiellement interdite",
+ "fields.robots.indicator.noindex": "Indexation interdite",
+ "fields.robots.label": "Directives des robots",
+ "fields.robots.index.label": "Indexation",
+ "fields.robots.index.help": "Indique si les moteurs de recherche peuvent indexer cette page.",
+ "fields.robots.follow.label": "Suivre les liens",
+ "fields.robots.follow.help": "Indique si les moteurs de recherche peuvent suivre les liens de cette page.",
+ "fields.robots.archive.label": "Archiver",
+ "fields.robots.archive.help": "Indique si les moteurs de recherche peuvent archiver cette page.",
+ "fields.robots.imageindex.label": "Indexation des images",
+ "fields.robots.imageindex.help": "Indique si les moteurs de recherche peuvent indexer les images de cette page.",
+ "fields.robots.snippet.label": "Extraits",
+ "fields.robots.snippet.help": "Indique si les moteurs de recherche peuvent afficher des extraits de texte de cette page.",
+ "fields.robots.ai.label": "Entraînement IA",
+ "fields.robots.ai.help": "Indique si les fournisseurs d'IA peuvent utiliser cette page pour l'entraînement.",
+ "fields.robots.imageai.label": "Entraînement IA des images",
+ "fields.robots.imageai.help": "Indique si les fournisseurs d'IA peuvent utiliser les images de cette page pour l'entraînement.",
+ "tabs.seo": "Métadonnées & SEO",
+ "site.meta.headline": "Paramètres SEO globaux",
+ "site.meta.headline.help": "Ces paramètres sont utilisés pour toutes les pages qui n'ont pas leurs propres métadonnées.\nVous pouvez les remplacer pour chaque page.",
+ "fields.metaTitleTemplate.label": "Modèle de titre",
+ "fields.metaTitleTemplate.help": "Un modèle à utiliser pour tous les titres de page.",
+ "fields.metaDescription.label": "Description de la page",
+ "fields.metaDescription.help": "Longueur recommandée de 150 caractères maximum. Utilisée si aucune description de page n'est spécifiée.",
+ "site.og.headline": "Paramètres globaux Open Graph",
+ "site.og.headline.help": "Définissez l'apparence de votre site web lorsqu'il est partagé sur les réseaux sociaux tels que Facebook ou Twitter.",
+ "fields.ogTitleTemplate.label": "Modèle de titre Open Graph",
+ "fields.ogDescription.label": "Description Open Graph",
+ "fields.ogSiteName.label": "Nom du site Open Graph",
+ "fields.ogImage.label": "Image Open Graph",
+ "fields.ogImage.help": "Taille recommandée de 1200x630 pixels.",
+ "fields.ogImage.empty": "Aucune image Open Graph sélectionnée",
+ "fields.cropOgImage.label": "Recadrer l'image OG à la taille recommandée?",
+ "fields.cropOgImage.help": "La taille recommandée est de 1200x630px. Lorsqu'activé, les images seront automatiquement recadrées à cette taille pour un affichage optimal sur les réseaux sociaux.",
+ "fields.socialMediaAccounts.label": "Comptes de réseaux sociaux",
+ "fields.socialMediaAccounts.help": "URLs ou @nom_utilisateur de vos comptes de réseaux sociaux. Utilisés pour mettre des liens vers vos comptes dans les métadonnées.",
+ "page.meta.headline": "Paramètres SEO",
+ "page.og.headline": "Paramètres Open Graph",
+ "fields.titleOverwrite.label": "Titre (remplacement)",
+ "fields.inheritSettings.label": "Hériter des paramètres",
+ "fields.inheritSettings.help": "Sélectionnez les paramètres à hériter par les sous-pages.\nCela peut être utile, par exemple, si tous les articles d'un blog doivent avoir leur propre modèle de titre, différent de celui de la page par défaut. Tous les paramètres peuvent toujours être remplacés dans la page principale.",
+ "fields.useTitleTemplate.label": "Utiliser le modèle de titre ?",
+ "fields.useTitleTemplate.no": "Non - seulement le titre",
+ "fields.useTitleTemplate.yes": "Oui - avec le modèle",
+ "fields.useTitleTemplate.help": "Indique si le modèle de titre doit être utilisé. Ne sera pas hérité.",
+ "writerNodes.template.title": "Titre de la page",
+ "writerNodes.template.siteTitle": "Titre du site",
+ "common.default": "Par défaut :",
+ "common.yes": "Oui",
+ "common.no": "Non",
+ "sitemap.title": "Sitemap",
+ "sitemap.index": "Index de sitemap",
+ "sitemap.description": "Il s'agit du sitemap de votre site web qui informe les moteurs de recherche des pages de votre site web qui peuvent être indexées.",
+ "sitemap.by": "par",
+ "sitemap.changefreq": "Fréquence de changement",
+ "sitemap.lastUpdated": "Dernière mise à jour",
+ "sitemap.priority": "Priorité",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "Aucune entrée",
+ "utmShare.button": "Partager",
+ "utmShare.parameters": "Paramètres",
+ "utmShare.source.label": "Source",
+ "utmShare.source.placeholder": "ex. google, newsletter",
+ "utmShare.medium.label": "Support",
+ "utmShare.medium.placeholder": "ex. cpc, email, social",
+ "utmShare.campaign.label": "Campagne",
+ "utmShare.campaign.placeholder": "ex. soldes_printemps",
+ "utmShare.content.label": "Contenu",
+ "utmShare.content.placeholder": "ex. lien_logo",
+ "utmShare.term.label": "Terme",
+ "utmShare.term.placeholder": "ex. chaussures de course",
+ "utmShare.ref.label": "Réf",
+ "utmShare.ref.placeholder": "ex. getkirby.com",
+ "ai.action.generate": "Générer avec l'IA",
+ "ai.action.regenerate": "Régénérer",
+ "ai.action.edit": "Modifier...",
+ "ai.action.customize": "Personnaliser la génération IA...",
+ "ai.action.stop": "Arrêter",
+ "ai.error.request": "La requête IA a échoué. Veuillez réessayer.",
+ "ai.error.disabled": "Les fonctionnalités d'IA sont désactivées.",
+ "ai.error.permission": "Vous n'avez pas la permission d'utiliser les fonctionnalités d'IA.",
+ "ai.dialog.instructions.label": "Instructions",
+ "ai.dialog.instructions.placeholder": "Quelles modifications souhaitez-vous apporter au texte ?",
+ "ai.dialog.edit.submit": "Appliquer les modifications",
+ "ai.dialog.custom.label": "Instructions personnalisées",
+ "ai.dialog.custom.placeholder": "Quel type de contenu souhaitez-vous générer ?",
+ "ai.dialog.custom.submit": "Générer",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Configure les identifiants Google Cloud pour connecter Search Console. Consulte la documentation pour les instructions de configuration.",
+ "sections.searchConsole.notConnected": "Connecte ton compte Google pour voir les données de recherche de cette page.",
+ "sections.searchConsole.selectProperty": "Sélectionne la propriété Search Console à utiliser.",
+ "sections.searchConsole.selectPropertyButton": "Sélectionner la propriété",
+ "sections.searchConsole.selectPropertyLabel": "Propriété",
+ "sections.searchConsole.scDomain": "domaine",
+ "sections.searchConsole.docs": "Documentation",
+ "sections.searchConsole.connect": "Connecter",
+ "sections.searchConsole.reconnect": "Reconnecter",
+ "sections.searchConsole.noData": "Aucune donnée de recherche disponible pour cette page.",
+ "sections.searchConsole.showMore": "Tout afficher",
+ "sections.searchConsole.sortBy": "Trier par",
+ "sections.searchConsole.query": "Requête",
+ "sections.searchConsole.clicks": "Clics",
+ "sections.searchConsole.impressions": "Impressions",
+ "sections.searchConsole.ctr": "CTR",
+ "sections.searchConsole.position": "Position",
+ "sections.searchConsole.openInGsc": "Ouvrir dans Search Console",
+ "altText.decorative.on": "Aucune description requise (purement visuel)",
+ "altText.decorative.off": "Nécessite une description",
+ "job.generateAltText": "Générer le texte alternatif",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/kirby-seo/translations/nl.json b/site/plugins/kirby-seo/translations/nl.json
new file mode 100644
index 0000000..1bdc6f2
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/nl.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Heading structuur",
+ "sections.headingStructure.errors.incorrectOrder": "Je heading structuur heeft een onjuiste volgorde en is ongeldig.",
+ "sections.headingStructure.errors.missingH1": "Je heading structuur bevat geen H1 en is ongeldig.",
+ "sections.headingStructure.errors.multipleH1": "Je heading structuur bevat meer dan één H1 en is ongeldig.",
+ "sections.preview.title": "Voorvertoning",
+ "sections.preview.titleWithPage": "Voorbeeld (toont \"{title}\")",
+ "sections.preview.viewPage": "Pagina bekijken",
+ "sections.preview.showFor": "Voorbeeld op",
+ "sections.preview.openDebugger": "Open Sharing Debugger",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "Indexeren toegestaan",
+ "fields.robots.indicator.any": "Indexeren gedeeltelijk niet toegestaan",
+ "fields.robots.indicator.noindex": "Indexeren niet toegestaan",
+ "fields.robots.label": "Robots Directives",
+ "fields.robots.index.label": "Indexeren",
+ "fields.robots.index.help": "Of zoekmachines deze pagina mogen indexeren.",
+ "fields.robots.follow.label": "Volg Links",
+ "fields.robots.follow.help": "Of zoekmachines links op deze pagina mogen volgen.",
+ "fields.robots.archive.label": "Archiveren",
+ "fields.robots.archive.help": "Of zoekmachines deze pagina mogen archiveren.",
+ "fields.robots.imageindex.label": "Afbeeldingen indexeren",
+ "fields.robots.imageindex.help": "Of zoekmachines afbeeldingen op deze pagina mogen indexeren.",
+ "fields.robots.snippet.label": "Fragmenten",
+ "fields.robots.snippet.help": "Of zoekmachines tekstfragmenten van deze pagina mogen tonen.",
+ "fields.robots.ai.label": "AI-training",
+ "fields.robots.ai.help": "Of AI-aanbieders deze pagina mogen gebruiken voor training.",
+ "fields.robots.imageai.label": "AI-beeldtraining",
+ "fields.robots.imageai.help": "Of AI-aanbieders afbeeldingen op deze pagina mogen gebruiken voor training.",
+ "tabs.seo": "Metadata & SEO",
+ "site.meta.headline": "Globale SEO Instellingen",
+ "site.meta.headline.help": "Deze instellingen worden gebruikt voor alle pagina's die geen eigen metadata hebben.\nJe kunt ze voor elke pagina overschrijven.",
+ "fields.metaTitleTemplate.label": "Titel template",
+ "fields.metaTitleTemplate.help": "Een sjabloon voor alle paginatitels.",
+ "fields.metaDescription.label": "Paginabeschrijving",
+ "fields.metaDescription.help": "Aanbevolen lengte van maximaal 150 tekens. Gebruikt als er geen paginabeschrijving is opgegeven.",
+ "site.og.headline": "Globale Open Graph Instellingen",
+ "site.og.headline.help": "Bepaal hoe je website verschijnt wanneer deze wordt gedeeld op sociale netwerken zoals Facebook of Twitter.",
+ "fields.ogTitleTemplate.label": "Open Graph Titel Template",
+ "fields.ogDescription.label": "Open Graph Beschrijving",
+ "fields.ogSiteName.label": "Open Graph Sitenaam",
+ "fields.ogImage.label": "Open Graph Afbeelding",
+ "fields.ogImage.help": "Aanbevolen grootte van 1200x630 pixels.",
+ "fields.ogImage.empty": "Geen Open Graph Afbeelding geselecteerd",
+ "fields.cropOgImage.label": "OG-afbeelding bijsnijden naar aanbevolen grootte?",
+ "fields.cropOgImage.help": "Aanbevolen grootte is 1200x630px. Indien ingeschakeld, worden afbeeldingen automatisch bijgesneden naar deze grootte voor optimale weergave op sociale media.",
+ "fields.socialMediaAccounts.label": "Sociale Media Accounts",
+ "fields.socialMediaAccounts.help": "URL's of @handles naar je sociale media accounts. Gebruikt om links naar je accounts in de metadata te plaatsen.",
+ "page.meta.headline": "SEO-instellingen",
+ "page.og.headline": "Open Graph-instellingen",
+ "fields.titleOverwrite.label": "Titel (overschrijven)",
+ "fields.inheritSettings.label": "Instellingen Overerven",
+ "fields.inheritSettings.help": "Selecteer welke instellingen door subpagina's moeten worden geërfd.\nDit kan nuttig zijn, bijvoorbeeld als alle berichten van een blog hun eigen titelsjabloon moeten hebben, die verschilt van de standaard van de pagina. Alle instellingen kunnen nog steeds worden overschreven op de hoofdpagina.",
+ "fields.useTitleTemplate.label": "Titelsjabloon gebruiken?",
+ "fields.useTitleTemplate.no": "Nee - alleen titel",
+ "fields.useTitleTemplate.yes": "Ja - met sjabloon",
+ "fields.useTitleTemplate.help": "Geeft aan of het titelsjabloon moet worden gebruikt. Wordt niet geërfd.",
+ "writerNodes.template.title": "Paginatitel",
+ "writerNodes.template.siteTitle": "Sitenaam",
+ "common.default": "Standaard:",
+ "common.yes": "Ja",
+ "common.no": "Nee",
+ "sitemap.title": "Sitemap",
+ "sitemap.index": "Sitemap Index",
+ "sitemap.description": "Dit is de sitemap voor je website die zoekmachines informeert over de pagina's op je website die geïndexeerd kunnen worden.",
+ "sitemap.by": "door",
+ "sitemap.changefreq": "Veranderingsfrequentie",
+ "sitemap.lastUpdated": "Laatst bijgewerkt",
+ "sitemap.priority": "Prioriteit",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "Geen vermeldingen",
+ "utmShare.button": "Delen",
+ "utmShare.parameters": "Parameters",
+ "utmShare.source.label": "Bron",
+ "utmShare.source.placeholder": "bijv. google, nieuwsbrief",
+ "utmShare.medium.label": "Medium",
+ "utmShare.medium.placeholder": "bijv. cpc, email, social",
+ "utmShare.campaign.label": "Campagne",
+ "utmShare.campaign.placeholder": "bijv. lente_uitverkoop",
+ "utmShare.content.label": "Content",
+ "utmShare.content.placeholder": "bijv. logo_link",
+ "utmShare.term.label": "Zoekterm",
+ "utmShare.term.placeholder": "bijv. hardloopschoenen",
+ "utmShare.ref.label": "Ref",
+ "utmShare.ref.placeholder": "bijv. getkirby.com",
+ "ai.action.generate": "Genereer met AI",
+ "ai.action.regenerate": "Opnieuw genereren",
+ "ai.action.edit": "Bewerken...",
+ "ai.action.customize": "AI-generatie aanpassen...",
+ "ai.action.stop": "Stoppen",
+ "ai.error.request": "Het AI-verzoek is mislukt. Probeer het opnieuw.",
+ "ai.error.disabled": "AI-functies zijn uitgeschakeld.",
+ "ai.error.permission": "Je hebt geen toestemming om AI-functies te gebruiken.",
+ "ai.dialog.instructions.label": "Instructies",
+ "ai.dialog.instructions.placeholder": "Welke wijzigingen wil je aanbrengen in de tekst?",
+ "ai.dialog.edit.submit": "Wijzigingen toepassen",
+ "ai.dialog.custom.label": "Aangepaste instructies",
+ "ai.dialog.custom.placeholder": "Wat voor soort content wil je genereren?",
+ "ai.dialog.custom.submit": "Genereren",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Stel Google Cloud-inloggegevens in om Search Console te verbinden. Bekijk de documentatie voor installatie-instructies.",
+ "sections.searchConsole.notConnected": "Verbind je Google-account om zoekgegevens voor deze pagina te bekijken.",
+ "sections.searchConsole.selectProperty": "Selecteer welke Search Console-property je wilt gebruiken.",
+ "sections.searchConsole.selectPropertyButton": "Selecteer property",
+ "sections.searchConsole.selectPropertyLabel": "Property",
+ "sections.searchConsole.scDomain": "domein",
+ "sections.searchConsole.docs": "Documentatie",
+ "sections.searchConsole.connect": "Verbinden",
+ "sections.searchConsole.reconnect": "Opnieuw verbinden",
+ "sections.searchConsole.noData": "Geen zoekgegevens beschikbaar voor deze pagina.",
+ "sections.searchConsole.showMore": "Alles tonen",
+ "sections.searchConsole.sortBy": "Sorteer op",
+ "sections.searchConsole.query": "Zoekopdracht",
+ "sections.searchConsole.clicks": "Klikken",
+ "sections.searchConsole.impressions": "Vertoningen",
+ "sections.searchConsole.ctr": "CTR",
+ "sections.searchConsole.position": "Positie",
+ "sections.searchConsole.openInGsc": "Openen in Search Console",
+ "altText.decorative.on": "Geen beschrijving nodig (alleen decoratief)",
+ "altText.decorative.off": "Beschrijving vereist",
+ "job.generateAltText": "Alt-tekst genereren",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/kirby-seo/translations/pt_PT.json b/site/plugins/kirby-seo/translations/pt_PT.json
new file mode 100644
index 0000000..d516ba2
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/pt_PT.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Estrutura de Títulos",
+ "sections.headingStructure.errors.incorrectOrder": "A estrutura de títulos tem uma ordem incorrecta e é inválida.",
+ "sections.headingStructure.errors.missingH1": "A estrutura de títulos não contém uma tag H1 e é inválida.",
+ "sections.headingStructure.errors.multipleH1": "A estrutura de títulos contém mais do que uma tag H1 e é inválida.",
+ "sections.preview.title": "Pré-visualização",
+ "sections.preview.titleWithPage": "Pré-visualização (mostra \"{title}\")",
+ "sections.preview.viewPage": "Ver página",
+ "sections.preview.showFor": "Mostrar",
+ "sections.preview.openDebugger": "Abrir Sharing Debugger",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "Indexação permitida",
+ "fields.robots.indicator.any": "Indexação parcialmente proibida",
+ "fields.robots.indicator.noindex": "Indexação proibida",
+ "fields.robots.label": "Diretivas Robots",
+ "fields.robots.index.label": "Indexação",
+ "fields.robots.index.help": "Se os motores de pesquisa podem indexar esta página.",
+ "fields.robots.follow.label": "Seguir Links",
+ "fields.robots.follow.help": "Se os motores de pesquisa podem seguir links nesta página.",
+ "fields.robots.archive.label": "Arquivo",
+ "fields.robots.archive.help": "Se os motores de pesquisa podem arquivar esta página.",
+ "fields.robots.imageindex.label": "Indexação de Imagens",
+ "fields.robots.imageindex.help": "Se os motores de pesquisa podem indexar imagens desta página.",
+ "fields.robots.snippet.label": "Snippets",
+ "fields.robots.snippet.help": "Se os motores de pesquisa podem mostrar snippets de texto desta página.",
+ "fields.robots.ai.label": "Treino de IA",
+ "fields.robots.ai.help": "Se os fornecedores de IA podem usar esta página para treino.",
+ "fields.robots.imageai.label": "Treino de IA de imagens",
+ "fields.robots.imageai.help": "Se os fornecedores de IA podem usar imagens desta página para treino.",
+ "tabs.seo": "Metadados & SEO",
+ "site.meta.headline": "Configurações Globais SEO",
+ "site.meta.headline.help": "Estas configurações são usadas para todas as páginas que não têm os seus próprios metadados.\nPode substituí-las em cada página.",
+ "fields.metaTitleTemplate.label": "Template de Título",
+ "fields.metaTitleTemplate.help": "Um modelo para usar em todos os títulos de página.",
+ "fields.metaDescription.label": "Descrição de Página",
+ "fields.metaDescription.help": "Recomendado um tamanho de 150 caracteres no máximo. Usada se nenhuma descrição de página for especificada.",
+ "site.og.headline": "Configurações Globais Open Graph",
+ "site.og.headline.help": "Defina como o seu site aparece quando é partilhado em redes sociais como o Facebook ou o Twitter.",
+ "fields.ogTitleTemplate.label": "Template de Título Open Graph",
+ "fields.ogDescription.label": "Descrição Open Graph",
+ "fields.ogSiteName.label": "Nome do Site Open Graph",
+ "fields.ogImage.label": "Imagem Open Graph",
+ "fields.ogImage.help": "Tamanho recomendado de 1200x630 pixels.",
+ "fields.ogImage.empty": "Nenhuma Imagem Open Graph selecionada",
+ "fields.cropOgImage.label": "Cortar imagem OG para tamanho recomendado?",
+ "fields.cropOgImage.help": "O tamanho recomendado é 1200x630px. Quando ativado, as imagens serão automaticamente cortadas para este tamanho para exibição ideal em redes sociais.",
+ "fields.socialMediaAccounts.label": "Contas de Redes Sociais",
+ "fields.socialMediaAccounts.help": "URLs ou @handles para as suas contas de redes sociais. Usado para colocar links para as suas contas nos metadados.",
+ "page.meta.headline": "Definições de SEO",
+ "page.og.headline": "Definições de Open Graph",
+ "fields.titleOverwrite.label": "Título (substituir)",
+ "fields.inheritSettings.label": "Herdar Configurações",
+ "fields.inheritSettings.help": "Selecione quais as configurações que devem ser herdadas pelas subpáginas.\nIsto pode ser útil, por exemplo, se todos os posts de um blog tiverem o seu próprio template de título, que pode ser diferente do pré-configurado na página. Todas as configurações continuam a poder ser substituídas na página principal.",
+ "fields.useTitleTemplate.label": "Usar o template de título?",
+ "fields.useTitleTemplate.no": "Não - apenas o título",
+ "fields.useTitleTemplate.yes": "Sim - com template",
+ "fields.useTitleTemplate.help": "Especifica se o template de título deve ser usado. Não será herdado.",
+ "writerNodes.template.title": "Título da página",
+ "writerNodes.template.siteTitle": "Título do site",
+ "common.default": "Por defeito:",
+ "common.yes": "Sim",
+ "common.no": "Não",
+ "sitemap.title": "Sitemap",
+ "sitemap.index": "Índice Sitemap",
+ "sitemap.description": "Este é o sitemap do site que informa os motores de pesquisa sobre as páginas que podem ser indexadas.",
+ "sitemap.by": "por",
+ "sitemap.changefreq": "Frequência de Mudança",
+ "sitemap.lastUpdated": "Última Atualização",
+ "sitemap.priority": "Prioridade",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "Sem registos",
+ "utmShare.button": "Partilhar",
+ "utmShare.parameters": "Parâmetros",
+ "utmShare.source.label": "Origem",
+ "utmShare.source.placeholder": "ex: google, newsletter",
+ "utmShare.medium.label": "Meio",
+ "utmShare.medium.placeholder": "ex: cpc, email, social",
+ "utmShare.campaign.label": "Campanha",
+ "utmShare.campaign.placeholder": "ex: saldos_primavera",
+ "utmShare.content.label": "Conteúdo",
+ "utmShare.content.placeholder": "ex: link_logotipo",
+ "utmShare.term.label": "Termo",
+ "utmShare.term.placeholder": "ex: ténis de corrida",
+ "utmShare.ref.label": "Ref",
+ "utmShare.ref.placeholder": "ex: getkirby.com",
+ "ai.action.generate": "Gerar com IA",
+ "ai.action.regenerate": "Regenerar",
+ "ai.action.edit": "Editar...",
+ "ai.action.customize": "Personalizar geração de IA...",
+ "ai.action.stop": "Parar",
+ "ai.error.request": "O pedido de IA falhou. Por favor, tenta novamente.",
+ "ai.error.disabled": "As funcionalidades de IA estão desativadas.",
+ "ai.error.permission": "Não tens permissão para usar as funcionalidades de IA.",
+ "ai.dialog.instructions.label": "Instruções",
+ "ai.dialog.instructions.placeholder": "Que alterações queres fazer ao texto?",
+ "ai.dialog.edit.submit": "Aplicar alterações",
+ "ai.dialog.custom.label": "Instruções personalizadas",
+ "ai.dialog.custom.placeholder": "Que tipo de conteúdo queres gerar?",
+ "ai.dialog.custom.submit": "Gerar",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Configura as credenciais do Google Cloud para conectar a Search Console. Consulta a documentação para instruções de configuração.",
+ "sections.searchConsole.notConnected": "Conecta a tua conta Google para veres os dados de pesquisa desta página.",
+ "sections.searchConsole.selectProperty": "Seleciona qual propriedade da Search Console queres usar.",
+ "sections.searchConsole.selectPropertyButton": "Selecionar propriedade",
+ "sections.searchConsole.selectPropertyLabel": "Propriedade",
+ "sections.searchConsole.scDomain": "domínio",
+ "sections.searchConsole.docs": "Documentação",
+ "sections.searchConsole.connect": "Conectar",
+ "sections.searchConsole.reconnect": "Reconectar",
+ "sections.searchConsole.noData": "Não há dados de pesquisa disponíveis para esta página.",
+ "sections.searchConsole.showMore": "Mostrar tudo",
+ "sections.searchConsole.sortBy": "Ordenar por",
+ "sections.searchConsole.query": "Consulta",
+ "sections.searchConsole.clicks": "Cliques",
+ "sections.searchConsole.impressions": "Impressões",
+ "sections.searchConsole.ctr": "CTR",
+ "sections.searchConsole.position": "Posição",
+ "sections.searchConsole.openInGsc": "Abrir na Search Console",
+ "altText.decorative.on": "Não precisa de descrição (é só visual)",
+ "altText.decorative.off": "Precisa de descrição",
+ "job.generateAltText": "Gerar texto alternativo",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/kirby-seo/translations/ro.json b/site/plugins/kirby-seo/translations/ro.json
new file mode 100644
index 0000000..979c2c6
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/ro.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Structura de subtitluri",
+ "sections.headingStructure.errors.incorrectOrder": "Structura de subtitluri are ordinea incorectă, ceea ce o face invalidă.",
+ "sections.headingStructure.errors.missingH1": "Structura de subtitluri nu conține un H1, ceea ce o face invalidă.",
+ "sections.headingStructure.errors.multipleH1": "Structura de subtitluri conține mai mult de un H1, ceea ce o face invalidă.",
+ "sections.preview.title": "Previzualizare",
+ "sections.preview.titleWithPage": "Previzualizare (afișează „{title}\")",
+ "sections.preview.viewPage": "Vezi pagina",
+ "sections.preview.showFor": "Arată-mi",
+ "sections.preview.openDebugger": "Deschide Sharing Debugger",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "Indexarea permisă",
+ "fields.robots.indicator.any": "Indexarea parțial interzisă",
+ "fields.robots.indicator.noindex": "Indexarea interzisă",
+ "fields.robots.label": "Directive Robots",
+ "fields.robots.index.label": "Indexare",
+ "fields.robots.index.help": "Dacă se permite motoarelor de căutare să indexeze această pagină.",
+ "fields.robots.follow.label": "Urmare linkuri",
+ "fields.robots.follow.help": "Dacă se permite motoarelor de căutare să urmeze linkurile de pe această pagină.",
+ "fields.robots.archive.label": "Arhivare",
+ "fields.robots.archive.help": "Dacă se permite motoarelor de căutare să arhiveze această pagină.",
+ "fields.robots.imageindex.label": "Indexare imagini",
+ "fields.robots.imageindex.help": "Dacă se permite motoarelor de căutare să indexeze imaginile de pe această pagină.",
+ "fields.robots.snippet.label": "Fragmente",
+ "fields.robots.snippet.help": "Dacă se permite motoarelor de căutare să afișeze fragmente din această pagină.",
+ "fields.robots.ai.label": "Antrenare AI",
+ "fields.robots.ai.help": "Dacă se permite furnizorilor de AI să utilizeze această pagină pentru antrenare.",
+ "fields.robots.imageai.label": "Antrenare AI imagini",
+ "fields.robots.imageai.help": "Dacă se permite furnizorilor de AI să utilizeze imaginile de pe această pagină pentru antrenare.",
+ "tabs.seo": "Metadata și SEO",
+ "site.meta.headline": "Setări globale SEO",
+ "site.meta.headline.help": "Aceste setări sunt folosite pentru toate paginile care nu au metadata proprie.\nEle se pot suprascrie pentru fiecare pagină în parte.",
+ "fields.metaTitleTemplate.label": "Șablon pentru titlu",
+ "fields.metaTitleTemplate.help": "Șablonul implicit folosit pentru toate paginile.",
+ "fields.metaDescription.label": "Descrierea paginii",
+ "fields.metaDescription.help": "Lungimea recomandată este de maxim 150 de caractere. Folosită dacă o pagină nu are propria descriere specificată.",
+ "site.og.headline": "Setări globale Open Graph",
+ "site.og.headline.help": "Stabilește cum apare website-ul atunci când este partajat pe rețelele sociale ca Facebook sau Twitter.",
+ "fields.ogTitleTemplate.label": "Șablon pentru titlu Open Graph",
+ "fields.ogDescription.label": "Descriere Open Graph",
+ "fields.ogSiteName.label": "Nume site Open Graph",
+ "fields.ogImage.label": "Imagine Open Graph",
+ "fields.ogImage.help": "Dimensiunea recomandată este de 1200x630 pixeli.",
+ "fields.ogImage.empty": "Nicio imagine Open Graph aleasă",
+ "fields.cropOgImage.label": "Decupează imaginea OG la dimensiunea recomandată?",
+ "fields.cropOgImage.help": "Dimensiunea recomandată este de 1200x630px. Când setarea este activă, imaginile vor fi decupate automat la această dimensiune pentru o afișare ideală pe rețelele sociale.",
+ "fields.socialMediaAccounts.label": "Conturi sociale",
+ "fields.socialMediaAccounts.help": "URL-uri sau @username-uri către conturile tale pe rețelele sociale. Folosite pentru a pune linkuri către aceste conturi în metadata.",
+ "page.meta.headline": "Setări SEO",
+ "page.og.headline": "Setări Open Graph",
+ "fields.titleOverwrite.label": "Titlu (suprascrie)",
+ "fields.inheritSettings.label": "Moștenește setări",
+ "fields.inheritSettings.help": "Alege ce setări să moștenească subpaginile.\nPoate fi util când, de exemplu, toate postările unui blog folosesc un șablon de titlu comun, dar care diferă de șablonul implicit. Setările pot fi suprascrise oricând în subpagină.",
+ "fields.useTitleTemplate.label": "Folosește șablon pentru titlu?",
+ "fields.useTitleTemplate.no": "Nu - doar titlul",
+ "fields.useTitleTemplate.yes": "Da - cu șablon",
+ "fields.useTitleTemplate.help": "Dacă să se folosească șablonul de titlu. Nu se moștenește.",
+ "writerNodes.template.title": "Titlul paginii",
+ "writerNodes.template.siteTitle": "Titlul website-ului",
+ "common.default": "Implicit:",
+ "common.yes": "Da",
+ "common.no": "Nu",
+ "sitemap.title": "Sitemap",
+ "sitemap.index": "Index sitemap-uri",
+ "sitemap.description": "Acesta este sitemap-ul care informează motoarele de căutare despre paginile din website care pot fi indexate.",
+ "sitemap.by": "de",
+ "sitemap.changefreq": "Schimbă frecvența",
+ "sitemap.lastUpdated": "Ultima actualizare",
+ "sitemap.priority": "Prioritate",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "Nicio înregistrare",
+ "utmShare.button": "Distribuie",
+ "utmShare.parameters": "Parametri",
+ "utmShare.source.label": "Sursă",
+ "utmShare.source.placeholder": "ex: google, newsletter",
+ "utmShare.medium.label": "Mediu",
+ "utmShare.medium.placeholder": "ex: cpc, email, social",
+ "utmShare.campaign.label": "Campanie",
+ "utmShare.campaign.placeholder": "ex: spring_sale",
+ "utmShare.content.label": "Conținut",
+ "utmShare.content.placeholder": "ex: logo_link",
+ "utmShare.term.label": "Termen",
+ "utmShare.term.placeholder": "ex: pantofi de alergare",
+ "utmShare.ref.label": "Ref",
+ "utmShare.ref.placeholder": "ex: getkirby.com",
+ "ai.action.generate": "Generează cu AI",
+ "ai.action.regenerate": "Regenerare",
+ "ai.action.edit": "Editează…",
+ "ai.action.customize": "Personalizează generarea AI…",
+ "ai.action.stop": "Oprește",
+ "ai.error.request": "Solicitarea AI a eșuat. Te rugăm să încerci din nou.",
+ "ai.error.disabled": "Funcțiile AI sunt dezactivate.",
+ "ai.error.permission": "Nu ai permisiunea de a utiliza funcțiile AI.",
+ "ai.dialog.instructions.label": "Instrucțiuni",
+ "ai.dialog.instructions.placeholder": "Ce modificări dorești să faci textului?",
+ "ai.dialog.edit.submit": "Aplică modificările",
+ "ai.dialog.custom.label": "Instrucțiuni personalizate",
+ "ai.dialog.custom.placeholder": "Ce tip de conținut dorești să generezi?",
+ "ai.dialog.custom.submit": "Generează",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Configurează acreditările Google Cloud pentru a conecta Search Console. Vezi documentația pentru instrucțiuni de configurare.",
+ "sections.searchConsole.notConnected": "Conectează-ți contul Google ca să vezi datele de căutare pentru această pagină.",
+ "sections.searchConsole.selectProperty": "Selectează ce proprietate Search Console vrei să folosești.",
+ "sections.searchConsole.selectPropertyButton": "Selectează proprietatea",
+ "sections.searchConsole.selectPropertyLabel": "Proprietate",
+ "sections.searchConsole.scDomain": "domeniu",
+ "sections.searchConsole.docs": "Documentație",
+ "sections.searchConsole.connect": "Conectează",
+ "sections.searchConsole.reconnect": "Reconectează",
+ "sections.searchConsole.noData": "Nu există date de căutare disponibile pentru această pagină.",
+ "sections.searchConsole.showMore": "Afișează tot",
+ "sections.searchConsole.sortBy": "Sortează după",
+ "sections.searchConsole.query": "Interogare",
+ "sections.searchConsole.clicks": "Clickuri",
+ "sections.searchConsole.impressions": "Afișări",
+ "sections.searchConsole.ctr": "CTR",
+ "sections.searchConsole.position": "Poziție",
+ "sections.searchConsole.openInGsc": "Deschide în Search Console",
+ "altText.decorative.on": "Nu este necesară nicio descriere (doar element vizual)",
+ "altText.decorative.off": "Are nevoie de descriere",
+ "job.generateAltText": "Generează text alternativ",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/kirby-seo/translations/sv_SE.json b/site/plugins/kirby-seo/translations/sv_SE.json
new file mode 100644
index 0000000..cda8142
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/sv_SE.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Rubrikstruktur",
+ "sections.headingStructure.errors.incorrectOrder": "Din rubrikstruktur har en felaktig ordning och är ogiltig.",
+ "sections.headingStructure.errors.missingH1": "Din rubrikstruktur innehåller ingen H1 och är ogiltig.",
+ "sections.headingStructure.errors.multipleH1": "Din rubrikstruktur innehåller mer än en H1 och är ogiltig.",
+ "sections.preview.title": "Förhandsvisning",
+ "sections.preview.titleWithPage": "Förhandsgranskning (visar \"{title}\")",
+ "sections.preview.viewPage": "Visa sida",
+ "sections.preview.showFor": "Visa mig",
+ "sections.preview.openDebugger": "Öppna Sharing Debugger",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "Indexering tillåten",
+ "fields.robots.indicator.any": "Indexering delvis förbjuden",
+ "fields.robots.indicator.noindex": "Indexering förbjuden",
+ "fields.robots.label": "Robotdirektiv",
+ "fields.robots.index.label": "Indexering",
+ "fields.robots.index.help": "Om sökmotorer får indexera den här sidan.",
+ "fields.robots.follow.label": "Följ länkar",
+ "fields.robots.follow.help": "Om sökmotorer får följa länkar på den här sidan.",
+ "fields.robots.archive.label": "Arkiv",
+ "fields.robots.archive.help": "Om sökmotorer får arkivera den här sidan.",
+ "fields.robots.imageindex.label": "Bildindexering",
+ "fields.robots.imageindex.help": "Om sökmotorer får indexera bilder på den här sidan.",
+ "fields.robots.snippet.label": "Textutdrag",
+ "fields.robots.snippet.help": "Om sökmotorer får visa textutdrag från den här sidan.",
+ "fields.robots.ai.label": "AI-träning",
+ "fields.robots.ai.help": "Om AI-leverantörer får använda den här sidan för träning.",
+ "fields.robots.imageai.label": "AI-bildträning",
+ "fields.robots.imageai.help": "Om AI-leverantörer får använda bilder på den här sidan för träning.",
+ "tabs.seo": "Metadata och SEO",
+ "site.meta.headline": "Globala SEO-inställningar",
+ "site.meta.headline.help": "Dessa inställningar används för alla sidor som inte har egna metadata.\nDu kan åsidosätta dem för varje sida.",
+ "fields.metaTitleTemplate.label": "Titelmall",
+ "fields.metaTitleTemplate.help": "En mall att använda för alla sidtitlar.",
+ "fields.metaDescription.label": "Sidbeskrivning",
+ "fields.metaDescription.help": "Rekommenderad längd på max 150 tecken. Används om ingen sidbeskrivning anges.",
+ "site.og.headline": "Globala Open Graph-inställningar",
+ "site.og.headline.help": "Ställ in hur din webbplats visas när den delas på sociala nätverk som Facebook eller Twitter.",
+ "fields.ogTitleTemplate.label": "Open Graph titelmall",
+ "fields.ogDescription.label": "Open Graph beskrivning",
+ "fields.ogSiteName.label": "Open Graph webbplatsnamn",
+ "fields.ogImage.label": "Open Graph bild",
+ "fields.ogImage.help": "Rekommenderad storlek på 1200x630 pixlar.",
+ "fields.ogImage.empty": "Ingen Open Graph-bild har valts",
+ "fields.cropOgImage.label": "Beskär OG-bild till rekommenderad storlek?",
+ "fields.cropOgImage.help": "Rekommenderad storlek är 1200x630px. När aktiverad kommer bilder automatiskt beskäras till denna storlek för optimal visning på sociala medier.",
+ "fields.socialMediaAccounts.label": "Konton för sociala medier",
+ "fields.socialMediaAccounts.help": "URL:er eller @användarnamn till dina sociala mediekonton. Används för att lägga länkar till dina konton i metadata.",
+ "page.meta.headline": "SEO-inställningar",
+ "page.og.headline": "Open Graph-inställningar",
+ "fields.titleOverwrite.label": "Titel (skriv över)",
+ "fields.inheritSettings.label": "Ärv inställningar",
+ "fields.inheritSettings.help": "Välj vilka inställningar som ska ärvas av undersidor.\nDetta kan till exempel vara användbart om alla inlägg på en blogg ska ha en egen titelmall, som skiljer sig från sidans standard. Alla inställningar kan fortfarande åsidosättas på huvudsidan.",
+ "fields.useTitleTemplate.label": "Använd titelmall?",
+ "fields.useTitleTemplate.no": "Nej - bara titel",
+ "fields.useTitleTemplate.yes": "Ja - med mall",
+ "fields.useTitleTemplate.help": "Anger om titelmallen ska användas. Kommer inte att ärvas.",
+ "writerNodes.template.title": "Sidtitel",
+ "writerNodes.template.siteTitle": "Webbplatstitel",
+ "common.default": "Standard:",
+ "common.yes": "Ja",
+ "common.no": "Nej",
+ "sitemap.title": "Webbplatskarta",
+ "sitemap.index": "Webbplatskartans index",
+ "sitemap.description": "Detta är webbplatskartan för din webbplats som informerar sökmotorer om vilka sidor på din webbplats som kan indexeras.",
+ "sitemap.by": "av",
+ "sitemap.changefreq": "Ändra frekvens",
+ "sitemap.lastUpdated": "Senast uppdaterad",
+ "sitemap.priority": "Prioritet",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "Inga poster",
+ "utmShare.button": "Dela",
+ "utmShare.parameters": "Parametrar",
+ "utmShare.source.label": "Källa",
+ "utmShare.source.placeholder": "t.ex. google, nyhetsbrev",
+ "utmShare.medium.label": "Medium",
+ "utmShare.medium.placeholder": "t.ex. cpc, e-post, sociala medier",
+ "utmShare.campaign.label": "Kampanj",
+ "utmShare.campaign.placeholder": "t.ex. spring_sale",
+ "utmShare.content.label": "Innehåll",
+ "utmShare.content.placeholder": "t.ex. logo_link",
+ "utmShare.term.label": "Sökord",
+ "utmShare.term.placeholder": "t.ex. löparskor",
+ "utmShare.ref.label": "Ref",
+ "utmShare.ref.placeholder": "t.ex. getkirby.com",
+ "ai.action.generate": "Generera med AI",
+ "ai.action.regenerate": "Generera om",
+ "ai.action.edit": "Redigera…",
+ "ai.action.customize": "Anpassa AI-generering…",
+ "ai.action.stop": "Stoppa",
+ "ai.error.request": "AI-förfrågan misslyckades. Försök igen.",
+ "ai.error.disabled": "AI-funktioner är inaktiverade.",
+ "ai.error.permission": "Du har inte behörighet att använda AI-funktioner.",
+ "ai.dialog.instructions.label": "Instruktioner",
+ "ai.dialog.instructions.placeholder": "Vilka ändringar vill du göra i texten?",
+ "ai.dialog.edit.submit": "Tillämpa ändringar",
+ "ai.dialog.custom.label": "Anpassade instruktioner",
+ "ai.dialog.custom.placeholder": "Vilken typ av innehåll vill du generera?",
+ "ai.dialog.custom.submit": "Generera",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Ställ in Google Cloud-referenser för att ansluta Search Console. Se dokumentationen för installationsinstruktioner.",
+ "sections.searchConsole.notConnected": "Anslut ditt Google-konto för att se sökdata för den här sidan.",
+ "sections.searchConsole.selectProperty": "Välj vilken Search Console-egenskap du vill använda.",
+ "sections.searchConsole.selectPropertyButton": "Välj egenskap",
+ "sections.searchConsole.selectPropertyLabel": "Egenskap",
+ "sections.searchConsole.scDomain": "domän",
+ "sections.searchConsole.docs": "Dokumentation",
+ "sections.searchConsole.connect": "Anslut",
+ "sections.searchConsole.reconnect": "Anslut igen",
+ "sections.searchConsole.noData": "Ingen sökdata tillgänglig för den här sidan.",
+ "sections.searchConsole.showMore": "Visa alla",
+ "sections.searchConsole.sortBy": "Sortera efter",
+ "sections.searchConsole.query": "Sökfråga",
+ "sections.searchConsole.clicks": "Klick",
+ "sections.searchConsole.impressions": "Visningar",
+ "sections.searchConsole.ctr": "CTR",
+ "sections.searchConsole.position": "Position",
+ "sections.searchConsole.openInGsc": "Öppna i Search Console",
+ "altText.decorative.on": "Ingen beskrivning behövs (endast dekorativt)",
+ "altText.decorative.off": "Behöver beskrivning",
+ "job.generateAltText": "Generera alternativtext",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/kirby-seo/translations/tr.json b/site/plugins/kirby-seo/translations/tr.json
new file mode 100644
index 0000000..4bf9513
--- /dev/null
+++ b/site/plugins/kirby-seo/translations/tr.json
@@ -0,0 +1,125 @@
+{
+ "sections.headingStructure.title": "Başlık Yapısı",
+ "sections.headingStructure.errors.incorrectOrder": "Başlık yapınız yanlış bir sıraya sahip ve geçersizdir.",
+ "sections.headingStructure.errors.missingH1": "Başlık yapınızda bir H1 etiketi bulunmuyor ve geçersizdir.",
+ "sections.headingStructure.errors.multipleH1": "Başlık yapınızda birden fazla H1 etiketi bulunuyor ve geçersizdir.",
+ "sections.preview.title": "Önizleme",
+ "sections.preview.titleWithPage": "Önizleme (\"{title}\" gösteriliyor)",
+ "sections.preview.viewPage": "Sayfayı görüntüle",
+ "sections.preview.showFor": "Bana göster",
+ "sections.preview.openDebugger": "Paylaşım Hata Ayıklayıcıyı Aç",
+ "sections.preview.google": "Google",
+ "sections.preview.facebook": "Facebook",
+ "sections.preview.slack": "Slack",
+ "fields.robots.indicator.index": "İndekslemeye izin verildi",
+ "fields.robots.indicator.any": "İndeksleme kısmen yasaklandı",
+ "fields.robots.indicator.noindex": "İndeksleme yasaklandı",
+ "fields.robots.label": "Robot Talimatları",
+ "fields.robots.index.label": "İndeksleme",
+ "fields.robots.index.help": "Arama motorlarının bu sayfayı indeksleyip indekslemeyeceğini belirtir.",
+ "fields.robots.follow.label": "Bağlantıları Takip Et",
+ "fields.robots.follow.help": "Arama motorlarının bu sayfadaki bağlantıları takip edip etmeyeceğini belirtir.",
+ "fields.robots.archive.label": "Arşivle",
+ "fields.robots.archive.help": "Arama motorlarının bu sayfayı arşivleyip arşivlemeyeceğini belirtir.",
+ "fields.robots.imageindex.label": "İmaj İndeksleme",
+ "fields.robots.imageindex.help": "Arama motorlarının bu sayfadaki görüntüleri indeksleyip indekslemeyeceğini belirtir.",
+ "fields.robots.snippet.label": "Özetler",
+ "fields.robots.snippet.help": "Arama motorlarının bu sayfanın metin özetlerini gösterip göstermeyeceğini belirtir.",
+ "fields.robots.ai.label": "Yapay Zeka Eğitimi",
+ "fields.robots.ai.help": "Yapay zeka sağlayıcılarının bu sayfayı eğitim için kullanıp kullanamayacağını belirtir.",
+ "fields.robots.imageai.label": "Yapay Zeka Görsel Eğitimi",
+ "fields.robots.imageai.help": "Yapay zeka sağlayıcılarının bu sayfadaki görselleri eğitim için kullanıp kullanamayacağını belirtir.",
+ "tabs.seo": "Meta Veriler & SEO",
+ "site.meta.headline": "Genel SEO Ayarları",
+ "site.meta.headline.help": "Bu ayarlar, kendi meta verileri olmayan tüm sayfalar için kullanılır.\nHer sayfa için bunları geçersiz kılabilirsiniz.",
+ "fields.metaTitleTemplate.label": "Başlık Şablonu",
+ "fields.metaTitleTemplate.help": "Tüm sayfa başlıkları için kullanılacak bir şablon.",
+ "fields.metaDescription.label": "Sayfa Açıklaması",
+ "fields.metaDescription.help": "Maksimum 150 karakter uzunluğunda önerilen bir sayfa açıklamasıdır. Sayfa açıklaması belirtilmemişse kullanılır.",
+ "site.og.headline": "Genel Açık Grafik (OG) Ayarları",
+ "site.og.headline.help": "Web sitenizin Facebook veya Twitter gibi sosyal ağlarda paylaşıldığında nasıl göründüğünü ayarlar.",
+ "fields.ogTitleTemplate.label": "Açık Grafik (OG) Başlık Şablonu",
+ "fields.ogDescription.label": "Açık Grafik (OG) Açıklaması",
+ "fields.ogSiteName.label": "Açık Grafik (OG) Site Adı",
+ "fields.ogImage.label": "Açık Grafik (OG) Görseli",
+ "fields.ogImage.help": "Önerilen boyut 1200x630 pikseldir.",
+ "fields.ogImage.empty": "Boş Açık Grafik (OG) Görseli",
+ "fields.cropOgImage.label": "OG görselini önerilen boyuta kırp?",
+ "fields.cropOgImage.help": "Önerilen boyut 1200x630px'dir. Etkinleştirildiğinde, sosyal medyada en iyi görüntü için görseller otomatik olarak bu boyuta kırpılacaktır.",
+ "fields.socialMediaAccounts.label": "Sosyal Medya Hesapları",
+ "fields.socialMediaAccounts.help": "Sosyal medya hesaplarınızın URL'leri veya @kullanici_adi. Meta verilerinde hesaplarınıza bağlantı eklemek için kullanılır.",
+ "page.meta.headline": "SEO Ayarları",
+ "page.og.headline": "Open Graph Ayarları",
+ "fields.titleOverwrite.label": "Başlık (üzerine yaz)",
+ "fields.inheritSettings.label": "Ayarları Miras Al",
+ "fields.inheritSettings.help": "Alt sayfalar tarafından miras alınacak ayarları seçin.\nÖrneğin, bir blogun tüm yazılarının sayfa varsayılanından farklı bir başlık şablonuna sahip olması gerekiyorsa bu yardımcı olabilir. Tüm ayarlar ana sayfada hala geçersiz kılınabilir.",
+ "fields.useTitleTemplate.label": "Başlık şablonunu kullan?",
+ "fields.useTitleTemplate.no": "Hayır - sadece başlık",
+ "fields.useTitleTemplate.yes": "Evet - şablonla",
+ "fields.useTitleTemplate.help": "Başlık şablonunun kullanılıp kullanılmayacağını belirtir. Miras alınmayacaktır.",
+ "writerNodes.template.title": "Sayfa başlığı",
+ "writerNodes.template.siteTitle": "Site başlığı",
+ "common.default": "Varsayılan:",
+ "common.yes": "Evet",
+ "common.no": "Hayır",
+ "sitemap.title": "Site Haritası",
+ "sitemap.index": "Site Haritası Dizini",
+ "sitemap.description": "Web sitenizdeki dizine eklenebilecek sayfalar hakkında arama motorlarını bilgilendiren web sitenizin site haritasıdır.",
+ "sitemap.by": "tarafından",
+ "sitemap.changefreq": "Değişim Sıklığı",
+ "sitemap.lastUpdated": "Son Güncelleme",
+ "sitemap.priority": "Öncelik",
+ "sitemap.url": "URL",
+ "sitemap.noEntries": "Giriş yok",
+ "utmShare.button": "Paylaş",
+ "utmShare.parameters": "Parametreler",
+ "utmShare.source.label": "Kaynak",
+ "utmShare.source.placeholder": "örn. google, bülten",
+ "utmShare.medium.label": "Ortam",
+ "utmShare.medium.placeholder": "örn. cpc, e-posta, sosyal medya",
+ "utmShare.campaign.label": "Kampanya",
+ "utmShare.campaign.placeholder": "örn. bahar_indirimi",
+ "utmShare.content.label": "İçerik",
+ "utmShare.content.placeholder": "örn. logo_linki",
+ "utmShare.term.label": "Terim",
+ "utmShare.term.placeholder": "örn. koşu ayakkabıları",
+ "utmShare.ref.label": "Ref",
+ "utmShare.ref.placeholder": "örn. getkirby.com",
+ "ai.action.generate": "Yapay zeka ile oluştur",
+ "ai.action.regenerate": "Yeniden oluştur",
+ "ai.action.edit": "Düzenle...",
+ "ai.action.customize": "Yapay zeka oluşturmayı özelleştir...",
+ "ai.action.stop": "Durdur",
+ "ai.error.request": "Yapay zeka isteği başarısız oldu. Lütfen tekrar dene.",
+ "ai.error.disabled": "Yapay zeka özellikleri devre dışı.",
+ "ai.error.permission": "Yapay zeka özelliklerini kullanma izniniz yok.",
+ "ai.dialog.instructions.label": "Talimatlar",
+ "ai.dialog.instructions.placeholder": "Metinde hangi değişiklikleri yapmak istiyorsun?",
+ "ai.dialog.edit.submit": "Değişiklikleri uygula",
+ "ai.dialog.custom.label": "Özel talimatlar",
+ "ai.dialog.custom.placeholder": "Ne tür bir içerik oluşturmak istiyorsun?",
+ "ai.dialog.custom.submit": "Oluştur",
+ "sections.searchConsole.title": "Google Search Console",
+ "sections.searchConsole.noCredentials": "Search Console'u bağlamak için Google Cloud kimlik bilgilerini ayarlayın. Kurulum talimatları için belgelere göz atın.",
+ "sections.searchConsole.notConnected": "Bu sayfa için arama verilerini görmek üzere Google hesabınızı bağlayın.",
+ "sections.searchConsole.selectProperty": "Hangi Search Console özelliğinin kullanılacağını seçin.",
+ "sections.searchConsole.selectPropertyButton": "Özellik seç",
+ "sections.searchConsole.selectPropertyLabel": "Özellik",
+ "sections.searchConsole.scDomain": "alan adı",
+ "sections.searchConsole.docs": "Belgeler",
+ "sections.searchConsole.connect": "Bağlan",
+ "sections.searchConsole.reconnect": "Yeniden bağlan",
+ "sections.searchConsole.noData": "Bu sayfa için kullanılabilir arama verisi yok.",
+ "sections.searchConsole.showMore": "Tümünü göster",
+ "sections.searchConsole.sortBy": "Sıralama ölçütü",
+ "sections.searchConsole.query": "Sorgu",
+ "sections.searchConsole.clicks": "Tıklamalar",
+ "sections.searchConsole.impressions": "Gösterimler",
+ "sections.searchConsole.ctr": "TO",
+ "sections.searchConsole.position": "Konum",
+ "sections.searchConsole.openInGsc": "Search Console'da aç",
+ "altText.decorative.on": "Açıklama gerekmez (sadece görsel amaçlı)",
+ "altText.decorative.off": "Açıklama gerekli",
+ "job.generateAltText": "Alt metin oluştur",
+ "job.indexNow": "IndexNow"
+}
diff --git a/site/plugins/promote-button/.editorconfig b/site/plugins/promote-button/.editorconfig
new file mode 100644
index 0000000..494ee04
--- /dev/null
+++ b/site/plugins/promote-button/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.php]
+indent_size = 2
\ No newline at end of file
diff --git a/site/plugins/promote-button/.gitignore b/site/plugins/promote-button/.gitignore
new file mode 100644
index 0000000..6607993
--- /dev/null
+++ b/site/plugins/promote-button/.gitignore
@@ -0,0 +1,10 @@
+# System files
+# ---------------------
+.DS_Store
+node_modules
+package-lock.json
+Icon
+
+# Lock Files
+# ---------------------
+.lock
diff --git a/site/plugins/promote-button/LICENSE b/site/plugins/promote-button/LICENSE
new file mode 100644
index 0000000..6db010a
--- /dev/null
+++ b/site/plugins/promote-button/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Scott Boms
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/site/plugins/promote-button/README.md b/site/plugins/promote-button/README.md
new file mode 100644
index 0000000..741e91d
--- /dev/null
+++ b/site/plugins/promote-button/README.md
@@ -0,0 +1,111 @@
+# Promote Button for Kirby
+
+
+
+A customizable View Button for Kirby 5 that builds on Bastian's demo from the [Kirby 5 Release Show](https://youtube.com/watch?v=o2xkzqiLEUM) adding missing functionality and configuration settings for Mastodon, Bluesky, and LinkedIn as well as other user-experience enhancements.
+
+## Requirements
+
+This plugin requires Kirby 5.x and newer. It will not work with earlier versions of Kirby.
+
+
+## Installation
+
+### [Kirby CLI](https://github.com/getkirby/cli)
+
+ kirby plugin:install scottboms/kirby-promote-button
+
+### Git submodule
+
+ git submodule add https://github.com/scottboms/kirby-promote-button.git site/plugins/promote-button
+
+### Copy and Paste
+
+1. [Download](https://github.com/scottboms/kirby-promote-button/archive/master.zip) the contents of this repository as Zip file.
+2. Rename the extracted folder to `promote-button` and copy it into the `site/plugins/` directory in your project.
+
+
+## Configuration
+
+To function, the plugin requires configuration as outlined below.
+
+### Required Settings
+
+Add these settings to your `/site/config/config.php` or `/site/config/env.php` file. Define which services you wish to use and then replace the CAPITALIZED PLACEHOLDERS with the necessary values.
+
+#### General
+
+```php
+ [
+ 'services' => [
+ 'mastodon',
+ 'bluesky',
+ 'linkedin'
+ ],
+ 'mastodon' => [
+ 'username' => 'USERNAME', // e.g. scottboms
+ 'url' => 'MASTODON_HOST', // e.g. mastodon.social
+ ],
+ 'bluesky' => [
+ 'base_url' => 'BLUESKY_HOST', // e.g. bsky.social
+ 'handle' => 'USERNAME', // e.g. example.bsky.social
+ ]
+ ],
+ ]
+```
+
+
+#### Tokens and Passwords
+
+To post to [Mastodon](https://mastodon.social/settings/applications), [Bluesky](https://bsky.app/settings/app-passwords) or [LinkedIn](https://linkedin.com/developers/apps), you will need the necessary authentication tokens or app passwords. Because this information is sensitive, you should not include these settings in your `/site/config/config.php` file and instead place them in the [env.php config file](https://getkirby.com/docs/guide/configuration#multi-environment-setup__deployment-configuration) which should be added to a `.gitignore` file to avoid sharing this info publicly.
+
+```php
+ 'MASTODON_API_TOKEN',
+ 'scottboms.promote.bluesky.password' => 'BLUESKY_APP_PASSWORD',
+ 'scottboms.promote.linkedin.token' => 'LINKEDIN_OAUTH_TOKEN',
+ ],
+```
+
+
+### Optional Settings
+
+If you run your Kirby site locally, the Promote button will function but page urls added to the dialog will use the local hostname (e.g. localhost) which isn't very helpful when posting to public services. You can override this behaviour by setting `host_url` in the configuration.
+
+```php
+ [
+ 'host_url' => 'SHARED_LINK_HOST', // e.g. https://example.com
+ ],
+ ]
+```
+
+
+## Blueprint Configuration
+
+There are multiple methods to add [View Buttons](https://getkirby.com/releases/5/view-buttons) to your Kirby installation. This plugin includes two distinct View Buttons -- the Promote button to access the core features of this plugin, and the Profile button which currently allows a way to quickly go to a Mastodon profile. The buttons can be added to any page by adding the `buttons` [option](https://getkirby.com/docs/reference/panel/blueprints/page#view-buttons) in a Page or Site Blueprint.
+
+```yml
+buttons:
+ promote: true
+ profile: true
+```
+
+## Credits
+
+* Original Concept and Starting Points: [Bastian Allgeier](https://github.com/bastianallgeier/)
+* Supported Services: [Mastodon](https://mastodon.social), [Bluesky](https://bsky.app), [LinkedIn](https://linkedin.com)
+
+
+## Disclaimer
+
+This plugin is provided "as is" with no guarantee. Use it at your own risk and always test before using it in a production environment. If you identify an issue, typo, etc, please [create a new issue](/issues/new) so I can investigate.
+
+
+## License
+
+[MIT](https://opensource.org/licenses/MIT)
diff --git a/site/plugins/promote-button/classes/PlatformPromoter.php b/site/plugins/promote-button/classes/PlatformPromoter.php
new file mode 100644
index 0000000..88f6d05
--- /dev/null
+++ b/site/plugins/promote-button/classes/PlatformPromoter.php
@@ -0,0 +1,303 @@
+config = [
+ 'mastodon' => [
+ 'token' => option('scottboms.promote.mastodon.token'),
+ 'username' => option('scottboms.promote.mastodon.username'),
+ 'url' => option('scottboms.promote.mastodon.url', 'mastodon.social'),
+ ],
+ 'linkedin' => [
+ 'username' => option('scottboms.promote.linkedin.username'),
+ 'token' => option('scottboms.promote.linkedin.token'),
+ ],
+ 'bluesky' => [
+ 'base_url' => option('scottboms.promote.bluesky.base_url', 'bsky.social'),
+ 'handle' => option('scottboms.promote.bluesky.handle'),
+ 'password' => option('scottboms.promote.bluesky.password'),
+ ],
+
+ ];
+ }
+
+ // --------------------------------------------------------------------------
+ // Posting...
+ public function post(string $platform, string $text): void
+ {
+ if (!method_exists($this, $platform)) {
+ throw new Exception("Platform '$platform' is not supported.");
+ }
+
+ $this->{$platform}($text);
+ }
+
+ // --------------------------------------------------------------------------
+ // Mastodon
+ protected function mastodon(string $text): void
+ {
+ $text = trim($text); // remove whitespace
+
+ if ($text === '') {
+ // $this->log('mastodon', 'error', 'Aborted: status text is empty');
+ throw new Exception('Mastodon post failed: status text is empty');
+ }
+
+ $token = $this->config['mastodon']['token'];
+ $baseUrl = 'https://' . rtrim($this->config['mastodon']['url'], '/');
+ $url = $baseUrl . '/api/v1/statuses';
+
+ // $this->log('mastodon', 'debug', 'Posting status: ' . $text);
+
+ // Ensure proper encoding
+ $text = mb_convert_encoding($text, 'UTF-8', 'auto');
+ $idempotencyKey = hash('sha256', $text); // hash of the content
+
+ $payloadArray = [
+ 'status' => $text,
+ 'language' => 'en',
+ 'visibility' => 'public'
+ ];
+
+ $payload = json_encode($payloadArray);
+
+ if ($payload === false) {
+ // $this->log('mastodon', 'error', 'Failed to encode payload: ' . json_last_error_msg());
+ throw new Exception('Mastodon post failed: JSON encoding error');
+ }
+
+ $this->log('mastodon', 'debug', 'Final payload: ' . $payload);
+
+ // Use cURL to post to Mastodon
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Authorization: Bearer ' . $token,
+ 'Content-Type: application/json',
+ 'Content-Length: ' . strlen($payload),
+ 'Idempotency-Key: ' . $idempotencyKey,
+ 'User-Agent: ' . self::USER_AGENT,
+ ]);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+ $this->log('mastodon', 'debug', 'Response HTTP code: ' . $httpCode);
+ // $this->log('mastodon', 'debug', 'Response: ' . $response);
+
+ if (!in_array($httpCode, [200, 202])) {
+ throw new Exception('Mastodon post failed with code ' . $httpCode . ': ' . $response);
+ }
+
+ $this->log('mastodon', 'info', 'Post succeeded.');
+ }
+
+ // --------------------------------------------------------------------------
+ // Bluesky
+ protected function bluesky(string $text): void
+ {
+ // Step 1: Authenticate
+ $identifier = preg_replace('/[^\x21-\x7E]/', '', ltrim($this->config['bluesky']['handle'], '@'));
+ $password = $this->config['bluesky']['password'];
+ $baseUrl = 'https://' . rtrim($this->config['bluesky']['base_url'] ?? 'https://bsky.social', '/');
+
+ $payload = json_encode([
+ 'identifier' => $identifier,
+ 'password' => $password,
+ ]);
+
+ // Use native cURL, json_encode/decode seemed to fail
+ $ch = curl_init($baseUrl . '/xrpc/com.atproto.server.createSession');
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+ 'Content-Length: ' . strlen($payload),
+ 'User-Agent: ' . self::USER_AGENT
+ ]);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+ $this->log('bluesky', 'debug', "cURL auth code: $httpCode");
+ // $this->log('bluesky', 'debug', "cURL auth response: $response");
+
+ $authData = json_decode($response, true);
+
+ if ($httpCode !== 200 || !isset($authData['accessJwt'])) {
+ throw new Exception('Bluesky authentication failed: ' . $response);
+ }
+
+ $accessJwt = $authData['accessJwt'];
+ $did = $authData['did'];
+
+ // Step 2: Post
+ $postPayload = json_encode([
+ 'collection' => 'app.bsky.feed.post',
+ 'repo' => $did,
+ 'record' => [
+ '$type' => 'app.bsky.feed.post',
+ 'text' => $text,
+ 'langs' => ['en-US'], // must be array
+ 'createdAt' => date('c'),
+ ]
+ ]);
+
+ $ch = curl_init($baseUrl . '/xrpc/com.atproto.repo.createRecord');
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $postPayload);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $accessJwt,
+ 'Content-Length: ' . strlen($postPayload),
+ 'User-Agent: ' . self::USER_AGENT
+ ]);
+
+ $postResponse = curl_exec($ch);
+ $postHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+ $this->log('bluesky', 'debug', 'Post HTTP code: ' . $postHttpCode);
+ // $this->log('bluesky', 'debug', 'Post response: ' . $postResponse);
+
+ if ($postHttpCode !== 200) {
+ throw new Exception('Bluesky post failed: ' . $postResponse);
+ }
+
+ $this->log('bluesky', 'info', 'Post succeeded.');
+ }
+
+ // --------------------------------------------------------------------------
+ // LinkedIn
+ protected function getLinkedInAuthorUrn(): string
+ {
+ $cache = kirby()->cache('linkedin');
+ $cacheKey = 'author_urn';
+
+ // Check cache first
+ if($this->cacheEnabled) {
+ $cachedUrn = $cache->get($cacheKey);
+ if($cachedUrn !== null) {
+ $this->log('linked', 'debug', 'Using cached LinkedIn author URN');
+ return $cachedUrn;
+ }
+ }
+
+ $token = $this->config['linkedin']['token'];
+
+ $response = Remote::get('https://api.linkedin.com/v2/userinfo', [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $token,
+ 'Content-Type' => 'application/json',
+ ],
+ ]);
+
+ if ($response->code() !== 200) {
+ throw new Exception('LinkedIn /userinfo failed: ' . $response->content());
+ }
+
+ $data = json_decode($response->content(), true);
+ if (!isset($data['sub'])) {
+ throw new \Exception('LinkedIn /userinfo response missing "sub" field.');
+ }
+
+ $urn = 'urn:li:person:' . $data['sub'];
+
+ if ($this->cacheEnabled) {
+ $cache->set($cacheKey, $urn, $this->cacheTimeout);
+ $this->log('linkedin', 'debug', 'Cached LinkedIn author URN');
+ }
+
+ return $urn;
+ }
+
+
+ protected function linkedin(string $text): void
+ {
+ $token = $this->config['linkedin']['token'];
+ $author = $this->getLinkedInAuthorUrn();
+
+ $payload = [
+ 'author' => $author,
+ 'lifecycleState' => 'PUBLISHED',
+ 'specificContent' => [
+ 'com.linkedin.ugc.ShareContent' => [
+ 'shareCommentary' => [
+ 'text' => $text
+ ],
+ 'shareMediaCategory' => 'NONE'
+ ]
+ ],
+ 'visibility' => [
+ 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC'
+ ]
+ ];
+
+ // $this->log('linkedin', 'debug', 'Post JSON: ' . json_encode($payload));
+
+ $json = json_encode($payload);
+ if ($json === false) {
+ $this->log('linkedin', 'error', 'Failed to encode payload: ' . json_last_error_msg());
+ throw new Exception('LinkedIn post failed: JSON encoding error');
+ }
+
+ $ch = curl_init('https://api.linkedin.com/v2/ugcPosts');
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Authorization: Bearer ' . $token,
+ 'Content-Type: application/json',
+ 'X-Restli-Protocol-Version: 2.0.0',
+ 'Content-Length: ' . strlen($json),
+ 'User-Agent: ' . self::USER_AGENT
+ ]);
+
+ $responseBody = curl_exec($ch);
+ $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ $this->log('linkedin', 'debug', 'Response Code: ' . $responseCode);
+ // $this->log('linkedin', 'debug', 'Response Body: ' . $responseBody);
+
+ if ($responseCode !== 201) {
+ throw new Exception('LinkedIn post failed: ' . $responseBody);
+ }
+
+ $this->log('linkedin', 'info', 'Post succeeded.');
+ }
+
+ // --------------------------------------------------------------------------
+ // Logging
+ // Implements custom logging to /site/logs/promote.log
+ private function log(string $platform, string $level, string $message): void
+ {
+ $timestamp = date('Y-m-d H:i:s');
+ $logDir = kirby()->root('logs');
+ $logFile = $logDir . '/promote.log';
+
+ // Ensure log directory exists
+ Dir::make($logDir);
+ $entry = Str::unhtml("[$timestamp][$level][$platform] $message") . PHP_EOL;
+ F::append($logFile, $entry);
+ }
+
+}
diff --git a/site/plugins/promote-button/composer.json b/site/plugins/promote-button/composer.json
new file mode 100644
index 0000000..66db31d
--- /dev/null
+++ b/site/plugins/promote-button/composer.json
@@ -0,0 +1,33 @@
+{
+ "name": "scottboms/promote-button",
+ "description": "Promote Panel Button for Kirby.",
+ "type": "kirby-plugin",
+ "homepage": "https://github.com/scottboms/kirby-promote-button",
+ "authors": [
+ {
+ "name": "Scott Boms",
+ "email": "plugins@scottboms.com",
+ "homepage": "https://scottboms.com"
+ }
+ ],
+ "support": {
+ "docs": "https://github.com/scottboms/kirby-promote-button/blob/main/README.md",
+ "source": "https://github.com/scottboms/kirby-promote-button"
+ },
+ "license": "MIT",
+ "keywords": [
+ "kirby",
+ "kirby5",
+ "kirby-cms",
+ "kirby-plugin",
+ "kirby5-plugin"
+ ],
+ "require": {
+ "php": ">8.1.0 <8.4.0",
+ "getkirby/cms": "^5.0",
+ "getkirby/composer-installer": "^1.1"
+ },
+ "extra": {
+ "installer-name": "promote-button"
+ }
+}
diff --git a/site/plugins/promote-button/index.js b/site/plugins/promote-button/index.js
new file mode 100644
index 0000000..dc3a0fc
--- /dev/null
+++ b/site/plugins/promote-button/index.js
@@ -0,0 +1 @@
+(function(){"use strict";function r(t,n,e,u,f,d,p,m){var o=typeof t=="function"?t.options:t;return n&&(o.render=n,o.staticRenderFns=e,o._compiled=!0),{exports:t,options:o}}const i={name:"KProfilesButton",props:{text:{String,default:"Profiles"},icon:{String,default:"account"},theme:{String,default:"pink-icon"},title:{String,default:""},items:{Array,default:()=>[]}},computed:{options(){return this.items.map(t=>({text:t.text,icon:t.icon,disabled:!!t.disabled,click:()=>this.go(t),target:t.target||null}))},anchor(){const t=this.$refs.btn;return t&&(t.$el||t)}},mounted(){console.log("[ProfilesButton] mounted",this.options)},methods:{toggle(){const t=this.$refs.menu;t&&this.anchor&&t.toggle()},onClose(){},go(t){t.disabled||t!=null&&t.link&&window.open(t.link,t.target||"_blank")}}};var s=function(){var n=this,e=n._self._c;return e("div",{staticClass:"k-profiles-button"},[e("k-button",{ref:"btn",attrs:{dropdown:!0,title:n.title,variant:"filled",icon:n.icon,size:"sm",text:n.text,theme:n.theme},on:{click:function(u){return n.$refs.menu.toggle()}}}),e("k-dropdown-content",{ref:"menu",attrs:{alignX:"end",anchor:n.anchor,options:n.options},on:{close:n.onClose}})],1)},l=[],c=r(i,s,l);const a=c.exports;panel.plugin("scottboms/promote-button",{components:{"k-profiles-button":a}})})();
diff --git a/site/plugins/promote-button/index.php b/site/plugins/promote-button/index.php
new file mode 100644
index 0000000..7c255fa
--- /dev/null
+++ b/site/plugins/promote-button/index.php
@@ -0,0 +1,179 @@
+ __DIR__ . '/classes/PlatformPromoter.php']);
+
+use ScottBoms\Promote\PlatformPromoter;
+use Kirby\Http\Remote;
+use Kirby\Cms\App;
+
+// shamelessly borrowed from distantnative/retour-for-kirby
+if (
+ version_compare(App::version() ?? '0.0.0', '5.0.0', '<') === true ||
+ version_compare(App::version() ?? '0.0.0', '6.0.0', '>=') === true
+) {
+ throw new Exception('Promote Button requires Kirby v4 or v5');
+}
+
+Kirby::plugin('scottboms/promote-button', [
+
+ 'areas' => [
+ 'site' => function () {
+ return [
+ 'buttons' => [
+ 'promote' => function ($page) {
+ return [
+ 'icon' => 'megaphone',
+ 'text' => 'Promouvoir',
+ 'theme' => 'pink-icon',
+ 'title' => 'Share this Page',
+ 'dialog' => 'promote/?page=' . $page->uuid()->toString(),
+ ];
+ },
+
+ 'profiles' => function () {
+ $services = option('scottboms.promote.services', []);
+ $items = [];
+
+ foreach ($services as $service) {
+ if ($service === 'mastodon') {
+ $host = trim((string) option('scottboms.promote.mastodon.url'));
+ $user = trim((string) option('scottboms.promote.mastodon.username'));
+ if ($host && $user) {
+ $items[] = [
+ 'text' => 'Mastodon',
+ 'icon' => 'mastodon', // keep generic to rule out custom icon issues
+ 'link' => 'https://' . $host . '/@' . $user,
+ 'target' => '_blank',
+ ];
+ }
+ } elseif ($service === 'bluesky') {
+ $handle = trim((string) option('scottboms.promote.bluesky.handle'));
+ if ($handle) {
+ $items[] = [
+ 'text' => 'Bluesky',
+ 'icon' => 'bluesky',
+ 'link' => 'https://bsky.app/profile/' . $handle,
+ 'target' => '_blank',
+ ];
+ }
+ } elseif ($service === 'linkedin') {
+ $username = trim((string) option('scottboms.promote.linkedin.username'));
+ if ($username) {
+ $items[] = [
+ 'text' => 'LinkedIn',
+ 'icon' => 'linkedin',
+ 'link' => 'https://linkedin.com/in/' . $username,
+ 'target' => '_blank',
+ ];
+ }
+ } elseif ($service === 'instagram') {
+ $username = trim((string) option('scottboms.promote.instagram.username'));
+ if ($username) {
+ $items[] = [
+ 'text' => 'Instagram',
+ 'icon' => 'instagram',
+ 'link' => 'https://instagram.com/' . $username,
+ 'target' => '_blank',
+ ];
+ }
+ }
+ }
+
+ if (!$items) {
+ $items[] = [
+ 'text' => 'No profiles configured',
+ 'icon' => 'alert',
+ 'disabled' => true,
+ ];
+ }
+
+ return [
+ 'component' => 'k-profiles-button',
+ 'props' => [
+ 'text' => 'Profiles',
+ 'icon' => 'account',
+ 'theme' => 'pink-icon',
+ 'title' => 'Profiles',
+ 'items' => $items,
+ ],
+ ];
+ },
+
+ ],
+
+ 'dialogs' => [
+ 'promote' => [
+ 'load' => function () {
+ $page = page(get('page'));
+ return [
+ 'component' => 'k-form-dialog',
+ 'props' => [
+ 'size' => 'large',
+ 'fields' => [
+ 'text' => [
+ 'label' => 'Text',
+ 'type' => 'textarea',
+ 'buttons' => false,
+ 'size' => 'small',
+ 'required' => true,
+ ],
+ 'platforms' => [
+ 'label' => 'Post To',
+ 'type' => 'checkboxes',
+ 'columns' => max(1, min(3, count(option('scottboms.promote.services', [])))),
+ 'options' => array_map(function ($service) {
+ $labelMap = [
+ 'mastodon' => 'Mastodon',
+ 'bluesky' => 'Bluesky',
+ 'linkedin' => 'LinkedIn',
+ 'instagram' => 'Instagram',
+ ];
+ return [
+ 'value' => $service,
+ 'text' => $labelMap[$service] ?? ucfirst($service)
+ ];
+ }, option('scottboms.promote.services', []))
+ ],
+ ],
+ 'value' => [
+ 'text' => 'Just posted ' . $page->title()->value() . ' ' . (
+ option('scottboms.promote.host_url')
+ ? rtrim(option('scottboms.promote.host_url'), '/') . '/' . $page->uri()
+ : $page->url()
+ ),
+ 'platforms' => ['mastodon','bluesky','linkedin'],
+ ],
+ 'submitButton' => [
+ 'icon' => 'megaphone',
+ 'text' => 'Poster',
+ 'theme' => 'pink'
+ ],
+ ],
+ ];
+ },
+ 'submit' => function () {
+ $text = get('text');
+ $enabled = option('scottboms.promote.services', []);
+ $platforms = array_intersect(get('platforms', []), $enabled);
+ if (!is_array($platforms)) return false;
+
+ $promoter = new ScottBoms\Promote\PlatformPromoter();
+ foreach ($platforms as $platform) {
+ $promoter->post($platform, $text);
+ }
+ return true;
+ },
+ ],
+ ],
+ ];
+ },
+ ],
+
+ 'info' => [
+ 'homepage' => 'https://github.com/scottboms/kirby-promote-button',
+ 'version' => '1.1.1',
+ 'license' => 'MIT',
+ 'authors' => [[ 'name' => 'Scott Boms' ]],
+ ],
+
+]);
diff --git a/site/plugins/promote-button/package.json b/site/plugins/promote-button/package.json
new file mode 100644
index 0000000..b31e249
--- /dev/null
+++ b/site/plugins/promote-button/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "promote-button",
+ "description": "Promote Button",
+ "author": "Scott Boms ",
+ "version": "1.1.1",
+ "type": "kirby-plugin",
+ "license": "MIT",
+ "scripts": {
+ "dev": "kirbyup serve src/index.js",
+ "build": "kirbyup src/index.js"
+ },
+ "devDependencies": {
+ "kirbyup": "^3.1.5"
+ }
+}
diff --git a/site/plugins/promote-button/src/assets/kirby-promote-button.png b/site/plugins/promote-button/src/assets/kirby-promote-button.png
new file mode 100644
index 0000000..197222f
Binary files /dev/null and b/site/plugins/promote-button/src/assets/kirby-promote-button.png differ
diff --git a/site/plugins/promote-button/src/components/ProfilesButton.vue b/site/plugins/promote-button/src/components/ProfilesButton.vue
new file mode 100644
index 0000000..7d8af53
--- /dev/null
+++ b/site/plugins/promote-button/src/components/ProfilesButton.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
diff --git a/site/plugins/promote-button/src/index.js b/site/plugins/promote-button/src/index.js
new file mode 100644
index 0000000..68eda97
--- /dev/null
+++ b/site/plugins/promote-button/src/index.js
@@ -0,0 +1,7 @@
+import ProfilesButton from "./components/ProfilesButton.vue";
+
+panel.plugin("scottboms/promote-button", {
+ components: {
+ "k-profiles-button": ProfilesButton,
+ }
+});
diff --git a/site/templates/admissions.php b/site/templates/admissions.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/agenda.php b/site/templates/agenda.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/alumni.php b/site/templates/alumni.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/archive-year.php b/site/templates/archive-year.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/archives.php b/site/templates/archives.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/career.php b/site/templates/career.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/continuing-education.php b/site/templates/continuing-education.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/cursus-art.php b/site/templates/cursus-art.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/cursus-design.php b/site/templates/cursus-design.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/higher-education.php b/site/templates/higher-education.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/home.php b/site/templates/home.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/international.php b/site/templates/international.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/news-item.php b/site/templates/news-item.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/news.php b/site/templates/news.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/presentation.php b/site/templates/presentation.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/programs.php b/site/templates/programs.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/public-courses.php b/site/templates/public-courses.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/registration.php b/site/templates/registration.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/resources.php b/site/templates/resources.php
new file mode 100644
index 0000000..e69de29
diff --git a/site/templates/starred-events.php b/site/templates/starred-events.php
new file mode 100644
index 0000000..e69de29