add seo plugin
This commit is contained in:
parent
23dbb65314
commit
e69dc23c91
1909 changed files with 1111579 additions and 10 deletions
|
|
@ -24,7 +24,8 @@
|
|||
"require": {
|
||||
"php": "~8.1.0 || ~8.2.0 || ~8.3.0",
|
||||
"getkirby/cms": "^4.4",
|
||||
"getbrevo/brevo-php": "2.0.2"
|
||||
"getbrevo/brevo-php": "2.0.2",
|
||||
"tobimori/kirby-seo": "^1.1"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
|
|
|||
132
composer.lock
generated
132
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6b30c0184b5141a81ecab284940f1bf6",
|
||||
"content-hash": "78486aa123389f97d5a3ba831395ede6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "christian-riesen/base32",
|
||||
|
|
@ -1320,6 +1320,79 @@
|
|||
},
|
||||
"time": "2019-03-08T08:55:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/schema-org",
|
||||
"version": "3.23.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/schema-org.git",
|
||||
"reference": "c95abf7da59f16570aec18ca11d861255e645c39"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/schema-org/zipball/c95abf7da59f16570aec18ca11d861255e645c39",
|
||||
"reference": "c95abf7da59f16570aec18ca11d861255e645c39",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.6",
|
||||
"graham-campbell/analyzer": "^3.0.5",
|
||||
"illuminate/collections": "^8.62.0",
|
||||
"league/flysystem": "^2.3.0 || ^3.0",
|
||||
"pestphp/pest": "^1.21",
|
||||
"symfony/console": "^5.3.7 || 6.0",
|
||||
"twig/twig": "^3.3.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\SchemaOrg\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian De Deyne",
|
||||
"email": "sebastian@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Tom Witkowski",
|
||||
"email": "dev.gummibeer@gmail.com",
|
||||
"homepage": "https://gummibeer.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A fluent builder Schema.org types and ld+json generator",
|
||||
"homepage": "https://github.com/spatie/schema-org",
|
||||
"keywords": [
|
||||
"schema-org",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/schema-org/issues",
|
||||
"source": "https://github.com/spatie/schema-org/tree/3.23.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-01-11T14:58:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.5.0",
|
||||
|
|
@ -1782,6 +1855,63 @@
|
|||
}
|
||||
],
|
||||
"time": "2024-08-12T09:55:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tobimori/kirby-seo",
|
||||
"version": "1.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tobimori/kirby-seo.git",
|
||||
"reference": "a06eb676f699797fdd04a515149559ffd4746be7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tobimori/kirby-seo/zipball/a06eb676f699797fdd04a515149559ffd4746be7",
|
||||
"reference": "a06eb676f699797fdd04a515149559ffd4746be7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"getkirby/composer-installer": "^1.2.1",
|
||||
"php": ">=8.1.0",
|
||||
"spatie/schema-org": "^3.14"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.48",
|
||||
"getkirby/cli": "^1.2",
|
||||
"getkirby/cms": "^4.0"
|
||||
},
|
||||
"type": "kirby-plugin",
|
||||
"extra": {
|
||||
"kirby-cms-path": false
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"tobimori\\Seo\\": "classes"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Tobias Möritz",
|
||||
"email": "tobias@moeritz.io"
|
||||
}
|
||||
],
|
||||
"description": "The ultimate Kirby SEO toolkit",
|
||||
"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/1.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/tobimori",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-04-10T09:49:19+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
|
|
|||
|
|
@ -223,4 +223,6 @@ tabs:
|
|||
width: 3/4
|
||||
when:
|
||||
isMapadoEvent: true
|
||||
|
||||
seo: seo/page
|
||||
files: tabs/files
|
||||
|
|
|
|||
|
|
@ -28,3 +28,4 @@ tabs:
|
|||
type: pages
|
||||
template: season
|
||||
info: "{{ page.children.count }} événements"
|
||||
seo: seo/page
|
||||
|
|
|
|||
|
|
@ -23,3 +23,4 @@ tabs:
|
|||
ratio: 4/3
|
||||
query: page.gallery.toFiles.first
|
||||
search: true
|
||||
seo: seo/page
|
||||
|
|
|
|||
|
|
@ -37,4 +37,5 @@ tabs:
|
|||
help: Laisser vide une hauteur illimitée.
|
||||
width: 1/2
|
||||
|
||||
seo: seo/page
|
||||
filesTab: tabs/files
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@ tabs:
|
|||
label: Sections
|
||||
type: pages
|
||||
template: section
|
||||
seo: seo/page
|
||||
|
|
|
|||
|
|
@ -139,3 +139,4 @@ tabs:
|
|||
facebook:
|
||||
type: url
|
||||
icon: facebook
|
||||
seo: seo/site
|
||||
|
|
|
|||
|
|
@ -48,4 +48,6 @@ return [
|
|||
],
|
||||
'locale' => 'fr_FR.UTF-8',
|
||||
'ticketingUrl' => 'https://cdn-besancon.mapado.com/',
|
||||
'tobimori.seo.canonicalBase' => 'https://ntbesancon.fr',
|
||||
'tobimori.seo.lang' => 'fr_FR',
|
||||
];
|
||||
4
site/plugins/kirby-seo/.husky/pre-commit
Executable file
4
site/plugins/kirby-seo/.husky/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm run build && git add index.css index.js
|
||||
1
site/plugins/kirby-seo/.nvmrc
Normal file
1
site/plugins/kirby-seo/.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
20
|
||||
59
site/plugins/kirby-seo/.php-cs-fixer.dist.php
Normal file
59
site/plugins/kirby-seo/.php-cs-fixer.dist.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->exclude('vendor')
|
||||
->in(__DIR__);
|
||||
|
||||
$config = new PhpCsFixer\Config();
|
||||
return $config
|
||||
->setRules([
|
||||
'@PSR12' => true,
|
||||
'align_multiline_comment' => ['comment_type' => 'phpdocs_like'],
|
||||
'array_indentation' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'cast_spaces' => ['space' => 'none'],
|
||||
// 'class_keyword_remove' => true, // replaces static::class with 'static' (won't work)
|
||||
'combine_consecutive_issets' => true,
|
||||
'combine_consecutive_unsets' => true,
|
||||
'combine_nested_dirname' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'declare_equal_normalize' => ['space' => 'single'],
|
||||
'dir_constant' => true,
|
||||
'function_typehint_space' => true,
|
||||
'include' => true,
|
||||
'logical_operators' => true,
|
||||
'lowercase_cast' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
'magic_constant_casing' => true,
|
||||
'magic_method_casing' => true,
|
||||
'method_chaining_indentation' => true,
|
||||
'modernize_types_casting' => true,
|
||||
'multiline_comment_opening_closing' => true,
|
||||
'native_function_casing' => true,
|
||||
'native_function_type_declaration_casing' => true,
|
||||
'new_with_braces' => true,
|
||||
'no_blank_lines_after_class_opening' => true,
|
||||
'no_blank_lines_after_phpdoc' => true,
|
||||
'no_empty_comment' => true,
|
||||
'no_empty_phpdoc' => true,
|
||||
'no_empty_statement' => true,
|
||||
'no_leading_namespace_whitespace' => true,
|
||||
'no_mixed_echo_print' => ['use' => 'echo'],
|
||||
'no_unneeded_control_parentheses' => true,
|
||||
'no_unused_imports' => true,
|
||||
'no_useless_return' => true,
|
||||
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||
// 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], // adds params in the wrong order
|
||||
'phpdoc_align' => ['align' => 'left'],
|
||||
'phpdoc_indent' => true,
|
||||
'phpdoc_scalar' => true,
|
||||
'phpdoc_trim' => true,
|
||||
'short_scalar_cast' => true,
|
||||
'single_line_comment_style' => true,
|
||||
'single_quote' => true,
|
||||
'ternary_to_null_coalescing' => true,
|
||||
'whitespace_after_comma_in_array' => true
|
||||
])
|
||||
->setRiskyAllowed(true)
|
||||
->setIndent("\t")
|
||||
->setFinder($finder);
|
||||
21
site/plugins/kirby-seo/LICENSE
Normal file
21
site/plugins/kirby-seo/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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.
|
||||
30
site/plugins/kirby-seo/README.md
Normal file
30
site/plugins/kirby-seo/README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||

|
||||
|
||||
<h1 align="center">Kirby SEO</h1>
|
||||
<p align="center">All-in-one toolkit that makes implementing SEO & Meta best practices a breeze</p>
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- 🔎 All-in-one SEO and meta solution
|
||||
- 🪜 The Meta Cascade: Intelligently merge metadata from multiple sources
|
||||
- 🎛 Completely configurable: Disable features you don't need
|
||||
- 💻 Simple Panel UI with previews for Google, Twitter, Facebook & Co.
|
||||
- 📮 [Schema.org (JSON-LD)](https://schema.org/) support with fluent classes
|
||||
- 🤖 Automatic Robots rule generation, based on page status
|
||||
- 📝 Sitemap generation with multi-lang support
|
||||
|
||||
## Get started
|
||||
|
||||
[Read the documentation](https://plugins.andkindness.com/seo/docs/get-started/feature-overview) to get started with Kirby SEO.
|
||||
|
||||
## Support the project
|
||||
|
||||
> [!NOTE]
|
||||
> This plugin is provided free of charge & published under the permissive MIT License. If you use it in a commercial project, please consider sponsoring me on GitHub to support further development and continued maintenance of Kirby SEO.
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](./../LICENSE)
|
||||
Copyright © 2023-2024 Tobias Möritz
|
||||
33
site/plugins/kirby-seo/blueprints/fields/meta-group.yml
Normal file
33
site/plugins/kirby-seo/blueprints/fields/meta-group.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
type: group
|
||||
fields:
|
||||
metaHeadline:
|
||||
label: meta-headline
|
||||
type: headline
|
||||
numbered: false
|
||||
metaTitle:
|
||||
label: title-overwrite
|
||||
type: text
|
||||
placeholder: '{{ page.title }}'
|
||||
metaTemplate:
|
||||
label: meta-title-template
|
||||
type: text
|
||||
help: meta-title-template-help
|
||||
width: 2/3
|
||||
placeholder: '{{ page.metadata.metaTemplate }}'
|
||||
useTitleTemplate:
|
||||
label: use-title-template
|
||||
type: toggle
|
||||
help: use-title-template-help
|
||||
width: 1/3
|
||||
default: true
|
||||
text:
|
||||
- "{{ t('use-title-template-no') }}"
|
||||
- "{{ t('use-title-template-yes') }}"
|
||||
metaDescription:
|
||||
label: meta-description
|
||||
type: textarea
|
||||
help: meta-description-help
|
||||
placeholder: '{{ page.metadata.metaDescription }}'
|
||||
buttons: false
|
||||
seoLine1:
|
||||
type: line
|
||||
48
site/plugins/kirby-seo/blueprints/fields/og-group.yml
Normal file
48
site/plugins/kirby-seo/blueprints/fields/og-group.yml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
type: group
|
||||
fields:
|
||||
ogHeadline:
|
||||
label: og-headline
|
||||
type: headline
|
||||
numbered: false
|
||||
help: global-og-headline-help
|
||||
ogTemplate:
|
||||
label: og-title-template
|
||||
type: text
|
||||
width: 2/3
|
||||
help: meta-title-template-help
|
||||
placeholder: '{{ page.metadata.ogTemplate }}'
|
||||
useOgTemplate:
|
||||
label: use-title-template
|
||||
type: toggle
|
||||
help: use-title-template-help
|
||||
width: 1/3
|
||||
default: true
|
||||
text:
|
||||
- "{{ t('use-title-template-no') }}"
|
||||
- "{{ t('use-title-template-yes') }}"
|
||||
ogDescription:
|
||||
label: og-description
|
||||
type: textarea
|
||||
buttons: false
|
||||
placeholder: '{{ page.metadata.ogDescription }}'
|
||||
ogImage:
|
||||
label: og-image
|
||||
extends: seo/fields/og-image
|
||||
empty: og-image-empty
|
||||
twitterCardType:
|
||||
label: twitter-card-type
|
||||
width: 1/2
|
||||
placeholder: "{{ t('default-select') }} {{ t(site.twitterCardType) }}"
|
||||
type: select
|
||||
options:
|
||||
summary: "{{ t('summary') }}"
|
||||
summary_large_image: "{{ t('summary_large_image') }}"
|
||||
help: twitter-card-type-help
|
||||
twitterAuthor:
|
||||
label: twitter-author
|
||||
width: 1/2
|
||||
type: text
|
||||
before: '@'
|
||||
placeholder: '{{ page.metadata.twitterCreator }}'
|
||||
seoLine2:
|
||||
type: line
|
||||
31
site/plugins/kirby-seo/blueprints/fields/og-image.php
Normal file
31
site/plugins/kirby-seo/blueprints/fields/og-image.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return function (App $kirby) {
|
||||
$blueprint = [
|
||||
'type' => 'files',
|
||||
'multiple' => false,
|
||||
'uploads' => [],
|
||||
'query' => Str::contains($kirby->path(), '/site') && !Str::contains($kirby->path(), 'pages') ? 'site.images' : 'page.images' // small hack to get context for field using api path
|
||||
];
|
||||
|
||||
if ($parent = option('tobimori.seo.files.parent')) {
|
||||
$blueprint['uploads'] = [
|
||||
'parent' => $parent
|
||||
];
|
||||
$blueprint['query'] = "{$parent}.images";
|
||||
}
|
||||
|
||||
if ($template = option('tobimori.seo.files.template')) {
|
||||
$blueprint['uploads'] = [
|
||||
...$blueprint['uploads'],
|
||||
'template' => $template
|
||||
];
|
||||
|
||||
$blueprint['query'] = "{$blueprint['query']}.filterBy('template', '{$template}')";
|
||||
}
|
||||
|
||||
return $blueprint;
|
||||
};
|
||||
55
site/plugins/kirby-seo/blueprints/fields/robots.php
Normal file
55
site/plugins/kirby-seo/blueprints/fields/robots.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use tobimori\Seo\Meta;
|
||||
|
||||
return function (App $kirby) {
|
||||
if (!$kirby->option('tobimori.seo.robots.active') || !$kirby->option('tobimori.seo.robots.pageSettings')) {
|
||||
return [
|
||||
'type' => 'hidden'
|
||||
];
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'robotsHeadline' => [
|
||||
'label' => 'robots',
|
||||
'type' => 'headline',
|
||||
'numbered' => false,
|
||||
]
|
||||
];
|
||||
|
||||
$page = Meta::currentPage();
|
||||
foreach ($kirby->option('tobimori.seo.robots.types') as $robots) {
|
||||
$upper = Str::ucfirst($robots);
|
||||
|
||||
$fields["robots{$upper}"] = [
|
||||
'label' => "robots-{$robots}",
|
||||
'type' => 'toggles',
|
||||
'help' => "robots-{$robots}-help",
|
||||
'width' => '1/2',
|
||||
'default' => 'default',
|
||||
'reset' => false,
|
||||
'options' => [
|
||||
'default' => $page ?
|
||||
A::join([
|
||||
t('default-select'),
|
||||
$page->metadata()->get("robots{$upper}", ['fields'])->toBool() ? t('yes') : t('no')
|
||||
], ' ')
|
||||
: t('default-select'),
|
||||
'true' => t('yes'),
|
||||
'false' => t('no'),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$fields['seoLine3'] = [
|
||||
'type' => 'line'
|
||||
];
|
||||
|
||||
return [
|
||||
'type' => 'group',
|
||||
'fields' => $fields,
|
||||
];
|
||||
};
|
||||
49
site/plugins/kirby-seo/blueprints/fields/site-robots.php
Normal file
49
site/plugins/kirby-seo/blueprints/fields/site-robots.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
|
||||
return function (App $kirby) {
|
||||
if (!$kirby->option('tobimori.seo.robots.active') || !$kirby->option('tobimori.seo.robots.pageSettings')) {
|
||||
return [
|
||||
'type' => 'hidden'
|
||||
];
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'robotsHeadline' => [
|
||||
'label' => 'robots',
|
||||
'type' => 'headline',
|
||||
'numbered' => false,
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($kirby->option('tobimori.seo.robots.types') as $robots) {
|
||||
$index = $kirby->option('tobimori.seo.robots.index');
|
||||
if (is_callable($index)) {
|
||||
$index = $index();
|
||||
}
|
||||
|
||||
$fields["robots{$robots}"] = [
|
||||
'label' => "robots-{$robots}",
|
||||
'type' => 'toggles',
|
||||
'help' => "robots-{$robots}-help",
|
||||
'width' => '1/2',
|
||||
'default' => 'default',
|
||||
'reset' => false,
|
||||
'options' => [
|
||||
'default' => t('default-select') . ' ' . ($index ? t('yes') : t('no')),
|
||||
'true' => t('yes'),
|
||||
'false' => t('no'),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$fields['seoLine3'] = [
|
||||
'type' => 'line'
|
||||
];
|
||||
|
||||
return [
|
||||
'type' => 'group',
|
||||
'fields' => $fields,
|
||||
];
|
||||
};
|
||||
30
site/plugins/kirby-seo/blueprints/fields/social-media.php
Normal file
30
site/plugins/kirby-seo/blueprints/fields/social-media.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Social Media Accounts field
|
||||
* Allows social media account list to be filled by config options
|
||||
*/
|
||||
|
||||
use Kirby\Cms\App;
|
||||
|
||||
return function (App $kirby) {
|
||||
$fields = [];
|
||||
|
||||
foreach ($kirby->option('tobimori.seo.socialMedia') as $key => $value) {
|
||||
if ($value) {
|
||||
$fields[$key] = [
|
||||
'label' => ucfirst($key),
|
||||
'type' => 'url',
|
||||
'icon' => strtolower($key),
|
||||
'placeholder' => $value
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => 'social-media-accounts',
|
||||
'type' => 'object',
|
||||
'help' => 'social-media-accounts-help',
|
||||
'fields' => $fields
|
||||
];
|
||||
};
|
||||
29
site/plugins/kirby-seo/blueprints/page.yml
Normal file
29
site/plugins/kirby-seo/blueprints/page.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
label: metadata-site
|
||||
icon: search
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 7/12
|
||||
fields:
|
||||
metaGroup: seo/fields/meta-group
|
||||
ogGroup: seo/fields/og-group
|
||||
robots: seo/fields/robots
|
||||
metaInherit:
|
||||
label: inherit-settings
|
||||
type: multiselect
|
||||
help: inherit-settings-help
|
||||
options:
|
||||
metaTemplate: "{{ t('meta-title-template') }}"
|
||||
metaDescription: "{{ t('meta-description') }}"
|
||||
ogTemplate: "{{ t('og-title-template') }}"
|
||||
ogDescription: "{{ t('og-description') }}"
|
||||
ogImage: "{{ t('og-image') }}"
|
||||
twitterCardType: "{{ t('twitter-card-type') }}"
|
||||
twitterAuthor: "{{ t('twitter-author') }}"
|
||||
robots: '{{ t("robots") }}'
|
||||
sidebar:
|
||||
width: 5/12
|
||||
sticky: true
|
||||
sections:
|
||||
seoPreview:
|
||||
type: seo-preview
|
||||
70
site/plugins/kirby-seo/blueprints/site.yml
Normal file
70
site/plugins/kirby-seo/blueprints/site.yml
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
label: metadata-site
|
||||
icon: search
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 7/12
|
||||
fields:
|
||||
metaHeadline:
|
||||
label: global-meta-headline
|
||||
type: headline
|
||||
numbered: false
|
||||
help: global-meta-headline-help
|
||||
metaTemplate:
|
||||
label: meta-title-template
|
||||
type: text
|
||||
help: meta-title-template-help
|
||||
metaDescription:
|
||||
label: meta-description
|
||||
type: textarea
|
||||
help: meta-description-help
|
||||
buttons: false
|
||||
seoLine1:
|
||||
type: line
|
||||
ogHeadline:
|
||||
label: global-og-headline
|
||||
type: headline
|
||||
numbered: false
|
||||
help: global-og-headline-help
|
||||
ogTemplate:
|
||||
label: og-title-template
|
||||
type: text
|
||||
default: '{{ title }}'
|
||||
help: meta-title-template-help
|
||||
placeholder: '{{ site.metaTemplate }}'
|
||||
ogDescription:
|
||||
label: og-description
|
||||
type: textarea
|
||||
buttons: false
|
||||
placeholder: '{{ site.metaDescription }}'
|
||||
ogSiteName:
|
||||
label: og-site-name
|
||||
type: text
|
||||
default: '{{ site.title }}'
|
||||
placeholder: '{{ site.title }}'
|
||||
width: 1/2
|
||||
ogImage:
|
||||
label: og-image
|
||||
extends: seo/fields/og-image
|
||||
empty: og-image-empty
|
||||
width: 1/2
|
||||
twitterCardType:
|
||||
label: twitter-card-type
|
||||
width: 1/2
|
||||
type: select
|
||||
default: summary
|
||||
required: true
|
||||
options:
|
||||
summary: "{{ t('summary') }}"
|
||||
summary_large_image: "{{ t('summary_large_image') }}"
|
||||
help: twitter-card-type-help
|
||||
seoLine2:
|
||||
type: line
|
||||
robots: seo/fields/site-robots
|
||||
socialMediaAccounts: seo/fields/social-media
|
||||
sidebar:
|
||||
width: 5/12
|
||||
sticky: true
|
||||
sections:
|
||||
seoPreview:
|
||||
type: seo-preview
|
||||
670
site/plugins/kirby-seo/classes/Meta.php
Normal file
670
site/plugins/kirby-seo/classes/Meta.php
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Content\Field;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Meta class is responsible for handling the meta data & cascading
|
||||
*/
|
||||
class Meta
|
||||
{
|
||||
/**
|
||||
* These values will be handled as 'field is empty'
|
||||
*/
|
||||
public const DEFAULT_VALUES = ['[]', 'default'];
|
||||
|
||||
protected Page $page;
|
||||
protected ?string $lang;
|
||||
protected array $consumed = [];
|
||||
protected array $metaDefaults = [];
|
||||
protected array $metaArray = [];
|
||||
|
||||
/**
|
||||
* Creates a new Meta instance
|
||||
*/
|
||||
public function __construct(Page $page, ?string $lang = null)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->lang = $lang;
|
||||
|
||||
if (method_exists($this->page, 'metaDefaults')) {
|
||||
$this->metaDefaults = $this->page->metaDefaults($this->lang);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the meta array which maps meta tags to their fieldnames
|
||||
*/
|
||||
protected function metaArray(): array
|
||||
{
|
||||
if ($this->metaArray) {
|
||||
return $this->metaArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* We have to specify field names and resolve them later, so we can use this
|
||||
* function to resolve meta tags from field names in the programmatic function
|
||||
*/
|
||||
$meta =
|
||||
[
|
||||
'title' => 'metaTitle',
|
||||
'description' => 'metaDescription',
|
||||
'date' => fn () => $this->page->modified($this->dateFormat()),
|
||||
'og:title' => 'ogTitle',
|
||||
'og:description' => 'ogDescription',
|
||||
'og:site_name' => 'ogSiteName',
|
||||
'og:image' => 'ogImage',
|
||||
'og:image:width' => fn () => $this->ogImage() ? $this->get('ogImage')->toFile()?->width() : null,
|
||||
'og:image:height' => fn () => $this->ogImage() ? $this->get('ogImage')->toFile()?->height() : null,
|
||||
'og:image:alt' => fn () => $this->get('ogImage')->toFile()?->alt(),
|
||||
'og:type' => 'ogType',
|
||||
];
|
||||
|
||||
|
||||
// Robots
|
||||
if ($robotsActive = option('tobimori.seo.robots.active')) {
|
||||
$meta['robots'] = fn () => $this->robots();
|
||||
}
|
||||
|
||||
// only add canonical and alternate tags if the page is indexable
|
||||
// we have to resolve this lazily (using a callable) to avoid an infinite loop
|
||||
$allowsIndexFn = fn () => !$robotsActive || !Str::contains($this->robots(), 'noindex');
|
||||
|
||||
// canonical
|
||||
$canonicalFn = fn () => $allowsIndexFn() ? $this->canonicalUrl() : null;
|
||||
$meta['canonical'] = $canonicalFn;
|
||||
$meta['og:url'] = $canonicalFn;
|
||||
|
||||
// Multi-lang alternate tags
|
||||
if (kirby()->languages()->count() > 1 && kirby()->language() !== null) {
|
||||
foreach (kirby()->languages() as $lang) {
|
||||
// only add alternate tags if the page is indexable
|
||||
$meta['alternate'][] = fn () => $allowsIndexFn() ? [
|
||||
'hreflang' => $lang->code(),
|
||||
'href' => $this->page->url($lang->code()),
|
||||
] : null;
|
||||
|
||||
if ($lang !== kirby()->language()) {
|
||||
$meta['og:locale:alternate'][] = fn () => $lang->code();
|
||||
}
|
||||
}
|
||||
|
||||
// only add alternate tags if the page is indexable
|
||||
$meta['alternate'][] = fn () => $allowsIndexFn() ? [
|
||||
'hreflang' => 'x-default',
|
||||
'href' => $this->page->url(kirby()->language()->code()),
|
||||
] : null;
|
||||
$meta['og:locale'] = fn () => kirby()->language()->locale(LC_ALL);
|
||||
} else {
|
||||
$meta['og:locale'] = fn () => $this->locale(LC_ALL);
|
||||
}
|
||||
|
||||
// Twitter tags "opt-in" - TODO: wip
|
||||
if (option('tobimori.seo.twitter', true)) {
|
||||
$meta = array_merge($meta, [
|
||||
'twitter:card' => 'twitterCardType',
|
||||
'twitter:title' => 'ogTitle',
|
||||
'twitter:description' => 'ogDescription',
|
||||
'twitter:image' => 'ogImage',
|
||||
'twitter:site' => 'twitterSite',
|
||||
'twitter:creator' => 'twitterCreator',
|
||||
]);
|
||||
}
|
||||
|
||||
// This array will be normalized for use in the snippet in $this->snippetData()
|
||||
return $this->metaArray = $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* This array defines what HTML tag the corresponding meta tags are using including the attributes,
|
||||
* so everything is a bit more elegant when defining programmatic content (supports regex)
|
||||
*/
|
||||
public const TAG_TYPE_MAP = [
|
||||
[
|
||||
'tag' => 'title',
|
||||
'tags' => [
|
||||
'title'
|
||||
]
|
||||
],
|
||||
[
|
||||
'tag' => 'link',
|
||||
'attributes' => [
|
||||
'name' => 'rel',
|
||||
'content' => 'href',
|
||||
],
|
||||
'tags' => [
|
||||
'canonical',
|
||||
'alternate',
|
||||
]
|
||||
],
|
||||
[
|
||||
'tag' => 'meta',
|
||||
'attributes' => [
|
||||
'name' => 'property',
|
||||
'content' => 'content',
|
||||
],
|
||||
'tags' => [
|
||||
'/og:.+/'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Normalize the meta array and remaining meta defaults to be used in the snippet,
|
||||
* also resolve the content, if necessary
|
||||
*/
|
||||
public function snippetData(array $raw = null): array
|
||||
{
|
||||
$mergeWithDefaults = !isset($raw);
|
||||
$raw ??= $this->metaArray();
|
||||
$tags = [];
|
||||
|
||||
foreach ($raw as $name => $value) {
|
||||
// if the key is numeric, it is already normalized to the correct array syntax
|
||||
if (is_numeric($name)) {
|
||||
// but we still check if the array is valid
|
||||
if (!is_array($value) || count(array_intersect(['tag', 'content', 'attributes'], array_keys($value))) !== count($value)) {
|
||||
throw new InvalidArgumentException("[kirby-seo] Invalid array structure found in programmatic content for page {$this->slug()}. Please check your metaDefaults method for template {$this->template()->name()}.");
|
||||
}
|
||||
|
||||
$tags[] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// allow overrides from metaDefaults for keys that are a callable or array by default
|
||||
// (all fields from meta array that are not part of the regular cascade)
|
||||
if ((is_callable($value) || is_array($value)) && $mergeWithDefaults && array_key_exists($name, $this->metaDefaults)) {
|
||||
$this->consumed[] = $name;
|
||||
$value = $this->metaDefaults[$name];
|
||||
}
|
||||
|
||||
// if the value is a string, we know it's a field name
|
||||
if (is_string($value)) {
|
||||
$value = $this->$value($name);
|
||||
}
|
||||
|
||||
// if the value is a callable, we resolve it
|
||||
if (is_callable($value)) {
|
||||
$value = $value($this->page);
|
||||
}
|
||||
|
||||
// if the value is empty, we don't want to output it
|
||||
if ((is_a($value, 'Kirby\Content\Field') && $value->isEmpty()) || !$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// resolve the tag type from the meta array
|
||||
// so we can use the correct attributes to normalize it
|
||||
$tag = $this->resolveTag($name);
|
||||
|
||||
// if the value is an associative array now, all of them are attributes
|
||||
// and we don't look for what the TAG_TYPE_MAP says
|
||||
// or there should be multiple tags with the same name (non-associative array)
|
||||
if (is_array($value)) {
|
||||
if (!A::isAssociative($value)) {
|
||||
foreach ($value as $val) {
|
||||
$tags = array_merge($tags, $this->snippetData([$name => $val]));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// array is associative, so it's an array of attributes
|
||||
// we resolve the values, if they are callable
|
||||
array_walk($value, function (&$item) {
|
||||
if (is_callable($item)) {
|
||||
$item = $item($this->page);
|
||||
}
|
||||
});
|
||||
|
||||
// add the tag to the array
|
||||
$tags[] = [
|
||||
'tag' => $tag['tag'],
|
||||
'attributes' => $value,
|
||||
'content' => null,
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the value is a string, we use the TAG_TYPE_MAP
|
||||
// to correctly map the attributes
|
||||
$tags[] = [
|
||||
'tag' => $tag['tag'],
|
||||
'attributes' => isset($tag['attributes']) ? [
|
||||
$tag['attributes']['name'] => $name,
|
||||
$tag['attributes']['content'] => $value,
|
||||
] : null,
|
||||
'content' => !isset($tag['attributes']) ? $value : null,
|
||||
];
|
||||
}
|
||||
|
||||
if ($mergeWithDefaults) {
|
||||
// merge the remaining meta defaults
|
||||
$tags = array_merge($tags, $this->snippetData(array_diff_key($this->metaDefaults, array_flip($this->consumed))));
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the tag type from the meta array
|
||||
*/
|
||||
protected function resolveTag(string $tag): array
|
||||
{
|
||||
foreach (self::TAG_TYPE_MAP as $type) {
|
||||
foreach ($type['tags'] as $regexOrString) {
|
||||
// Check if the supplied tag is a regex or a normal tag name
|
||||
if (Str::startsWith($regexOrString, '/') && Str::endsWith($regexOrString, '/') ?
|
||||
Str::match($tag, $regexOrString) : $tag === $regexOrString
|
||||
) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'tag' => 'meta',
|
||||
'attributes' => [
|
||||
'name' => 'name',
|
||||
'content' => 'content',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to get a meta value by calling the method name
|
||||
*/
|
||||
public function __call($name, $args = null): mixed
|
||||
{
|
||||
if (method_exists($this, $name)) {
|
||||
return $this->$name($args);
|
||||
}
|
||||
|
||||
return $this->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta value for a given key
|
||||
*/
|
||||
public function get(string $key, array $exclude = []): Field
|
||||
{
|
||||
$cascade = option('tobimori.seo.cascade');
|
||||
if (count(array_intersect(get_class_methods($this), $cascade)) !== count($cascade)) {
|
||||
throw new InvalidArgumentException('[kirby-seo] Invalid cascade method in config. Please check your options for `tobimori.seo.cascade`.');
|
||||
}
|
||||
|
||||
// Track consumed keys, so we don't output legacy field values
|
||||
$toBeConsumed = $key;
|
||||
if (
|
||||
(array_key_exists($toBeConsumed, $this->metaDefaults)
|
||||
|| array_key_exists($toBeConsumed = $this->findTagForField($toBeConsumed), $this->metaDefaults))
|
||||
&& !in_array($toBeConsumed, $this->consumed)
|
||||
) {
|
||||
$this->consumed[] = $toBeConsumed;
|
||||
}
|
||||
|
||||
foreach (array_diff($cascade, $exclude) as $method) {
|
||||
if ($field = $this->$method($key)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
return new Field($this->page, $key, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta value for a given key from the page's fields
|
||||
*/
|
||||
protected function fields(string $key): Field|null
|
||||
{
|
||||
if (($field = $this->page->content($this->lang)->get($key))) {
|
||||
if (Str::contains($key, 'robots') && !option('tobimori.seo.robots.pageSettings')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($field->isNotEmpty() && !A::has(self::DEFAULT_VALUES, $field->value())) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps Open Graph fields to Meta fields for fallbackFields
|
||||
* cascade method
|
||||
*/
|
||||
public const FALLBACK_MAP = [
|
||||
'ogTitle' => 'metaTitle',
|
||||
'ogDescription' => 'metaDescription',
|
||||
'ogTemplate' => 'metaTemplate',
|
||||
];
|
||||
|
||||
/**
|
||||
* We only allow the following cascade methods for fallbacks,
|
||||
* because we don't want to fallback to the config defaults for
|
||||
* Meta fields, because we most likely already have those set
|
||||
* for the Open Graph fields
|
||||
*/
|
||||
public const FALLBACK_CASCADE = [
|
||||
'fields',
|
||||
'programmatic',
|
||||
'parent',
|
||||
'site'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the meta value for a given key using the fallback fields
|
||||
* defined above (usually Open Graph > Meta Fields)
|
||||
*/
|
||||
protected function fallbackFields(string $key): Field|null
|
||||
{
|
||||
if (array_key_exists($key, self::FALLBACK_MAP)) {
|
||||
$fallback = self::FALLBACK_MAP[$key];
|
||||
$cascade = option('tobimori.seo.cascade');
|
||||
|
||||
foreach (array_intersect($cascade, self::FALLBACK_CASCADE) as $method) {
|
||||
if ($field = $this->$method($fallback)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function findTagForField(string $fieldName): string|null
|
||||
{
|
||||
return array_search($fieldName, $this->metaArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta value for a given key from the page's meta
|
||||
* array, which can be set in the page's model metaDefaults method
|
||||
*/
|
||||
protected function programmatic(string $key): Field|null
|
||||
{
|
||||
if (!$this->metaDefaults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the key (field name) is in the array syntax
|
||||
if (array_key_exists($key, $this->metaDefaults)) {
|
||||
$val = $this->metaDefaults[$key];
|
||||
}
|
||||
|
||||
/* If there is no programmatic value for the key,
|
||||
* try looking it up in the meta array
|
||||
* maybe it is a meta tag and not a field name?
|
||||
*/
|
||||
if (!isset($val) && ($key = $this->findTagForField($key)) && array_key_exists($key, $this->metaDefaults)) {
|
||||
$val = $this->metaDefaults[$key];
|
||||
}
|
||||
|
||||
if (isset($val)) {
|
||||
if (is_callable($val)) {
|
||||
$val = $val($this->page);
|
||||
}
|
||||
|
||||
if (is_array($val)) {
|
||||
$val = $val['content'] ?? $val['href'] ?? null;
|
||||
|
||||
// Last sanity check, if the array syntax doesn't have a supported key
|
||||
if ($val === null) {
|
||||
// Remove the key from the consumed array, so it doesn't get filtered out
|
||||
// (we can assume the entry is a custom meta tag that uses different attributes)
|
||||
$this->consumed = array_filter($this->consumed, fn ($item) => $item !== $key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_a($val, 'Kirby\Content\Field')) {
|
||||
return new Field($this->page, $key, $val->value());
|
||||
}
|
||||
|
||||
return new Field($this->page, $key, $val);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta value for a given key from the page's parent,
|
||||
* if the page is allowed to inherit the value
|
||||
*/
|
||||
protected function parent(string $key): Field|null
|
||||
{
|
||||
if ($this->canInherit($key)) {
|
||||
$parent = $this->page->parent();
|
||||
$parentMeta = new Meta($parent, $this->lang);
|
||||
if ($value = $parentMeta->get($key)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta value for a given key from the
|
||||
* site's meta blueprint & content
|
||||
*/
|
||||
protected function site(string $key): Field|null
|
||||
{
|
||||
if (($site = $this->page->site()->content($this->lang)->get($key)) && ($site->isNotEmpty() && !A::has(self::DEFAULT_VALUES, $site->value))) {
|
||||
return $site;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta value for a given key from the
|
||||
* config.php options
|
||||
*/
|
||||
protected function options(string $key): Field|null
|
||||
{
|
||||
if ($option = option("tobimori.seo.default.{$key}")) {
|
||||
if (is_callable($option)) {
|
||||
$option = $option($this->page);
|
||||
}
|
||||
|
||||
if (is_a($option, 'Kirby\Content\Field')) {
|
||||
return $option;
|
||||
}
|
||||
|
||||
return new Field($this->page, $key, $option);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page can inherit a meta value from its parent
|
||||
*/
|
||||
private function canInherit(string $key): bool
|
||||
{
|
||||
$parent = $this->page->parent();
|
||||
if (!$parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$inherit = $parent->metaInherit()->split();
|
||||
if (Str::contains($key, 'robots') && A::has($inherit, 'robots')) {
|
||||
return true;
|
||||
}
|
||||
return A::has($inherit, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the title template, and returns the correct title
|
||||
*/
|
||||
public function metaTitle()
|
||||
{
|
||||
$title = $this->get('metaTitle');
|
||||
$template = $this->get('metaTemplate');
|
||||
|
||||
$useTemplate = $this->page->useTitleTemplate();
|
||||
$useTemplate = $useTemplate->isEmpty() ? true : $useTemplate->toBool();
|
||||
|
||||
$string = $title->value();
|
||||
if ($useTemplate) {
|
||||
$string = $this->page->toString(
|
||||
$template,
|
||||
['title' => $title]
|
||||
);
|
||||
}
|
||||
|
||||
return new Field(
|
||||
$this->page,
|
||||
'metaTitle',
|
||||
$string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the OG title template, and returns the OG Title
|
||||
*/
|
||||
public function ogTitle()
|
||||
{
|
||||
$title = $this->get('metaTitle');
|
||||
$template = $this->get('ogTemplate');
|
||||
|
||||
$useTemplate = $this->page->useOgTemplate();
|
||||
$useTemplate = $useTemplate->isEmpty() ? true : $useTemplate->toBool();
|
||||
|
||||
$string = $title->value();
|
||||
if ($useTemplate) {
|
||||
$string = $this->page->toString(
|
||||
$template,
|
||||
['title' => $title]
|
||||
);
|
||||
}
|
||||
|
||||
return new Field(
|
||||
$this->page,
|
||||
'ogTitle',
|
||||
$string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the canonical url for the page
|
||||
*/
|
||||
public function canonicalUrl()
|
||||
{
|
||||
return $this->page->site()->canonicalFor($this->page->url());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Twitter username from an account url set in the site options
|
||||
*/
|
||||
public function twitterSite()
|
||||
{
|
||||
$accs = $this->page->site()->socialMediaAccounts()->toObject();
|
||||
$username = '';
|
||||
|
||||
if ($accs->twitter()->isNotEmpty()) {
|
||||
// tries to match all twitter urls, and extract the username
|
||||
$matches = [];
|
||||
preg_match('/^(https?:\/\/)?(www\.)?twitter\.com\/(#!\/)?@?(?<name>[^\/\?]*)$/', $accs->twitter()->value(), $matches);
|
||||
if (isset($matches['name'])) {
|
||||
$username = $matches['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return new Field($this->page, 'twitter', $username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the date format for modified meta tags, based on the registered date handler
|
||||
*/
|
||||
public function dateFormat(): string
|
||||
{
|
||||
if ($custom = option('tobimori.seo.dateFormat')) {
|
||||
if (is_callable($custom)) {
|
||||
return $custom($this->page);
|
||||
}
|
||||
|
||||
return $custom;
|
||||
}
|
||||
|
||||
switch (option('date.handler')) {
|
||||
case 'strftime':
|
||||
return '%Y-%m-%d';
|
||||
case 'intl':
|
||||
return 'yyyy-MM-dd';
|
||||
case 'date':
|
||||
default:
|
||||
return 'Y-m-d';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pages' robots rules as string
|
||||
*/
|
||||
public function robots()
|
||||
{
|
||||
$robots = [];
|
||||
foreach (option('tobimori.seo.robots.types') as $type) {
|
||||
if (!$this->get('robots' . Str::ucfirst($type))->toBool()) {
|
||||
$robots[] = 'no' . Str::lower($type);
|
||||
}
|
||||
}
|
||||
|
||||
if (A::count($robots) === 0) {
|
||||
$robots = ['all'];
|
||||
}
|
||||
|
||||
return A::join($robots, ',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the og:image url
|
||||
*/
|
||||
public function ogImage(): string|null
|
||||
{
|
||||
$field = $this->get('ogImage');
|
||||
|
||||
if ($ogImage = $field->toFile()?->thumb([
|
||||
'width' => 1200,
|
||||
'height' => 630,
|
||||
'crop' => true,
|
||||
])) {
|
||||
return $ogImage->url();
|
||||
}
|
||||
|
||||
if ($field->isNotEmpty()) {
|
||||
return $field->value();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method the get the current page from the URL path,
|
||||
* for use in programmatic blueprints
|
||||
*/
|
||||
public static function currentPage(): Page|null
|
||||
{
|
||||
$path = App::instance()->request()->url()->toString();
|
||||
$matches = Str::match($path, "/pages\/([a-zA-Z0-9-_+]+)\/?/m");
|
||||
$segments = Str::split($matches[1], '+');
|
||||
|
||||
$page = App::instance()->site();
|
||||
foreach ($segments as $segment) {
|
||||
if ($page = $page->findPageOrDraft($segment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
29
site/plugins/kirby-seo/classes/SchemaSingleton.php
Normal file
29
site/plugins/kirby-seo/classes/SchemaSingleton.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo;
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Spatie\SchemaOrg\Schema;
|
||||
|
||||
class SchemaSingleton
|
||||
{
|
||||
private static $instances = [];
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function getInstance(string $type, Page|null $page = null): mixed
|
||||
{
|
||||
if (!isset(self::$instances[$page?->id() ?? 'default'][$type])) {
|
||||
self::$instances[$page?->id() ?? 'default'][$type] = Schema::{$type}();
|
||||
}
|
||||
|
||||
return self::$instances[$page?->id() ?? 'default'][$type];
|
||||
}
|
||||
|
||||
public static function getInstances(Page|null $page = null): array
|
||||
{
|
||||
return self::$instances[$page?->id() ?? 'default'] ?? [];
|
||||
}
|
||||
}
|
||||
82
site/plugins/kirby-seo/classes/Sitemap/Sitemap.php
Normal file
82
site/plugins/kirby-seo/classes/Sitemap/Sitemap.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo\Sitemap;
|
||||
|
||||
use DOMDocument;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Toolkit\Collection;
|
||||
|
||||
class Sitemap extends Collection
|
||||
{
|
||||
public function __construct(protected string $key, array $data = [], bool $caseSensitive = false)
|
||||
{
|
||||
parent::__construct($data, $caseSensitive);
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function loc(): string
|
||||
{
|
||||
return kirby()->site()->canonicalFor('sitemap-' . $this->key . '.xml');
|
||||
}
|
||||
|
||||
public function lastmod(): string
|
||||
{
|
||||
$lastmod = 0;
|
||||
foreach ($this as $url) {
|
||||
$lastmod = max($lastmod, strtotime($url->lastmod()));
|
||||
}
|
||||
|
||||
if ($lastmod > 0) {
|
||||
return date('c', $lastmod);
|
||||
}
|
||||
|
||||
return date('c');
|
||||
}
|
||||
|
||||
public function createUrl(string $loc): SitemapUrl
|
||||
{
|
||||
$url = $this->makeUrl($loc);
|
||||
$this->append($url);
|
||||
return $url;
|
||||
}
|
||||
|
||||
public static function makeUrl(string $url): SitemapUrl
|
||||
{
|
||||
return new SitemapUrl($url);
|
||||
}
|
||||
|
||||
public function toDOMNode(DOMDocument $doc = new DOMDocument('1.0', 'UTF-8'))
|
||||
{
|
||||
$doc->formatOutput = true;
|
||||
|
||||
$root = $doc->createElement('sitemap');
|
||||
foreach (['loc', 'lastmod'] as $key) {
|
||||
$root->appendChild($doc->createElement($key, $this->$key()));
|
||||
}
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
||||
$doc->formatOutput = true;
|
||||
|
||||
$doc->appendChild($doc->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="/sitemap.xsl"'));
|
||||
|
||||
$root = $doc->createElementNS('http://www.sitemaps.org/schemas/sitemap/0.9', 'urlset');
|
||||
$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
|
||||
$root->setAttribute('seo-version', App::plugin('tobimori/seo')->version());
|
||||
|
||||
foreach ($this as $url) {
|
||||
$root->appendChild($url->toDOMNode($doc));
|
||||
}
|
||||
|
||||
$doc->appendChild($root);
|
||||
return $doc->saveXML();
|
||||
}
|
||||
}
|
||||
100
site/plugins/kirby-seo/classes/Sitemap/SitemapIndex.php
Normal file
100
site/plugins/kirby-seo/classes/Sitemap/SitemapIndex.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo\Sitemap;
|
||||
|
||||
use DOMDocument;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Toolkit\Collection;
|
||||
|
||||
class SitemapIndex extends Collection
|
||||
{
|
||||
protected static $instance = null;
|
||||
|
||||
public static function instance(...$args): static
|
||||
{
|
||||
if (static::$instance === null) {
|
||||
static::$instance = new static(...$args);
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
public function create(string $key = 'pages'): Sitemap
|
||||
{
|
||||
$sitemap = $this->make($key);
|
||||
$this->append($sitemap);
|
||||
return $sitemap;
|
||||
}
|
||||
|
||||
public static function make(string $key = 'pages'): Sitemap
|
||||
{
|
||||
return new Sitemap($key);
|
||||
}
|
||||
|
||||
public static function makeUrl(string $url): SitemapUrl
|
||||
{
|
||||
return new SitemapUrl($url);
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
||||
$doc->formatOutput = true;
|
||||
|
||||
$doc->appendChild($doc->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="sitemap.xsl"'));
|
||||
|
||||
$root = $doc->createElementNS('http://www.sitemaps.org/schemas/sitemap/0.9', 'sitemapindex');
|
||||
$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
|
||||
$root->setAttribute('seo-version', App::plugin('tobimori/seo')->version());
|
||||
$doc->appendChild($root);
|
||||
|
||||
foreach ($this as $sitemap) {
|
||||
$root->appendChild($sitemap->toDOMNode($doc));
|
||||
}
|
||||
|
||||
return $doc->saveXML();
|
||||
}
|
||||
|
||||
public function isValidIndex(string $key = null): bool
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->count() > 1;
|
||||
}
|
||||
|
||||
return !!$this->findBy('key', $key) && $this->count() > 1;
|
||||
}
|
||||
|
||||
public function generate(): void
|
||||
{
|
||||
$generator = option('tobimori.seo.sitemap.generator');
|
||||
if (is_callable($generator)) {
|
||||
$generator($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(Page $page): string|null
|
||||
{
|
||||
// There always has to be at least one index,
|
||||
// otherwise the sitemap will fail to render
|
||||
if ($this->count() === 0) {
|
||||
$this->generate();
|
||||
}
|
||||
|
||||
if ($this->count() === 0) {
|
||||
$this->create();
|
||||
}
|
||||
|
||||
if (($index = $page->content()->get('index'))->isEmpty()) {
|
||||
// If there is only one index, we do not need to render the index page
|
||||
return $this->count() > 1 ? $this->toString() : $this->first()->toString();
|
||||
}
|
||||
|
||||
$sitemap = $this->findBy('key', $index->value());
|
||||
if ($sitemap) {
|
||||
return $sitemap->toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
113
site/plugins/kirby-seo/classes/Sitemap/SitemapUrl.php
Normal file
113
site/plugins/kirby-seo/classes/Sitemap/SitemapUrl.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo\Sitemap;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
use Kirby\Exception\Exception;
|
||||
|
||||
class SitemapUrl
|
||||
{
|
||||
protected string $lastmod;
|
||||
protected string $changefreq;
|
||||
protected string $priority;
|
||||
protected array $alternates = [];
|
||||
|
||||
public function __construct(protected string $loc)
|
||||
{
|
||||
}
|
||||
|
||||
public function loc(string $url = null): SitemapUrl|string
|
||||
{
|
||||
if ($url === null) {
|
||||
return $this->loc;
|
||||
}
|
||||
|
||||
$this->loc = $url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function lastmod(string $lastmod = null): SitemapUrl|string
|
||||
{
|
||||
if ($lastmod === null) {
|
||||
return $this->lastmod;
|
||||
}
|
||||
|
||||
$this->lastmod = date('c', $lastmod);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function changefreq(string $changefreq = null): SitemapUrl|string
|
||||
{
|
||||
if ($changefreq === null) {
|
||||
return $this->changefreq;
|
||||
}
|
||||
|
||||
$this->changefreq = $changefreq;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function priority(string $priority = null): SitemapUrl|string
|
||||
{
|
||||
if ($priority === null) {
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
$this->priority = $priority;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function alternates(array $alternates = []): SitemapUrl|array
|
||||
{
|
||||
if (empty($alternates)) {
|
||||
return $this->alternates;
|
||||
}
|
||||
|
||||
foreach ($alternates as $alternate) {
|
||||
foreach (['href', 'hreflang'] as $key) {
|
||||
if (!array_key_exists($key, $alternate)) {
|
||||
new Exception("[kirby-seo] The alternate link to '{$this->loc()} is missing the '{$key}' attribute");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->alternates = $alternates;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toDOMNode(DOMDocument $doc = new DOMDocument('1.0', 'UTF-8')): DOMNode
|
||||
{
|
||||
$doc->formatOutput = true;
|
||||
|
||||
$node = $doc->createElement('url');
|
||||
|
||||
foreach (array_diff_key(get_object_vars($this), array_flip(['alternates'])) as $key => $value) {
|
||||
$node->appendChild($doc->createElement($key, $value));
|
||||
}
|
||||
|
||||
if (!empty($this->alternates())) {
|
||||
foreach ($this->alternates() as $alternate) {
|
||||
$alternateNode = $doc->createElement('xhtml:link');
|
||||
foreach ($alternate as $key => $value) {
|
||||
$alternateNode->setAttribute($key, $value);
|
||||
}
|
||||
|
||||
$node->appendChild($alternateNode);
|
||||
}
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
||||
$doc->formatOutput = true;
|
||||
|
||||
$node = $this->toDOMNode();
|
||||
$doc->appendChild($node);
|
||||
|
||||
return $doc->saveXML($node);
|
||||
}
|
||||
}
|
||||
41
site/plugins/kirby-seo/composer.json
Normal file
41
site/plugins/kirby-seo/composer.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "tobimori/kirby-seo",
|
||||
"description": "The ultimate Kirby SEO toolkit",
|
||||
"type": "kirby-plugin",
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/tobimori/kirby-seo#readme",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Tobias Möritz",
|
||||
"email": "tobias@moeritz.io"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"tobimori\\Seo\\": "classes"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1.0",
|
||||
"getkirby/composer-installer": "^1.2.1",
|
||||
"spatie/schema-org": "^3.14"
|
||||
},
|
||||
"require-dev": {
|
||||
"getkirby/cms": "^4.0",
|
||||
"getkirby/cli": "^1.2",
|
||||
"friendsofphp/php-cs-fixer": "^3.48"
|
||||
},
|
||||
"scripts": {
|
||||
"dist": "composer install --no-dev --optimize-autoloader"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"allow-plugins": {
|
||||
"getkirby/composer-installer": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"kirby-cms-path": false
|
||||
}
|
||||
}
|
||||
102
site/plugins/kirby-seo/config/api.php
Normal file
102
site/plugins/kirby-seo/config/api.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\Site;
|
||||
use Kirby\Form\Form;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'data' => [
|
||||
'dirtyPageOrSite' => function (string $slug) {
|
||||
$kirby = kirby();
|
||||
$page = $slug == 'site' ? $kirby->site() : $kirby->page(Str::replace($slug, '+', '/'));
|
||||
|
||||
if ($this->requestBody()) {
|
||||
$form = Form::for($page, [ // Form class handles transformation of changed items
|
||||
'ignoreDisabled' => true,
|
||||
'input' => array_merge(['title' => $page->title()], $page->content()->data(), $this->requestBody()),
|
||||
'language' => $kirby->language()?->code()
|
||||
]);
|
||||
|
||||
$page = $page->clone(['content' => $form->data()]);
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
],
|
||||
'routes' => [
|
||||
[
|
||||
'pattern' => '/k-seo/(:any)/heading-structure',
|
||||
'method' => 'POST',
|
||||
'action' => function (string $slug) {
|
||||
$model = $this->dirtyPageOrSite($slug);
|
||||
|
||||
if ($model instanceof Page) {
|
||||
$page = $model->render();
|
||||
$dom = new DOMDocument();
|
||||
$dom->loadHTML(htmlspecialchars_decode(mb_convert_encoding(htmlentities($page, ENT_COMPAT, 'UTF-8'), 'ISO-8859-1', 'UTF-8'), ENT_QUOTES), libxml_use_internal_errors(true));
|
||||
|
||||
$xpath = new DOMXPath($dom);
|
||||
$headings = $xpath->query('//h1|//h2|//h3|//h4|//h5|//h6');
|
||||
$data = [];
|
||||
|
||||
foreach ($headings as $heading) {
|
||||
$data[] = [
|
||||
'level' => (int)str_replace('h', '', $heading->nodeName),
|
||||
'text' => $heading->textContent,
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => '/k-seo/(:any)/seo-preview',
|
||||
'method' => 'POST',
|
||||
'action' => function (string $slug) {
|
||||
$model = $this->dirtyPageOrSite($slug);
|
||||
|
||||
if ($model instanceof Site) {
|
||||
$model = $model->homePage();
|
||||
}
|
||||
|
||||
if ($model instanceof Page) {
|
||||
$meta = $model->metadata();
|
||||
|
||||
return [
|
||||
'page' => $model->slug(),
|
||||
'url' => $model->url(),
|
||||
'title' => $meta->metaTitle()->value(),
|
||||
'description' => $meta->metaDescription()->value(),
|
||||
'ogSiteName' => $meta->ogSiteName()->value(),
|
||||
'ogTitle' => $meta->ogTitle()->value(),
|
||||
'ogDescription' => $meta->ogDescription()->value(),
|
||||
'ogImage' => $meta->ogImage(),
|
||||
'twitterCardType' => $meta->twitterCardType()->value(),
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => '/k-seo/(:any)/robots',
|
||||
'method' => 'POST',
|
||||
'action' => function (string $slug) {
|
||||
$model = $this->dirtyPageOrSite($slug);
|
||||
if (!($model instanceof Page)) {
|
||||
return null;
|
||||
}
|
||||
$robots = $model->robots();
|
||||
|
||||
return [
|
||||
'active' => option('tobimori.seo.robots.indicator', option('tobimori.seo.robots.active', true)),
|
||||
'state' => $robots,
|
||||
];
|
||||
}
|
||||
]
|
||||
]
|
||||
];
|
||||
11
site/plugins/kirby-seo/config/commands/hello.php
Normal file
11
site/plugins/kirby-seo/config/commands/hello.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
use Kirby\CLI\CLI;
|
||||
|
||||
return [
|
||||
'description' => 'Hello world',
|
||||
'args' => [],
|
||||
'command' => static function (CLI $cli): void {
|
||||
$cli->success('Hello world! This command is a preparation for a future release.');
|
||||
}
|
||||
];
|
||||
27
site/plugins/kirby-seo/config/hooks.php
Normal file
27
site/plugins/kirby-seo/config/hooks.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'page.update:after' => function (Page $newPage, Page $oldPage) {
|
||||
foreach ($newPage->kirby()->option('tobimori.seo.robots.types') as $robots) {
|
||||
$upper = Str::ucfirst($robots);
|
||||
if ($newPage->content()->get("robots{$upper}")->value() === "") {
|
||||
$newPage = $newPage->update([
|
||||
"robots{$upper}" => 'default'
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
'page.render:before' => function (string $contentType, array $data, Page $page) {
|
||||
if (option('tobimori.seo.generateSchema')) {
|
||||
$page->schema('WebSite')
|
||||
->url($page->metadata()->canonicalUrl())
|
||||
->copyrightYear(date('Y'))
|
||||
->description($page->metadata()->metaDescription())
|
||||
->name($page->metadata()->metaTitle())
|
||||
->headline($page->metadata()->title());
|
||||
}
|
||||
},
|
||||
];
|
||||
82
site/plugins/kirby-seo/config/options.php
Normal file
82
site/plugins/kirby-seo/config/options.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
|
||||
return [
|
||||
'cascade' => [
|
||||
'fields',
|
||||
'programmatic',
|
||||
// 'fallbackFields', // fallback to meta fields for open graph fields
|
||||
'parent',
|
||||
'site',
|
||||
'options'
|
||||
],
|
||||
'default' => [ // default field values for metadata, format is [field => value]
|
||||
'metaTitle' => fn (Page $page) => $page->title(),
|
||||
'metaTemplate' => '{{ title }} - {{ site.title }}',
|
||||
'ogTemplate' => '{{ title }}',
|
||||
'ogSiteName' => fn (Page $page) => $page->site()->title(),
|
||||
'ogType' => 'website',
|
||||
'twitterCardType' => 'summary',
|
||||
'ogDescription' => fn (Page $page) => $page->metadata()->metaDescription(),
|
||||
'twitterCreator' => fn (Page $page) => $page->metadata()->twitterSite(),
|
||||
'lang' => fn (Page $page) => $page->kirby()->language()?->locale(LC_ALL) ?? $page->kirby()->option('tobimori.seo.lang', 'en_US'),
|
||||
// default for robots: noIndex if global index configuration is set, otherwise fall back to page status
|
||||
'robotsIndex' => function (Page $page) {
|
||||
$index = $page->kirby()->option('tobimori.seo.robots.index');
|
||||
if (is_callable($index)) {
|
||||
$index = $index();
|
||||
}
|
||||
|
||||
if (!$index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $page->kirby()->option('tobimori.seo.robots.followPageStatus', true) ? $page->isListed() : true;
|
||||
},
|
||||
'robotsFollow' => fn (Page $page) => $page->kirby()->option('tobimori.seo.default.robotsIndex')($page),
|
||||
'robotsArchive' => fn (Page $page) => $page->kirby()->option('tobimori.seo.default.robotsIndex')($page),
|
||||
'robotsImageindex' => fn (Page $page) => $page->kirby()->option('tobimori.seo.default.robotsIndex')($page),
|
||||
'robotsSnippet' => fn (Page $page) => $page->kirby()->option('tobimori.seo.default.robotsIndex')($page),
|
||||
],
|
||||
'socialMedia' => [ // default fields for social media links, format is [field => placeholder]
|
||||
'twitter' => 'https://twitter.com/my-company',
|
||||
'facebook' => 'https://facebook.com/my-company',
|
||||
'instagram' => 'https://instagram.com/my-company',
|
||||
'youtube' => 'https://youtube.com/channel/my-company',
|
||||
'linkedin' => 'https://linkedin.com/company/my-company',
|
||||
],
|
||||
'previews' => [
|
||||
'google',
|
||||
'facebook',
|
||||
'slack'
|
||||
],
|
||||
'robots' => [
|
||||
'active' => true, // whether robots handling should be done by the plugin
|
||||
'followPageStatus' => true, // should unlisted pages be noindex by default?
|
||||
'pageSettings' => true, // whether to have robots settings on each page
|
||||
'indicator' => true, // whether the indicator should be shown in the panel
|
||||
'index' => fn () => !option('debug'), // default site-wide robots setting
|
||||
'sitemap' => null, // sets sitemap url, will be replaced by plugin sitemap in the future
|
||||
'content' => [], // custom robots content
|
||||
'types' => ['index', 'follow', 'archive', 'imageindex', 'snippet'] // available robots types
|
||||
],
|
||||
'sitemap' => [
|
||||
'active' => true,
|
||||
'redirect' => true, // redirect /sitemap to /sitemap.xml
|
||||
'lang' => 'en',
|
||||
'generator' => require __DIR__ . '/options/sitemap.php',
|
||||
'changefreq' => 'weekly',
|
||||
'groupByTemplate' => false,
|
||||
'excludeTemplates' => ['error'],
|
||||
'priority' => fn (Page $p) => number_format(($p->isHomePage()) ? 1 : max(1 - 0.2 * $p->depth(), 0.2), 1),
|
||||
],
|
||||
'files' => [
|
||||
'parent' => null,
|
||||
'template' => null,
|
||||
],
|
||||
'canonicalBase' => null, // base url for canonical links
|
||||
'generateSchema' => true, // whether to generate default schema.org data
|
||||
'lang' => 'en_US', // default language, used for single-language sites
|
||||
'dateFormat' => null, // custom date format
|
||||
];
|
||||
37
site/plugins/kirby-seo/config/options/sitemap.php
Normal file
37
site/plugins/kirby-seo/config/options/sitemap.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\Obj;
|
||||
use tobimori\Seo\Sitemap\SitemapIndex;
|
||||
|
||||
return function (SitemapIndex $sitemap) {
|
||||
$exclude = option('tobimori.seo.sitemap.excludeTemplates', []);
|
||||
$pages = site()->index()->filter(fn ($page) => $page->metadata()->robotsIndex()->toBool() && !in_array($page->intendedTemplate()->name(), $exclude));
|
||||
|
||||
if ($group = option('tobimori.seo.sitemap.groupByTemplate')) {
|
||||
$pages = $pages->group('intendedTemplate');
|
||||
}
|
||||
|
||||
if (is_a($pages->first(), 'Kirby\Cms\Page')) {
|
||||
$pages = $pages->group(fn () => 'pages');
|
||||
}
|
||||
|
||||
foreach ($pages as $group) {
|
||||
$index = $sitemap->create($group ? $group->first()->intendedTemplate()->name() : 'pages');
|
||||
|
||||
foreach ($group as $page) {
|
||||
$url = $index->createUrl($page->metadata()->canonicalUrl())
|
||||
->lastmod($page->modified() ?? (int)(date('c')))
|
||||
->changefreq(is_callable($changefreq = option('tobimori.seo.sitemap.changefreq')) ? $changefreq($page) : $changefreq)
|
||||
->priority(is_callable($priority = option('tobimori.seo.sitemap.priority')) ? $priority($page) : $priority);
|
||||
|
||||
if (kirby()->languages()->count() > 1 && kirby()->language() !== null) {
|
||||
$url->alternates(
|
||||
kirby()->languages()->map(fn ($language) => new Obj([
|
||||
'hreflang' => $language->code() === kirby()->language()->code() ? 'x-default' : $language->code(),
|
||||
'href' => $page->url($language->code()),
|
||||
]))->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
11
site/plugins/kirby-seo/config/pageMethods.php
Normal file
11
site/plugins/kirby-seo/config/pageMethods.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
use tobimori\Seo\Meta;
|
||||
use tobimori\Seo\SchemaSingleton;
|
||||
|
||||
return [
|
||||
'schema' => fn ($type) => SchemaSingleton::getInstance($type, $this),
|
||||
'schemas' => fn () => SchemaSingleton::getInstances($this),
|
||||
'metadata' => fn (?string $lang = null) => new Meta($this, $lang),
|
||||
'robots' => fn (?string $lang = null) => $this->metadata($lang)->robots(),
|
||||
];
|
||||
98
site/plugins/kirby-seo/config/routes.php
Normal file
98
site/plugins/kirby-seo/config/routes.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Http\Response;
|
||||
use tobimori\Seo\Sitemap\SitemapIndex;
|
||||
|
||||
return [
|
||||
[
|
||||
'pattern' => 'robots.txt',
|
||||
'action' => function () {
|
||||
if (option('tobimori.seo.robots.active', true)) {
|
||||
$content = snippet('seo/robots.txt', [], true);
|
||||
return new Response($content, 'text/plain', 200);
|
||||
}
|
||||
|
||||
$this->next();
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'sitemap',
|
||||
'action' => function () {
|
||||
if (!option('tobimori.seo.sitemap.redirect', true) || !option('tobimori.seo.sitemap.active', true)) {
|
||||
$this->next();
|
||||
}
|
||||
|
||||
go('/sitemap.xml');
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'sitemap.xsl',
|
||||
'action' => function () {
|
||||
if (!option('tobimori.seo.sitemap.active', true)) {
|
||||
$this->next();
|
||||
}
|
||||
|
||||
kirby()->response()->type('text/xsl');
|
||||
|
||||
$lang = option('tobimori.seo.sitemap.lang', 'en');
|
||||
if (is_callable($lang)) {
|
||||
$lang = $lang();
|
||||
}
|
||||
kirby()->setCurrentTranslation($lang);
|
||||
|
||||
return Page::factory([
|
||||
'slug' => 'sitemap',
|
||||
'template' => 'sitemap',
|
||||
'model' => 'sitemap',
|
||||
'content' => [
|
||||
'title' => t('sitemap'),
|
||||
],
|
||||
])->render(contentType: 'xsl');
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'sitemap.xml',
|
||||
'action' => function () {
|
||||
if (!option('tobimori.seo.sitemap.active', true)) {
|
||||
$this->next();
|
||||
}
|
||||
|
||||
SitemapIndex::instance()->generate();
|
||||
kirby()->response()->type('text/xml');
|
||||
return Page::factory([
|
||||
'slug' => 'sitemap',
|
||||
'template' => 'sitemap',
|
||||
'model' => 'sitemap',
|
||||
'content' => [
|
||||
'title' => t('sitemap'),
|
||||
'index' => null,
|
||||
],
|
||||
])->render(contentType: 'xml');
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'sitemap-(:any).xml',
|
||||
'action' => function (string $index) {
|
||||
if (!option('tobimori.seo.sitemap.active', true)) {
|
||||
$this->next();
|
||||
}
|
||||
|
||||
SitemapIndex::instance()->generate();
|
||||
if (!SitemapIndex::instance()->isValidIndex($index)) {
|
||||
$this->next();
|
||||
}
|
||||
|
||||
kirby()->response()->type('text/xml');
|
||||
return Page::factory([
|
||||
'slug' => 'sitemap-' . $index,
|
||||
'template' => 'sitemap',
|
||||
'model' => 'sitemap',
|
||||
'content' => [
|
||||
'title' => t('sitemap'),
|
||||
'index' => $index,
|
||||
],
|
||||
])->render(contentType: 'xml');
|
||||
}
|
||||
]
|
||||
];
|
||||
20
site/plugins/kirby-seo/config/sections.php
Normal file
20
site/plugins/kirby-seo/config/sections.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
return [
|
||||
'seo-preview' => [
|
||||
'mixins' => ['headline'],
|
||||
'computed' => [
|
||||
'options' => function () {
|
||||
return A::map(option('tobimori.seo.previews'), fn ($item) => [
|
||||
'value' => $item,
|
||||
'text' => t($item)
|
||||
]);
|
||||
}
|
||||
]
|
||||
],
|
||||
'heading-structure' => [
|
||||
'mixins' => ['headline']
|
||||
]
|
||||
];
|
||||
28
site/plugins/kirby-seo/config/siteMethods.php
Normal file
28
site/plugins/kirby-seo/config/siteMethods.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Http\Url;
|
||||
use Kirby\Toolkit\Str;
|
||||
use tobimori\Seo\SchemaSingleton;
|
||||
|
||||
return [
|
||||
'schema' => fn ($type) => SchemaSingleton::getInstance($type),
|
||||
'schemas' => fn () => SchemaSingleton::getInstances(),
|
||||
'lang' => fn () => Str::replace(option('tobimori.seo.default.lang')($this->homePage()), '_', '-'),
|
||||
'canonicalFor' => function (string $url) {
|
||||
$base = option('tobimori.seo.canonicalBase');
|
||||
if (is_callable($base)) {
|
||||
$base = $base($url);
|
||||
}
|
||||
|
||||
if ($base === null) {
|
||||
$base = $this->url(); // graceful fallback to site url
|
||||
}
|
||||
|
||||
if (Str::startsWith($url, $base)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$path = Url::path($url);
|
||||
return url($base . '/' . $path);
|
||||
}
|
||||
];
|
||||
1
site/plugins/kirby-seo/index.css
Normal file
1
site/plugins/kirby-seo/index.css
Normal file
File diff suppressed because one or more lines are too long
1
site/plugins/kirby-seo/index.js
Normal file
1
site/plugins/kirby-seo/index.js
Normal file
File diff suppressed because one or more lines are too long
69
site/plugins/kirby-seo/index.php
Normal file
69
site/plugins/kirby-seo/index.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
@include_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Data\Yaml;
|
||||
use Kirby\Filesystem\Dir;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Toolkit\A;
|
||||
use Spatie\SchemaOrg\Schema;
|
||||
|
||||
// shamelessly borrowed from distantnative/retour-for-kirby
|
||||
if (
|
||||
version_compare(App::version() ?? '0.0.0', '4.0.2', '<') === true ||
|
||||
version_compare(App::version() ?? '0.0.0', '5.0.0', '>') === true
|
||||
) {
|
||||
throw new Exception('Kirby SEO requires Kirby 4.0.2 or higher.');
|
||||
}
|
||||
|
||||
App::plugin('tobimori/seo', [
|
||||
'options' => require __DIR__ . '/config/options.php',
|
||||
'sections' => require __DIR__ . '/config/sections.php',
|
||||
'api' => require __DIR__ . '/config/api.php',
|
||||
'siteMethods' => require __DIR__ . '/config/siteMethods.php',
|
||||
'pageMethods' => require __DIR__ . '/config/pageMethods.php',
|
||||
'hooks' => require __DIR__ . '/config/hooks.php',
|
||||
'routes' => require __DIR__ . '/config/routes.php',
|
||||
// load all commands automatically
|
||||
'commands' => A::keyBy(A::map(
|
||||
Dir::read(__DIR__ . '/config/commands'),
|
||||
fn ($file) => A::merge([
|
||||
'id' => 'seo:' . F::name($file),
|
||||
], require __DIR__ . '/config/commands/' . $file)
|
||||
), 'id'),
|
||||
// get all files from /translations and register them as language files
|
||||
'translations' => A::keyBy(A::map(
|
||||
Dir::read(__DIR__ . '/translations'),
|
||||
fn ($file) => A::merge([
|
||||
'lang' => F::name($file),
|
||||
], Yaml::decode(F::read(__DIR__ . '/translations/' . $file)))
|
||||
), 'lang'),
|
||||
'snippets' => [
|
||||
'seo/schemas' => __DIR__ . '/snippets/schemas.php',
|
||||
'seo/head' => __DIR__ . '/snippets/head.php',
|
||||
'seo/robots.txt' => __DIR__ . '/snippets/robots.txt.php',
|
||||
],
|
||||
'templates' => [
|
||||
'sitemap' => __DIR__ . '/templates/sitemap.php',
|
||||
'sitemap.xml' => __DIR__ . '/templates/sitemap.xml.php',
|
||||
'sitemap.xsl' => __DIR__ . '/templates/sitemap.xsl.php',
|
||||
],
|
||||
'blueprints' => [
|
||||
'seo/site' => __DIR__ . '/blueprints/site.yml',
|
||||
'seo/page' => __DIR__ . '/blueprints/page.yml',
|
||||
'seo/fields/og-image' => require __DIR__ . '/blueprints/fields/og-image.php',
|
||||
'seo/fields/og-group' => __DIR__ . '/blueprints/fields/og-group.yml',
|
||||
'seo/fields/meta-group' => __DIR__ . '/blueprints/fields/meta-group.yml',
|
||||
'seo/fields/robots' => require __DIR__ . '/blueprints/fields/robots.php',
|
||||
'seo/fields/site-robots' => require __DIR__ . '/blueprints/fields/site-robots.php',
|
||||
'seo/fields/social-media' => require __DIR__ . '/blueprints/fields/social-media.php',
|
||||
],
|
||||
]);
|
||||
|
||||
if (!function_exists('schema')) {
|
||||
function schema($type)
|
||||
{
|
||||
return Schema::{$type}();
|
||||
}
|
||||
}
|
||||
17
site/plugins/kirby-seo/package.json
Normal file
17
site/plugins/kirby-seo/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"author": "Tobias Möritz",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "kirbyup serve src/index.js",
|
||||
"build": "kirbyup src/index.js",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"husky": "^9.0.11",
|
||||
"kirbyup": "^3.1.4",
|
||||
"postcss": "^8.4.35",
|
||||
"sass": "^1.71.1"
|
||||
}
|
||||
}
|
||||
6
site/plugins/kirby-seo/postcss.config.js
Normal file
6
site/plugins/kirby-seo/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'postcss-logical': {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
13
site/plugins/kirby-seo/snippets/head.php
Normal file
13
site/plugins/kirby-seo/snippets/head.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Page $page
|
||||
*/
|
||||
|
||||
use Kirby\Cms\Html;
|
||||
|
||||
$tags = $page->metadata()->snippetData();
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
echo Html::tag($tag['tag'], $tag['content'] ?? null, $tag['attributes'] ?? []) . PHP_EOL;
|
||||
}
|
||||
58
site/plugins/kirby-seo/snippets/robots.txt.php
Normal file
58
site/plugins/kirby-seo/snippets/robots.txt.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
if ($content = option('tobimori.seo.robots.content')) {
|
||||
if (is_callable($content)) {
|
||||
$content = $content();
|
||||
}
|
||||
|
||||
if (is_array($content)) {
|
||||
$str = [];
|
||||
|
||||
foreach ($content as $ua => $data) {
|
||||
$str[] = 'User-agent: ' . $ua;
|
||||
foreach ($data as $type => $values) {
|
||||
foreach ($values as $value) {
|
||||
$str[] = $type . ': ' . $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$content = A::join($str, PHP_EOL);
|
||||
}
|
||||
|
||||
echo $content;
|
||||
} else {
|
||||
// output default
|
||||
echo "User-agent: *\n";
|
||||
|
||||
$index = option('tobimori.seo.robots.index');
|
||||
if (is_callable($index)) {
|
||||
$index = $index();
|
||||
}
|
||||
|
||||
if ($index) {
|
||||
echo 'Allow: /';
|
||||
echo "\nDisallow: /panel";
|
||||
} else {
|
||||
echo 'Disallow: /';
|
||||
}
|
||||
}
|
||||
|
||||
if (($sitemap = option('tobimori.seo.robots.sitemap')) || ($sitemapModule = option('tobimori.seo.sitemap.active'))) {
|
||||
// Allow closure to be used
|
||||
if (is_callable($sitemap)) {
|
||||
$sitemap = $sitemap();
|
||||
}
|
||||
|
||||
// Use default sitemap if none is set
|
||||
if (!$sitemap && $sitemapModule) {
|
||||
$sitemap = site()->canonicalFor('/sitemap.xml');
|
||||
}
|
||||
|
||||
// Check again, so falsy values can't be used
|
||||
if ($sitemap) {
|
||||
echo "\n\nSitemap: {$sitemap}";
|
||||
}
|
||||
}
|
||||
8
site/plugins/kirby-seo/snippets/schemas.php
Normal file
8
site/plugins/kirby-seo/snippets/schemas.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
$siteSchema ??= true;
|
||||
$pageSchema ??= true;
|
||||
|
||||
foreach (array_merge($siteSchema ? $site->schemas() : [], $pageSchema ? $page->schemas() : []) as $schema) {
|
||||
echo $schema;
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="k-facebook-preview">
|
||||
<div class="k-facebook-preview__image" v-if="ogImage">
|
||||
<img :src="ogImage" class="k-facebook-preview__img" />
|
||||
</div>
|
||||
<div class="k-facebook-preview__content">
|
||||
<span class="k-facebook-preview__url">{{ host }}</span>
|
||||
<span class="k-facebook-preview__title">{{ ogTitle }}</span>
|
||||
<p class="k-facebook-preview__description">{{ ogDescription }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="k-seo-preview__debugger"
|
||||
href="https://developers.facebook.com/tools/debug/"
|
||||
aria-label="Facebook Sharing Debugger"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t('open-debugger') }}
|
||||
<k-icon type="open" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
ogTitle: String,
|
||||
url: String,
|
||||
ogDescription: String,
|
||||
ogImage: String
|
||||
},
|
||||
computed: {
|
||||
host() {
|
||||
return new URL(this.url).host
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.k-facebook-preview {
|
||||
background: #f0f2f5;
|
||||
border: 1px solid #ced0d4;
|
||||
overflow: hidden;
|
||||
border-radius: var(--rounded);
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 52.355%;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
&__title,
|
||||
&__description,
|
||||
&__url {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
&__url {
|
||||
color: #65676b;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
line-height: 1.1765;
|
||||
font-size: 1rem;
|
||||
color: #050505;
|
||||
margin: 0.3125rem 0;
|
||||
}
|
||||
|
||||
&__description {
|
||||
line-height: 1.3333;
|
||||
color: #65676b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
115
site/plugins/kirby-seo/src/components/Previews/GooglePreview.vue
Normal file
115
site/plugins/kirby-seo/src/components/Previews/GooglePreview.vue
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="k-google-search-preview">
|
||||
<span class="k-google-search-preview__url">
|
||||
<span>{{ origin }}</span>
|
||||
<span
|
||||
v-for="(breadcrumb, index) in breadcrumbs"
|
||||
:key="index"
|
||||
class="k-google-search-preview__url__breadcrumb"
|
||||
>
|
||||
{{ breadcrumb }}
|
||||
</span>
|
||||
</span>
|
||||
<h2 class="k-google-search-preview__headline">{{ title }}</h2>
|
||||
<p class="k-google-search-preview__paragraph">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
class="k-seo-preview__debugger"
|
||||
href="https://search.google.com/search-console"
|
||||
aria-label="Google Search Console"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t('open-search-console') }}
|
||||
<k-icon type="open" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: String,
|
||||
url: String,
|
||||
description: String
|
||||
},
|
||||
computed: {
|
||||
origin() {
|
||||
return new URL(this.url).origin
|
||||
},
|
||||
breadcrumbs() {
|
||||
return this.url.split('/').slice(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.k-google-search-preview {
|
||||
padding: 1em;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
letter-spacing: -0.005em;
|
||||
border-radius: var(--rounded);
|
||||
|
||||
&__headline,
|
||||
&__paragraph {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__headline {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.25em;
|
||||
font-size: 1.25em;
|
||||
font-weight: normal;
|
||||
color: #1a0dab;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__url {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5em;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.3;
|
||||
color: #202124;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
|
||||
> * {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&__breadcrumb {
|
||||
color: #5f6368;
|
||||
display: inline-block;
|
||||
|
||||
&::before {
|
||||
content: '› ';
|
||||
}
|
||||
}
|
||||
|
||||
.k-icon {
|
||||
margin-left: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
&__paragraph {
|
||||
margin: 0;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.3em;
|
||||
color: #3c4043;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<div class="k-slack-preview">
|
||||
<div class="k-slack-preview__content">
|
||||
<div class="k-slack-preview__site-name">{{ ogSiteName || origin }}</div>
|
||||
<span class="k-slack-preview__title">{{ ogTitle }}</span>
|
||||
<p class="k-slack-preview__description">{{ ogDescription }}</p>
|
||||
</div>
|
||||
<div class="k-slack-preview__image" v-if="ogImage">
|
||||
<img :src="ogImage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
ogTitle: String,
|
||||
ogSiteName: String,
|
||||
ogDescription: String,
|
||||
ogImage: String
|
||||
},
|
||||
computed: {
|
||||
origin() {
|
||||
return new URL(this.url).origin
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.k-slack-preview {
|
||||
max-width: 32.5rem;
|
||||
position: relative;
|
||||
padding-left: 1rem;
|
||||
line-height: 1.46666667;
|
||||
font-size: 0.9375rem;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
&__site-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #717274;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
color: #0576b9;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: #2c2d30;
|
||||
}
|
||||
|
||||
&__image {
|
||||
border-radius: 0.5rem;
|
||||
max-width: 22.5rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
&::before {
|
||||
border-radius: 0.5rem;
|
||||
content: '';
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
150
site/plugins/kirby-seo/src/components/Views/PageView.vue
Normal file
150
site/plugins/kirby-seo/src/components/Views/PageView.vue
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
https://github.com/getkirby/kirby/blob/main/panel/src/components/Views/Pages/PageView.vue
|
||||
|
||||
<template>
|
||||
<k-panel-inside
|
||||
:data-has-tabs="tabs.length > 1"
|
||||
:data-id="model.id"
|
||||
:data-locked="isLocked"
|
||||
:data-template="blueprint"
|
||||
class="k-page-view"
|
||||
>
|
||||
<template #topbar>
|
||||
<k-prev-next v-if="model.id" :prev="prev" :next="next" />
|
||||
</template>
|
||||
|
||||
<k-header
|
||||
:editable="permissions.changeTitle && !isLocked"
|
||||
class="k-page-view-header"
|
||||
@edit="$dialog(id + '/changeTitle')"
|
||||
>
|
||||
{{ model.title }}
|
||||
<template #buttons>
|
||||
<k-button-group>
|
||||
<k-button
|
||||
v-if="permissions.preview && model.previewUrl"
|
||||
:link="model.previewUrl"
|
||||
:title="$t('open')"
|
||||
icon="open"
|
||||
target="_blank"
|
||||
variant="filled"
|
||||
size="sm"
|
||||
class="k-page-view-preview"
|
||||
/>
|
||||
|
||||
<k-button
|
||||
:disabled="isLocked === true"
|
||||
:dropdown="true"
|
||||
:title="$t('settings')"
|
||||
icon="cog"
|
||||
variant="filled"
|
||||
size="sm"
|
||||
class="k-page-view-options"
|
||||
@click="$refs.settings.toggle()"
|
||||
/>
|
||||
<k-dropdown-content ref="settings" :options="$dropdown(id)" align-x="end" />
|
||||
|
||||
<k-languages-dropdown />
|
||||
|
||||
<k-button
|
||||
v-if="status"
|
||||
v-bind="statusBtn"
|
||||
class="k-page-view-status"
|
||||
variant="filled"
|
||||
@click="$dialog(id + '/changeStatus')"
|
||||
/>
|
||||
|
||||
<k-button
|
||||
class="k-page-view-status k-page-view-robots"
|
||||
v-if="robots && robots.active"
|
||||
v-bind="robotsBtn"
|
||||
@click="openSeoTab"
|
||||
/>
|
||||
</k-button-group>
|
||||
|
||||
<k-form-buttons :lock="lock" />
|
||||
</template>
|
||||
</k-header>
|
||||
|
||||
<k-model-tabs :tab="tab.name" :tabs="tabs" />
|
||||
|
||||
<k-sections
|
||||
:blueprint="blueprint"
|
||||
:empty="$t('page.blueprint', { blueprint: $esc(blueprint) })"
|
||||
:lock="lock"
|
||||
:parent="id"
|
||||
:tab="tab"
|
||||
/>
|
||||
</k-panel-inside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
extends: 'k-page-view',
|
||||
data() {
|
||||
return {
|
||||
dirty: false,
|
||||
robots: {
|
||||
active: false,
|
||||
state: []
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.handleLoad()
|
||||
},
|
||||
methods: {
|
||||
openSeoTab() {
|
||||
panel.view.open(panel.view.path + '?tab=seo')
|
||||
},
|
||||
async handleLoad(changes) {
|
||||
if (!panel.view.props.tabs.some((tab) => tab.name === 'seo')) return
|
||||
|
||||
const page = this.model.id.replaceAll('/', '+')
|
||||
this.robots = await panel.api.post(`/k-seo/${page}/robots`, changes ?? this.changes)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
changes() {
|
||||
return this.$store.getters['content/changes']() // TODO: new panel API for changes?
|
||||
},
|
||||
robotsBtn() {
|
||||
const btn = {
|
||||
responsive: true,
|
||||
size: 'sm',
|
||||
icon: 'robots',
|
||||
theme: 'positive',
|
||||
text: this.$t('indicator-index'),
|
||||
variant: 'filled'
|
||||
}
|
||||
|
||||
if (this.robots.state.includes('no')) {
|
||||
btn.text = this.$t('indicator-any')
|
||||
btn.theme = 'notice'
|
||||
btn.icon = 'robots-off'
|
||||
}
|
||||
|
||||
if (this.robots.state.includes('noindex')) {
|
||||
btn.text = this.$t('indicator-noindex')
|
||||
btn.theme = 'negative'
|
||||
}
|
||||
|
||||
return btn
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
changes(changes) {
|
||||
if (Object.keys(changes).some((key) => key.includes('robots')) || this.dirty) {
|
||||
this.dirty = false
|
||||
this.handleLoad(changes)
|
||||
if (changes) this.dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.k-page-view-robots {
|
||||
--color-green-boost: -15%;
|
||||
}
|
||||
</style>
|
||||
13
site/plugins/kirby-seo/src/index.js
Normal file
13
site/plugins/kirby-seo/src/index.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { kirbyup } from 'kirbyup/plugin'
|
||||
import PageView from './components/Views/PageView.vue'
|
||||
|
||||
panel.plugin('tobimori/seo', {
|
||||
icons: {
|
||||
robots: `<path d="M13.5 2c0 .444-.193.843-.5 1.118V5h5a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3h5V3.118A1.5 1.5 0 1 1 13.5 2ZM6 7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H6Zm-4 3H0v6h2v-6Zm20 0h2v6h-2v-6ZM9 14.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm6 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />`,
|
||||
'robots-off': `<path fill-rule="evenodd" clip-rule="evenodd" d="M21 16.786V8a3 3 0 0 0-3-3h-5V3.118a1.5 1.5 0 1 0-2 0V5H9.214l2 2H18a1 1 0 0 1 1 1v6.786l2 2ZM2.093 3.507l2.099 2.099A2.995 2.995 0 0 0 3 8v10a3 3 0 0 0 3 3h12c.463 0 .902-.105 1.293-.292l1.9 1.9 1.414-1.415-6.88-6.88a1.5 1.5 0 1 0-2.04-2.04L3.508 2.093 2.093 3.507ZM5 8a1 1 0 0 1 .65-.937L17.585 19H6a1 1 0 0 1-1-1V8Zm-5 2h2v6H0v-6Zm24 0h-2v6h2v-6Zm-13.5 3a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" />`
|
||||
},
|
||||
sections: kirbyup.import('./sections/*.vue'),
|
||||
components: {
|
||||
'k-page-view': PageView
|
||||
}
|
||||
})
|
||||
191
site/plugins/kirby-seo/src/sections/heading-structure.vue
Normal file
191
site/plugins/kirby-seo/src/sections/heading-structure.vue
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<template>
|
||||
<div class="k-section k-heading-structure" v-if="value">
|
||||
<div class="k-field-header k-heading-structure__label k-label k-field-label">
|
||||
<k-icon type="headline" />
|
||||
<span class="k-label-text">{{ label || $t('heading-structure') }}</span>
|
||||
<k-loader v-if="isLoading" />
|
||||
</div>
|
||||
<k-box theme="white">
|
||||
<ol class="k-heading-structure__list">
|
||||
<li
|
||||
v-for="(item, index) in value"
|
||||
:key="index"
|
||||
:style="`z-index: ${value.length - index}`"
|
||||
:class="`k-heading-structure__item level-${item.level} ${
|
||||
itemInvalid(item, index) ? 'is-invalid' : ''
|
||||
}`"
|
||||
>
|
||||
<span class="k-heading-structure__item__level">H{{ item.level }}</span>
|
||||
<span class="k-heading-structure__item__text">{{ item.text }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</k-box>
|
||||
<k-box class="k-heading-structure__notice" theme="negative" v-if="incorrectOrder && !noH1">
|
||||
<k-icon type="alert" />
|
||||
<k-text>{{ $t('incorrect-heading-order') }}</k-text>
|
||||
</k-box>
|
||||
<k-box class="k-heading-structure__notice" theme="negative" v-if="multipleH1">
|
||||
<k-icon type="alert" />
|
||||
<k-text>{{ $t('multiple-h1-tags') }}</k-text>
|
||||
</k-box>
|
||||
<k-box class="k-heading-structure__notice" theme="negative" v-if="noH1">
|
||||
<k-icon type="alert" />
|
||||
<k-text>{{ $t('missing-h1-tag') }}</k-text>
|
||||
</k-box>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
label: null,
|
||||
value: null,
|
||||
isLoading: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.isLoading = true
|
||||
|
||||
this.load().then((data) => {
|
||||
this.label = data.label
|
||||
}) // loads label and properties
|
||||
this.handleLoad() // handles metadata & title change
|
||||
|
||||
this.debouncedLoad = this.$helper.debounce((changes) => {
|
||||
this.handleLoad(changes)
|
||||
}, 200) // debounce function for dirty changes watcher
|
||||
},
|
||||
computed: {
|
||||
changes() {
|
||||
return this.$store.getters['content/changes']()
|
||||
},
|
||||
incorrectOrder() {
|
||||
return this.value?.some((item, index) => item.level > (this.value[index - 1]?.level ?? 0) + 1)
|
||||
},
|
||||
multipleH1() {
|
||||
return this.value?.filter((item) => item.level === 1).length > 1
|
||||
},
|
||||
noH1() {
|
||||
return this.value?.filter((item) => item.level === 1).length === 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLoad(changes) {
|
||||
this.isLoading = true
|
||||
|
||||
const page = panel.view.props.model.id
|
||||
|
||||
if (!page) {
|
||||
throw new Error('[kirby-seo] The Heading structure section is only available for pages')
|
||||
}
|
||||
|
||||
const response = await panel.api.post(
|
||||
`/k-seo/${page.replaceAll('/', '+')}/heading-structure`,
|
||||
changes ?? this.changes
|
||||
)
|
||||
|
||||
this.value = response
|
||||
this.isLoading = false
|
||||
},
|
||||
itemInvalid(item, index) {
|
||||
if (item.level > (this.value[index - 1]?.level ?? 0) + 1) return true // wrong order
|
||||
if (item.level === 1 && this.value[index - 1]) return true // wrong order
|
||||
if (item.level === 1 && this.value.filter((item) => item.level === 1).length > 1) return true // multiple h1
|
||||
|
||||
return false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
changes(changes) {
|
||||
this.debouncedLoad(changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.k-heading-structure {
|
||||
&__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: var(--spacing-2);
|
||||
|
||||
> .k-icon {
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
> .k-loader {
|
||||
margin-left: auto;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
}
|
||||
|
||||
&__notice {
|
||||
margin-top: var(--spacing-2);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
> .k-icon {
|
||||
margin-top: 0.125rem;
|
||||
margin-right: var(--spacing-1);
|
||||
color: var(--color-red);
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
background: var(--theme-color-back);
|
||||
padding-block: var(--spacing-px);
|
||||
display: flex;
|
||||
|
||||
&__level {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
margin-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
&__text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.is-invalid {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
@for $i from 2 through 6 {
|
||||
&.level-#{$i} {
|
||||
margin-left: ($i - 2) * 1.6rem;
|
||||
padding-left: 1.6rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc(50% - 0.0625rem);
|
||||
left: 0.4rem;
|
||||
width: 0.8rem;
|
||||
height: 0.125rem;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: calc(50% - 0.0625rem);
|
||||
left: 0.4rem;
|
||||
height: 9999px;
|
||||
width: 0.125rem;
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
126
site/plugins/kirby-seo/src/sections/seo-preview.vue
Normal file
126
site/plugins/kirby-seo/src/sections/seo-preview.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="k-section k-seo-preview">
|
||||
<div class="k-field-header k-seo-preview__label k-label k-field-label">
|
||||
<k-icon type="preview" /><span class="k-label-text">{{ label || $t('seo-preview') }}</span>
|
||||
<k-loader v-if="isLoading" />
|
||||
</div>
|
||||
<k-select-field
|
||||
type="select"
|
||||
name="seo-preview-type"
|
||||
:before="$t('seo-preview-for')"
|
||||
v-model="type"
|
||||
:options="options"
|
||||
:empty="false"
|
||||
/>
|
||||
<div class="k-seo-preview__inner" v-if="value">
|
||||
<google-preview v-if="type === 'google'" v-bind="value" />
|
||||
<facebook-preview v-if="type === 'facebook'" v-bind="value" />
|
||||
<slack-preview v-if="type === 'slack'" v-bind="value" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FacebookPreview from '../components/Previews/FacebookPreview.vue'
|
||||
import GooglePreview from '../components/Previews/GooglePreview.vue'
|
||||
import SlackPreview from '../components/Previews/SlackPreview.vue'
|
||||
|
||||
export default {
|
||||
components: { GooglePreview, FacebookPreview, SlackPreview },
|
||||
data() {
|
||||
const type = localStorage.getItem('kSEOPreviewType') ?? 'google'
|
||||
|
||||
return {
|
||||
label: null,
|
||||
value: null,
|
||||
isLoading: true,
|
||||
options: [],
|
||||
type
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.isLoading = true
|
||||
|
||||
this.load().then((data) => {
|
||||
this.label = data.label
|
||||
this.options = data.options
|
||||
}) // loads label and properties
|
||||
this.handleLoad() // handles metadata & title change
|
||||
|
||||
this.debouncedLoad = this.$helper.debounce((changes) => {
|
||||
this.handleLoad(changes)
|
||||
}, 200) // debounce function for dirty changes watcher
|
||||
},
|
||||
computed: {
|
||||
changes() {
|
||||
return this.$store.getters['content/changes']()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLoad(changes) {
|
||||
this.isLoading = true
|
||||
|
||||
const page = panel.view.props.model?.id?.replaceAll('/', '+') ?? 'site'
|
||||
const response = await panel.api.post(`/k-seo/${page}/seo-preview`, changes ?? this.changes)
|
||||
|
||||
this.value = response
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
changes(changes) {
|
||||
this.debouncedLoad(changes)
|
||||
},
|
||||
type() {
|
||||
localStorage.setItem('kSEOPreviewType', this.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.k-field-name-seo-preview-type .k-field-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.k-seo-preview {
|
||||
&__inner {
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
&__debugger {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-gray-700);
|
||||
line-height: 1.25rem;
|
||||
width: max-content;
|
||||
margin-left: auto;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--text-gray-800);
|
||||
}
|
||||
|
||||
> .k-icon {
|
||||
margin-left: var(--spacing-2);
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: var(--spacing-2);
|
||||
|
||||
> .k-icon {
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
> .k-loader {
|
||||
margin-left: auto;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
site/plugins/kirby-seo/templates/sitemap.php
Normal file
3
site/plugins/kirby-seo/templates/sitemap.php
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
go($page->url() . '.xml');
|
||||
5
site/plugins/kirby-seo/templates/sitemap.xml.php
Normal file
5
site/plugins/kirby-seo/templates/sitemap.xml.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
use tobimori\Seo\Sitemap\SitemapIndex;
|
||||
|
||||
echo SitemapIndex::instance()->render($page);
|
||||
196
site/plugins/kirby-seo/templates/sitemap.xsl.php
Normal file
196
site/plugins/kirby-seo/templates/sitemap.xsl.php
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?= '<?xml version="1.0" encoding="UTF-8"?>' ?>
|
||||
<xsl:stylesheet version="2.0" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||
<xsl:template match="/">
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title><?= $page->metadata()->title()->escape() ?></title>
|
||||
<style>
|
||||
/* Document styles */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
|
||||
font-size: 0.875rem;
|
||||
color: #000;
|
||||
background-color: #f0f0f0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.k-sitemap-body {
|
||||
max-width: 64rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Font styles */
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.k-sitemap-body>p {
|
||||
color: rgb(115, 115, 115);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(29, 84, 139);
|
||||
}
|
||||
|
||||
.k-sitemap-credits {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
table {
|
||||
font-size: 0.875rem;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px 0 #0000000d, 0 1px 2px 0 #00000006;
|
||||
}
|
||||
|
||||
.k-sitemap-table-empty {
|
||||
box-shadow: none;
|
||||
border: rgb(204, 204, 204) 1px dashed;
|
||||
height: 2.25rem;
|
||||
padding: 0 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
color: rgb(104, 104, 104);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-family: "SFMono-Regular", Consolas, Liberation Mono, Menlo, Courier, monospace;
|
||||
color: rgb(115 115 115);
|
||||
background: rgb(250 250 250);
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
th:not(:last-child),
|
||||
td:not(:last-child) {
|
||||
border-inline-end: 1px solid rgb(240 240 240);
|
||||
}
|
||||
|
||||
th,
|
||||
tr:not(:last-child) td {
|
||||
border-block-end: 1px solid rgb(240 240 240);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
height: 2.25rem;
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
td {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 48rem) {
|
||||
.k-sitemap-secondary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="k-sitemap-body">
|
||||
<h1><?= $page->title() ?></h1>
|
||||
<p><?= t('sitemap-description') ?></p>
|
||||
|
||||
<xsl:if test="count(sitemap:sitemapindex/sitemap:sitemap) > 0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="66%"><?= t('sitemap') ?></th>
|
||||
<th width="33%" class="k-sitemap-secondary"><?= t('sitemap-last-updated') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<xsl:for-each select="sitemap:sitemapindex/sitemap:sitemap">
|
||||
<tr>
|
||||
<td>
|
||||
<xsl:variable name="link">
|
||||
<xsl:value-of select="sitemap:loc" />
|
||||
</xsl:variable>
|
||||
<a href="{$link}"><xsl:value-of select="sitemap:loc" /></a>
|
||||
</td>
|
||||
<td class="k-sitemap-secondary">
|
||||
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)))" />
|
||||
</td>
|
||||
</tr>
|
||||
</xsl:for-each>
|
||||
</tbody>
|
||||
</table>
|
||||
</xsl:if>
|
||||
|
||||
<xsl:if test="count(sitemap:urlset/sitemap:url) > 0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="55%"><?= t('sitemap-url') ?></th>
|
||||
<th width="10%" class="k-sitemap-secondary"><?= t('sitemap-priority') ?></th>
|
||||
<th width="15%" class="k-sitemap-secondary"><?= t('sitemap-changefreq') ?></th>
|
||||
<th width="20%" class="k-sitemap-secondary"><?= t('sitemap-last-updated') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<xsl:for-each select="sitemap:urlset/sitemap:url">
|
||||
<tr>
|
||||
<td>
|
||||
<xsl:variable name="link">
|
||||
<xsl:value-of select="sitemap:loc" />
|
||||
</xsl:variable>
|
||||
<a target="_blank" rel="noopener nofollow" href="{$link}"><xsl:value-of select="sitemap:loc" /></a>
|
||||
</td>
|
||||
<td class="k-sitemap-secondary">
|
||||
<xsl:value-of select="sitemap:priority" />
|
||||
</td>
|
||||
<td class="k-sitemap-secondary">
|
||||
<xsl:value-of select="sitemap:changefreq" />
|
||||
</td>
|
||||
<td class="k-sitemap-secondary">
|
||||
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)))" />
|
||||
</td>
|
||||
</tr>
|
||||
</xsl:for-each>
|
||||
</tbody>
|
||||
</table>
|
||||
</xsl:if>
|
||||
|
||||
<xsl:if test="(sitemap:urlset and count(sitemap:urlset/sitemap:url) = 0) or (sitemap:sitemapindex and count(sitemap:sitemapindex/sitemap:sitemap) = 0)">
|
||||
<div class="k-sitemap-table-empty">
|
||||
<?= t('sitemap-no-entries') ?>
|
||||
</div>
|
||||
</xsl:if>
|
||||
|
||||
<p class="k-sitemap-credits">
|
||||
<a target="_blank" rel="noopener nofollow" href="https://getkirby.com/plugins/tobimori/seo">Kirby SEO</a>
|
||||
<xsl:if test="sitemap:urlset">
|
||||
v<xsl:value-of select="sitemap:urlset/@seo-version" />
|
||||
</xsl:if>
|
||||
<xsl:if test="sitemap:sitemapindex">
|
||||
v<xsl:value-of select="sitemap:sitemapindex/@seo-version" />
|
||||
</xsl:if>
|
||||
<?= t('sitemap-by') ?> <a target="_blank" rel="noopener nofollow" href="https://moeritz.io/">Tobias Möritz</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
87
site/plugins/kirby-seo/translations/de.yml
Normal file
87
site/plugins/kirby-seo/translations/de.yml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Heading Structure
|
||||
heading-structure: Überschriftenstruktur
|
||||
incorrect-heading-order: Deine Überschriftenstruktur hat eine falsche Abfolge und ist ungültig.
|
||||
missing-h1-tag: Deine Überschriftenstruktur enthält keine H1 und ist ungültig.
|
||||
multiple-h1-tags: Deine Überschriftenstruktur enthält mehr als eine H1 und ist ungültig.
|
||||
|
||||
# SEO Preview
|
||||
seo-preview: Vorschau
|
||||
seo-preview-for: Zeige mir
|
||||
open-debugger: Sharing-Debugger öffnen
|
||||
open-search-console: Search Console öffnen
|
||||
|
||||
# Robots
|
||||
indicator-index: Indexierung erlaubt
|
||||
indicator-any: Indexierung teilw. verboten
|
||||
indicator-noindex: Indexierung verboten
|
||||
robots: Richtlinien für Suchmaschinen & Crawler
|
||||
robots-index: Indexierung
|
||||
robots-index-help: Ob Suchmaschinen die Seite indexieren dürfen.
|
||||
robots-follow: Links folgen
|
||||
robots-follow-help: Ob Suchmaschinen Links auf dieser Seite folgen dürfen.
|
||||
robots-archive: Archivierung
|
||||
robots-archive-help: Ob Suchmaschinen zwischengespeicherte Versionen der Seite ausliefern dürfen.
|
||||
robots-imageindex: Bilder-Indexierung
|
||||
robots-imageindex-help: Ob Bilder dieser Seite in der Bildersuche angezeigt werden dürfen.
|
||||
robots-snippet: Snippets
|
||||
robots-snippet-help: Ob Suchmaschinen Textausschnitte aus der Seite anzeigen dürfen.
|
||||
|
||||
# Blueprint - Site/Common
|
||||
metadata-site: Metadaten & SEO
|
||||
global-meta-headline: Globale SEO-Einstellungen
|
||||
global-meta-headline-help: |
|
||||
Diese Einstellungen werden für alle Seiten verwendet, die keine eigenen Metadaten haben.
|
||||
Du kannst sie für jede Seite überschreiben.
|
||||
meta-title-template: Titel-Template
|
||||
meta-title-template-help: |
|
||||
Ein Template, das für alle Seitentitel verwendet werden soll.
|
||||
"\{\{ title }}" wird durch den Titel der Seite ersetzt.
|
||||
meta-description: Seitenbeschreibung
|
||||
meta-description-help: Empfohlene Länge von max. 150 Zeichen. Wird verwendet, falls keine Seitenbeschreibung angegeben ist.
|
||||
|
||||
global-og-headline: Globale Open Graph-Einstellungen
|
||||
global-og-headline-help: Stelle ein, wie deine Website erscheint, wenn sie auf sozialen Netzwerken wie Facebook oder Twitter geteilt wird.
|
||||
og-title-template: Open Graph-Titel-Template
|
||||
og-description: Open Graph-Beschreibung
|
||||
og-site-name: Open Graph-Seitenname
|
||||
og-image: Open Graph-Bild
|
||||
og-image-help: Empfohlene Größe von 1200x630 Pixeln.
|
||||
og-image-empty: Kein Open Graph-Bild ausgewählt
|
||||
twitter-card-type: Twitter-Kartentyp
|
||||
twitter-card-type-help: Der Twitter-Kartentyp bestimmt, wie deine Website aussieht, wenn sie auf Twitter geteilt wird. [Mehr Informationen](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards).
|
||||
|
||||
social-media-accounts: Social Media-Accounts
|
||||
social-media-accounts-help: URLs bzw. @-Handles zu deinen Social Media-Accounts. Werden verwendet, um Links zu deinen Accounts in den Metadaten zu setzen.
|
||||
|
||||
# Blueprint - Page
|
||||
meta-headline: SEO-Einstellungen
|
||||
og-headline: Open Graph-Einstellungen
|
||||
title-overwrite: Titel (Überschreiben)
|
||||
summary: Karte mit quadratischem Bild
|
||||
summary_large_image: Karte mit großem Bild
|
||||
default-select: 'Standard:'
|
||||
twitter-author: '@username des Autors auf Twitter'
|
||||
inherit-settings: Einstellungen vererben
|
||||
inherit-settings-help: |
|
||||
Wähle aus, welche Einstellungen an Unterseiten vererbt werden sollen.
|
||||
Dies 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.
|
||||
use-title-template: Titel-Template verwenden?
|
||||
use-title-template-no: 'Nein - reiner Titel'
|
||||
use-title-template-yes: 'Ja - mit Template'
|
||||
use-title-template-help: Gibt an, ob das Titel-Template verwendet werden soll. Wird nicht vererbt.
|
||||
|
||||
# Labels
|
||||
google: Google
|
||||
facebook: Facebook
|
||||
slack: Slack
|
||||
|
||||
# Sitemap
|
||||
sitemap: 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-last-updated: Letzte Änderung
|
||||
sitemap-priority: Priorität
|
||||
sitemap-url: URL
|
||||
sitemap-no-entries: Keine Einträge
|
||||
87
site/plugins/kirby-seo/translations/en.yml
Normal file
87
site/plugins/kirby-seo/translations/en.yml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Heading Structure
|
||||
heading-structure: Heading Structure
|
||||
incorrect-heading-order: Your heading structure has an incorrect order and is invalid.
|
||||
missing-h1-tag: Your heading structure does not contain an H1 and is invalid.
|
||||
multiple-h1-tags: Your heading structure contains more than one H1 and is invalid.
|
||||
|
||||
# SEO Preview
|
||||
seo-preview: Preview
|
||||
seo-preview-for: Show me
|
||||
open-debugger: Open Sharing Debugger
|
||||
open-search-console: Open Search Console
|
||||
|
||||
# Robots
|
||||
indicator-index: Indexing allowed
|
||||
indicator-any: Indexing partly forbidden
|
||||
indicator-noindex: Indexing forbidden
|
||||
robots: Robots Directives
|
||||
robots-index: Indexing
|
||||
robots-index-help: Whether search engines may index this page.
|
||||
robots-follow: Follow Links
|
||||
robots-follow-help: Whether search engines may follow links on this page.
|
||||
robots-archive: Archive
|
||||
robots-archive-help: Whether search engines may archive this page.
|
||||
robots-imageindex: Image Indexing
|
||||
robots-imageindex-help: Whether search engines may index images on this page.
|
||||
robots-snippet: Snippets
|
||||
robots-snippet-help: Whether search engines may show text snippets of this page.
|
||||
|
||||
# Blueprint - Site/Common
|
||||
metadata-site: Metadata & SEO
|
||||
global-meta-headline: Global SEO Settings
|
||||
global-meta-headline-help: |
|
||||
These settings are used for all pages that do not have their own metadata.
|
||||
You can override them for each page.
|
||||
meta-title-template: Title Template
|
||||
meta-title-template-help: |
|
||||
A template to use for all page titles.
|
||||
"\{\{ title }}" will be replaced with the page title.
|
||||
meta-description: Page Description
|
||||
meta-description-help: Recommended length of 150 characters max. Used if no page description is specified.
|
||||
|
||||
global-og-headline: Global Open Graph Settings
|
||||
global-og-headline-help: Set how your website appears when shared on social networks like Facebook or Twitter.
|
||||
og-title-template: Open Graph Title Template
|
||||
og-description: Open Graph Description
|
||||
og-site-name: Open Graph Site Name
|
||||
og-image: Open Graph Image
|
||||
og-image-help: Recommended size of 1200x630 pixels.
|
||||
og-image-empty: No Open Graph Image selected
|
||||
twitter-card-type: Twitter Card Type
|
||||
twitter-card-type-help: Twitter Card Type determines how your website looks when shared on Twitter. [Read more](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards).
|
||||
|
||||
social-media-accounts: Social Media Accounts
|
||||
social-media-accounts-help: URLs or @handles to your social media accounts. Used to put links to your accounts in the metadata.
|
||||
|
||||
# Blueprint - Page
|
||||
meta-headline: SEO Settings
|
||||
og-headline: Open Graph Settings
|
||||
title-overwrite: Title (overwrite)
|
||||
summary: Summary with Square Image
|
||||
summary_large_image: Summary with Large Image
|
||||
default-select: 'Default:'
|
||||
twitter-author: 'Author @username on Twitter'
|
||||
inherit-settings: Inherit Settings
|
||||
inherit-settings-help: |
|
||||
Select which settings should be inherited by subpages.
|
||||
This 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.
|
||||
use-title-template: Use title template?
|
||||
use-title-template-no: 'No - only title'
|
||||
use-title-template-yes: 'Yes - with template'
|
||||
use-title-template-help: Specifies whether the title template should be used. Will not be inherited.
|
||||
|
||||
# Labels
|
||||
google: Google
|
||||
facebook: Facebook
|
||||
slack: Slack
|
||||
|
||||
# Sitemap
|
||||
sitemap: 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-last-updated: Last Updated
|
||||
sitemap-priority: Priority
|
||||
sitemap-url: URL
|
||||
sitemap-no-entries: No entries
|
||||
75
site/plugins/kirby-seo/translations/fr.yml
Normal file
75
site/plugins/kirby-seo/translations/fr.yml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Heading Structure
|
||||
heading-structure: Structure des titres
|
||||
incorrect-heading-order: Votre structure de titres a un ordre incorrect et est invalide.
|
||||
missing-h1-tag: Votre structure de titres ne contient pas de balise H1 et est invalide.
|
||||
multiple-h1-tags: Votre structure de titres contient plus d’une balise H1 et est invalide.
|
||||
|
||||
# SEO Preview
|
||||
seo-preview: Aperçu
|
||||
seo-preview-for: Montre-moi
|
||||
open-debugger: Ouvrir le débogueur de partage
|
||||
open-search-console: Ouvrir la Console de recherche
|
||||
|
||||
# Robots
|
||||
indicator-index: Indexation autorisée
|
||||
indicator-any: Indexation partiellement interdite
|
||||
indicator-noindex: Indexation interdite
|
||||
robots: Directives des robots
|
||||
robots-index: Indexation
|
||||
robots-index-help: Indique si les moteurs de recherche peuvent indexer cette page.
|
||||
robots-follow: Suivre les liens
|
||||
robots-follow-help: Indique si les moteurs de recherche peuvent suivre les liens de cette page.
|
||||
robots-archive: Archiver
|
||||
robots-archive-help: Indique si les moteurs de recherche peuvent archiver cette page.
|
||||
robots-imageindex: Indexation des images
|
||||
robots-imageindex-help: Indique si les moteurs de recherche peuvent indexer les images de cette page.
|
||||
robots-snippet: Extraits
|
||||
robots-snippet-help: Indique si les moteurs de recherche peuvent afficher des extraits de texte de cette page.
|
||||
|
||||
# Blueprint - Site/Common
|
||||
metadata-site: Métadonnées et SEO
|
||||
global-meta-headline: Paramètres SEO globaux
|
||||
global-meta-headline-help: |
|
||||
Ces paramètres sont utilisés pour toutes les pages qui n’ont pas leurs propres métadonnées.
|
||||
Vous pouvez les remplacer pour chaque page.
|
||||
meta-title-template: Modèle de titre
|
||||
meta-title-template-help: |
|
||||
Un modèle à utiliser pour tous les titres de page.
|
||||
"\{\{ title }}" sera remplacé par le titre de la page.
|
||||
meta-description: Description de la page
|
||||
meta-description-help: Longueur recommandée de 150 caractères maximum. Utilisée si aucune description de page n’est spécifiée.
|
||||
|
||||
global-og-headline: Paramètres globaux Open Graph
|
||||
global-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.
|
||||
og-title-template: Modèle de titre Open Graph
|
||||
og-description: Description Open Graph
|
||||
og-site-name: Nom du site Open Graph
|
||||
og-image: Image Open Graph
|
||||
og-image-help: Taille recommandée de 1200x630 pixels.
|
||||
og-image-empty: Aucune image Open Graph sélectionnée
|
||||
twitter-card-type: Type de carte Twitter
|
||||
twitter-card-type-help: Le type de carte Twitter détermine l’apparence de votre site web lorsqu’il est partagé sur Twitter. [En savoir plus](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards).
|
||||
|
||||
social-media-accounts: Comptes de réseaux sociaux
|
||||
social-media-accounts-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.
|
||||
# Blueprint - Page
|
||||
meta-headline: Paramètres SEO
|
||||
og-headline: Paramètres Open Graph
|
||||
title-overwrite: Titre (remplacement)
|
||||
summary: Résumé avec une image carrée
|
||||
summary_large_image: Résumé avec une grande image
|
||||
default-select: 'Par défaut :'
|
||||
twitter-author: 'Auteur @nom_utilisateur sur Twitter'
|
||||
inherit-settings: Hériter des paramètres
|
||||
inherit-settings-help: |
|
||||
Sélectionnez les paramètres à hériter par les sous-pages.
|
||||
Cela 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.
|
||||
use-title-template: Utiliser le modèle de titre ?
|
||||
use-title-template-no: 'Non - seulement le titre'
|
||||
use-title-template-yes: 'Oui - avec le modèle'
|
||||
use-title-template-help: Indique si le modèle de titre doit être utilisé. Ne sera pas hérité.
|
||||
|
||||
# Labels
|
||||
google: Google
|
||||
facebook: Facebook
|
||||
slack: Slack
|
||||
87
site/plugins/kirby-seo/translations/pt_PT.yml
Normal file
87
site/plugins/kirby-seo/translations/pt_PT.yml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Heading Structure
|
||||
heading-structure: Estrutura de Títulos
|
||||
incorrect-heading-order: A estrutura de títulos tem uma ordem incorrecta e é inválida.
|
||||
missing-h1-tag: A estrutura de títulos não contém uma tag H1 e é inválida.
|
||||
multiple-h1-tags: A estrutura de títulos contém mais do que uma tag H1 e é inválida.
|
||||
|
||||
# SEO Preview
|
||||
seo-preview: Pré-visualização
|
||||
seo-preview-for: Mostrar
|
||||
open-debugger: Abrir Sharing Debugger
|
||||
open-search-console: Abrir Search Console
|
||||
|
||||
# Robots
|
||||
indicator-index: Indexação permitida
|
||||
indicator-any: Indexação parcialmente proibida
|
||||
indicator-noindex: Indexação proibida
|
||||
robots: Diretivas Robots
|
||||
robots-index: Indexação
|
||||
robots-index-help: Se os motores de pesquisa podem indexar esta página.
|
||||
robots-follow: Seguir Links
|
||||
robots-follow-help: Se os motores de pesquisa podem seguir links nesta página.
|
||||
robots-archive: Arquivo
|
||||
robots-archive-help: Se os motores de pesquisa podem arquivar esta página.
|
||||
robots-imageindex: Indexação de Imagens
|
||||
robots-imageindex-help: Se os motores de pesquisa podem indexar imagens desta página.
|
||||
robots-snippet: Snippets
|
||||
robots-snippet-help: Se os motores de pesquisa podem mostrar snippets de texto desta página.
|
||||
|
||||
# Blueprint - Site/Common
|
||||
metadata-site: Metadados & SEO
|
||||
global-meta-headline: Configurações Globais SEO
|
||||
global-meta-headline-help: |
|
||||
Estas configurações são usadas para todas as páginas que não têm os seus próprios metadados.
|
||||
Pode substituí-las em cada página.
|
||||
meta-title-template: Template de Título
|
||||
meta-title-template-help: |
|
||||
Template para usar em todos os títulos de páginas.
|
||||
"\{\{ title }}" será substituído pelo título da página.
|
||||
meta-description: Descrição de Página
|
||||
meta-description-help: Recomendado um tamanho de 150 caracteres no máximo. Usada se nenhuma descrição de página for especificada.
|
||||
|
||||
global-og-headline: Configurações Globais Open Graph
|
||||
global-og-headline-help: Defina como o seu site aparece quando é partilhado em redes sociais como o Facebook ou o Twitter.
|
||||
og-title-template: Template de Título Open Graph
|
||||
og-description: Descrição Open Graph
|
||||
og-site-name: Nome do Site Open Graph
|
||||
og-image: Imagem Open Graph
|
||||
og-image-help: Tamanho recomendado de 1200x630 pixels.
|
||||
og-image-empty: Nenhuma Imagem Open Graph selecionada
|
||||
twitter-card-type: Tipo de Twitter Card
|
||||
twitter-card-type-help: O tipo de Twitter Card determina a aparência do seu site quando é partilhado no Twitter. [Leia mais](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards).
|
||||
|
||||
social-media-accounts: Contas de Redes Sociais
|
||||
social-media-accounts-help: URLs ou @handles para as suas contas de redes sociais. Usado para colocar links para as suas contas nos metadados.
|
||||
|
||||
# Blueprint - Page
|
||||
meta-headline: Configurações SEO
|
||||
og-headline: Configurações Open Graph
|
||||
title-overwrite: Título (substituir)
|
||||
summary: Resumo com Imagem Quadrada
|
||||
summary_large_image: Resumo com Imagem Grande
|
||||
default-select: 'Por defeito:'
|
||||
twitter-author: 'Autor @username no Twitter'
|
||||
inherit-settings: Herdar Configurações
|
||||
inherit-settings-help: |
|
||||
Selecione quais as configurações que devem ser herdadas pelas subpáginas.
|
||||
Isto 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.
|
||||
use-title-template: Usar o template de título?
|
||||
use-title-template-no: 'Não - apenas o título'
|
||||
use-title-template-yes: 'Sim - com template'
|
||||
use-title-template-help: Especifica se o template de título deve ser usado. Não será herdado.
|
||||
|
||||
# Labels
|
||||
google: Google
|
||||
facebook: Facebook
|
||||
slack: Slack
|
||||
|
||||
# Sitemap
|
||||
sitemap: 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-last-updated: Última Atualização
|
||||
sitemap-priority: Prioridade
|
||||
sitemap-url: URL
|
||||
sitemap-no-entries: Sem registos
|
||||
87
site/plugins/kirby-seo/translations/tr.yml
Normal file
87
site/plugins/kirby-seo/translations/tr.yml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Başlık Yapısı
|
||||
heading-structure: Başlık Yapısı
|
||||
incorrect-heading-order: Başlık yapınız yanlış bir sıraya sahip ve geçersizdir.
|
||||
missing-h1-tag: Başlık yapınızda bir H1 etiketi bulunmuyor ve geçersizdir.
|
||||
multiple-h1-tags: Başlık yapınızda birden fazla H1 etiketi bulunuyor ve geçersizdir.
|
||||
|
||||
# SEO Önizleme
|
||||
seo-preview: Önizleme
|
||||
seo-preview-for: Bana göster
|
||||
open-debugger: Paylaşım Hata Ayıklayıcıyı Aç
|
||||
open-search-console: Arama Konsolunu Aç
|
||||
|
||||
# Robotlar
|
||||
indicator-index: İndekslemeye izin verildi
|
||||
indicator-any: İndeksleme kısmen yasaklandı
|
||||
indicator-noindex: İndeksleme yasaklandı
|
||||
robots: Robot Talimatları
|
||||
robots-index: İndeksleme
|
||||
robots-index-help: Arama motorlarının bu sayfayı indeksleyip indekslemeyeceğini belirtir.
|
||||
robots-follow: Bağlantıları Takip Et
|
||||
robots-follow-help: Arama motorlarının bu sayfadaki bağlantıları takip edip etmeyeceğini belirtir.
|
||||
robots-archive: Arşivle
|
||||
robots-archive-help: Arama motorlarının bu sayfayı arşivleyip arşivlemeyeceğini belirtir.
|
||||
robots-imageindex: İmaj İndeksleme
|
||||
robots-imageindex-help: Arama motorlarının bu sayfadaki görüntüleri indeksleyip indekslemeyeceğini belirtir.
|
||||
robots-snippet: Özetler
|
||||
robots-snippet-help: Arama motorlarının bu sayfanın metin özetlerini gösterip göstermeyeceğini belirtir.
|
||||
|
||||
# Şablona Dayalı - Site/Ortak
|
||||
metadata-site: Meta Verileri ve SEO
|
||||
global-meta-headline: Genel SEO Ayarları
|
||||
global-meta-headline-help: |
|
||||
Bu ayarlar, kendi meta verileri olmayan tüm sayfalar için kullanılır.
|
||||
Her sayfa için bunları geçersiz kılabilirsiniz.
|
||||
meta-title-template: Başlık Şablonu
|
||||
meta-title-template-help: |
|
||||
Tüm sayfa başlıkları için kullanılacak bir şablon.
|
||||
"\{\{ başlık }}" sayfa başlığı ile değiştirilecektir.
|
||||
meta-description: Sayfa Açıklaması
|
||||
meta-description-help: Maksimum 150 karakter uzunluğunda önerilen bir sayfa açıklamasıdır. Sayfa açıklaması belirtilmemişse kullanılır.
|
||||
|
||||
global-og-headline: Genel Açık Grafik (OG) Ayarları
|
||||
global-og-headline-help: Web sitenizin Facebook veya Twitter gibi sosyal ağlarda paylaşıldığında nasıl göründüğünü ayarlar.
|
||||
og-title-template: Açık Grafik (OG) Başlık Şablonu
|
||||
og-description: Açık Grafik (OG) Açıklaması
|
||||
og-site-name: Açık Grafik (OG) Site Adı
|
||||
og-image: Açık Grafik (OG) Görseli
|
||||
og-image-help: Önerilen boyut 1200x630 pikseldir.
|
||||
og-image-empty: Boş Açık Grafik (OG) Görseli
|
||||
twitter-card-type: Twitter Kart Türü
|
||||
twitter-card-type-help: Twitter Kart Türü, Twitter'da paylaşıldığında web sitenizin nasıl göründüğünü belirler. [Daha fazla bilgi](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards).
|
||||
|
||||
social-media-accounts: Sosyal Medya Hesapları
|
||||
social-media-accounts-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.
|
||||
|
||||
# Şablona Dayalı - Sayfa
|
||||
meta-headline: SEO Ayarları
|
||||
og-headline: Açık Grafik (OG) Ayarları
|
||||
title-overwrite: Başlık (üzerine yaz)
|
||||
summary: Kare İmaj ile Özet
|
||||
summary_large_image: Büyük İmaj ile Özet
|
||||
default-select: 'Varsayılan:'
|
||||
twitter-author: Twitter'da @kullanici_adi
|
||||
inherit-settings: Ayarları Miras Al
|
||||
inherit-settings-help: |
|
||||
Alt sayfalar tarafından miras alınacak ayarları seçin.
|
||||
Ö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.
|
||||
use-title-template: Başlık şablonunu kullan?
|
||||
use-title-template-no: 'Hayır - sadece başlık'
|
||||
use-title-template-yes: 'Evet - şablonla'
|
||||
use-title-template-help: Başlık şablonunun kullanılıp kullanılmayacağını belirtir. Miras alınmayacaktır.
|
||||
|
||||
# Etiketler
|
||||
google: Google
|
||||
facebook: Facebook
|
||||
slack: Slack
|
||||
|
||||
# Sitemap
|
||||
sitemap: 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-last-updated: Son Güncelleme
|
||||
sitemap-priority: Öncelik
|
||||
sitemap-url: URL
|
||||
sitemap-no-entries: Giriş yok
|
||||
|
|
@ -26,5 +26,6 @@
|
|||
<a href="#">Contacts et Mentions légales</a>
|
||||
</div>
|
||||
</footer>
|
||||
<?php snippet('seo/schemas'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<?php snippet('seo/head'); ?>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>
|
||||
<?= e($page->isHomePage() != true, $page->title() . ' - ') . $site->title() ?>
|
||||
</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/style.css?version-cache-prevent') . rand(0, 1000) ?>">
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
|
|
|
|||
1809
vendor/composer/autoload_classmap.php
vendored
1809
vendor/composer/autoload_classmap.php
vendored
File diff suppressed because it is too large
Load diff
4
vendor/composer/autoload_psr4.php
vendored
4
vendor/composer/autoload_psr4.php
vendored
|
|
@ -6,19 +6,21 @@ $vendorDir = dirname(__DIR__);
|
|||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'tobimori\\Seo\\' => array($baseDir . '/site/plugins/kirby-seo/classes'),
|
||||
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
|
||||
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
|
||||
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
|
||||
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
|
||||
'Spatie\\SchemaOrg\\' => array($vendorDir . '/spatie/schema-org/src'),
|
||||
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
|
||||
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
|
||||
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
|
||||
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
|
||||
'League\\ColorExtractor\\' => array($vendorDir . '/league/color-extractor/src'),
|
||||
'Laminas\\Escaper\\' => array($vendorDir . '/laminas/laminas-escaper/src'),
|
||||
'Kirby\\' => array($vendorDir . '/getkirby/composer-installer/src', $baseDir . '/kirby/src'),
|
||||
'Kirby\\' => array($baseDir . '/kirby/src', $vendorDir . '/getkirby/composer-installer/src'),
|
||||
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
|
||||
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
|
||||
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
|
||||
|
|
|
|||
1826
vendor/composer/autoload_static.php
vendored
1826
vendor/composer/autoload_static.php
vendored
File diff suppressed because it is too large
Load diff
136
vendor/composer/installed.json
vendored
136
vendor/composer/installed.json
vendored
|
|
@ -1371,6 +1371,82 @@
|
|||
},
|
||||
"install-path": "../ralouphie/getallheaders"
|
||||
},
|
||||
{
|
||||
"name": "spatie/schema-org",
|
||||
"version": "3.23.0",
|
||||
"version_normalized": "3.23.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/schema-org.git",
|
||||
"reference": "c95abf7da59f16570aec18ca11d861255e645c39"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/schema-org/zipball/c95abf7da59f16570aec18ca11d861255e645c39",
|
||||
"reference": "c95abf7da59f16570aec18ca11d861255e645c39",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.6",
|
||||
"graham-campbell/analyzer": "^3.0.5",
|
||||
"illuminate/collections": "^8.62.0",
|
||||
"league/flysystem": "^2.3.0 || ^3.0",
|
||||
"pestphp/pest": "^1.21",
|
||||
"symfony/console": "^5.3.7 || 6.0",
|
||||
"twig/twig": "^3.3.3"
|
||||
},
|
||||
"time": "2024-01-11T14:58:44+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\SchemaOrg\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian De Deyne",
|
||||
"email": "sebastian@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Tom Witkowski",
|
||||
"email": "dev.gummibeer@gmail.com",
|
||||
"homepage": "https://gummibeer.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A fluent builder Schema.org types and ld+json generator",
|
||||
"homepage": "https://github.com/spatie/schema-org",
|
||||
"keywords": [
|
||||
"schema-org",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/schema-org/issues",
|
||||
"source": "https://github.com/spatie/schema-org/tree/3.23.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"install-path": "../spatie/schema-org"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.5.0",
|
||||
|
|
@ -1851,6 +1927,66 @@
|
|||
}
|
||||
],
|
||||
"install-path": "../symfony/yaml"
|
||||
},
|
||||
{
|
||||
"name": "tobimori/kirby-seo",
|
||||
"version": "1.1.2",
|
||||
"version_normalized": "1.1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tobimori/kirby-seo.git",
|
||||
"reference": "a06eb676f699797fdd04a515149559ffd4746be7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tobimori/kirby-seo/zipball/a06eb676f699797fdd04a515149559ffd4746be7",
|
||||
"reference": "a06eb676f699797fdd04a515149559ffd4746be7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"getkirby/composer-installer": "^1.2.1",
|
||||
"php": ">=8.1.0",
|
||||
"spatie/schema-org": "^3.14"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.48",
|
||||
"getkirby/cli": "^1.2",
|
||||
"getkirby/cms": "^4.0"
|
||||
},
|
||||
"time": "2024-04-10T09:49:19+00:00",
|
||||
"type": "kirby-plugin",
|
||||
"extra": {
|
||||
"kirby-cms-path": false
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"tobimori\\Seo\\": "classes"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Tobias Möritz",
|
||||
"email": "tobias@moeritz.io"
|
||||
}
|
||||
],
|
||||
"description": "The ultimate Kirby SEO toolkit",
|
||||
"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/1.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/tobimori",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"install-path": "../../site/plugins/kirby-seo"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
|
|
|
|||
22
vendor/composer/installed.php
vendored
22
vendor/composer/installed.php
vendored
|
|
@ -3,7 +3,7 @@
|
|||
'name' => 'getkirby/plainkit',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '85869bdb48955612aab7bc6e320d8a3a705edfd0',
|
||||
'reference' => '23dbb65314d398cbc246b5c5539906c8c5bf6a1e',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
'getkirby/plainkit' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '85869bdb48955612aab7bc6e320d8a3a705edfd0',
|
||||
'reference' => '23dbb65314d398cbc246b5c5539906c8c5bf6a1e',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
@ -214,6 +214,15 @@
|
|||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'spatie/schema-org' => array(
|
||||
'pretty_version' => '3.23.0',
|
||||
'version' => '3.23.0.0',
|
||||
'reference' => 'c95abf7da59f16570aec18ca11d861255e645c39',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../spatie/schema-org',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/deprecation-contracts' => array(
|
||||
'pretty_version' => 'v3.5.0',
|
||||
'version' => '3.5.0.0',
|
||||
|
|
@ -274,5 +283,14 @@
|
|||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'tobimori/kirby-seo' => array(
|
||||
'pretty_version' => '1.1.2',
|
||||
'version' => '1.1.2.0',
|
||||
'reference' => 'a06eb676f699797fdd04a515149559ffd4746be7',
|
||||
'type' => 'kirby-plugin',
|
||||
'install_path' => __DIR__ . '/../../site/plugins/kirby-seo',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
8
vendor/spatie/schema-org/.styleci.yml
vendored
Normal file
8
vendor/spatie/schema-org/.styleci.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
preset: laravel
|
||||
|
||||
disabled:
|
||||
- single_class_element_per_statement
|
||||
|
||||
finder:
|
||||
exclude:
|
||||
- "src"
|
||||
328
vendor/spatie/schema-org/CHANGELOG.md
vendored
Normal file
328
vendor/spatie/schema-org/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
# Changelog
|
||||
|
||||
All Notable changes to `schema-org` will be documented in this file.
|
||||
|
||||
## 3.22.0 - 2024-01-11
|
||||
|
||||
### What's Changed
|
||||
|
||||
* upgrade to schema.org v23 by @Gummibeer in https://github.com/spatie/schema-org/pull/212
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.21.0...3.22.0
|
||||
|
||||
## 3.21.0 - 2024-01-11
|
||||
|
||||
### What's Changed
|
||||
|
||||
* upgrade to schema.org v22 by @Gummibeer in https://github.com/spatie/schema-org/pull/211
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.20.0...3.21.0
|
||||
|
||||
## 3.20.0 - 2024-01-11
|
||||
|
||||
### What's Changed
|
||||
|
||||
* upgrade to schema.org v21 by @Gummibeer in https://github.com/spatie/schema-org/pull/210
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.19.0...3.20.0
|
||||
|
||||
## 3.19.0 - 2024-01-11
|
||||
|
||||
### What's Changed
|
||||
|
||||
* upgrade to schema.org v20 by @Gummibeer in https://github.com/spatie/schema-org/pull/209
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.18.0...3.19.0
|
||||
|
||||
## 3.18.0 - 2024-01-11
|
||||
|
||||
### What's Changed
|
||||
|
||||
* upgrade to schema.org v19 by @Gummibeer in https://github.com/spatie/schema-org/pull/208
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.17.0...3.18.0
|
||||
|
||||
## 3.17.0 - 2024-01-11
|
||||
|
||||
### What's Changed
|
||||
|
||||
* upgrade to schema.org v18 by @Gummibeer in https://github.com/spatie/schema-org/pull/207
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.16.0...3.17.0
|
||||
|
||||
## 3.16.0 - 2024-01-11
|
||||
|
||||
### What's Changed
|
||||
|
||||
* upgrade to schema.org v17 by @Gummibeer in https://github.com/spatie/schema-org/pull/206
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.15.0...3.16.0
|
||||
|
||||
## 3.15.0 - 2024-01-11
|
||||
|
||||
### What's Changed
|
||||
|
||||
* upgrade to schema.org v16 by @Gummibeer in https://github.com/spatie/schema-org/pull/204
|
||||
* add support/test-run for php8.3 by @Gummibeer in https://github.com/spatie/schema-org/pull/205
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.14.0...3.15.0
|
||||
|
||||
## 3.14.0 - 2023-02-14
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Add PHP 8.2 Support by @patinthehat in https://github.com/spatie/schema-org/pull/190
|
||||
- Add support for nonce attribute by @smortexa in https://github.com/spatie/schema-org/pull/191
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @patinthehat made their first contribution in https://github.com/spatie/schema-org/pull/190
|
||||
- @smortexa made their first contribution in https://github.com/spatie/schema-org/pull/191
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.13.0...3.14.0
|
||||
|
||||
## 3.13.0 - 2022-11-14
|
||||
|
||||
### What's Changed
|
||||
|
||||
- add support for complex context by @Gummibeer in https://github.com/spatie/schema-org/pull/189
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.12.0...3.13.0
|
||||
|
||||
## 3.12.0 - 2022-11-14
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Update generator to use schema v15 by @osopolar in https://github.com/spatie/schema-org/pull/188
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @osopolar made their first contribution in https://github.com/spatie/schema-org/pull/188
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.11.0...3.12.0
|
||||
|
||||
## 3.11.0 - 2022-04-11
|
||||
|
||||
## What's Changed
|
||||
|
||||
- Rewrite tests to use pest instead of phpunit by @otsch in https://github.com/spatie/schema-org/pull/179
|
||||
- upgrade to schema:v14 by @Gummibeer in https://github.com/spatie/schema-org/pull/181
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @otsch made their first contribution in https://github.com/spatie/schema-org/pull/179
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/schema-org/compare/3.10.0...3.11.0
|
||||
|
||||
## 3.10.0 - 2022-03-04
|
||||
|
||||
- Drop PHP 7.4 Support - [#174](https://github.com/spatie/schema-org/pull/174)
|
||||
- Add PHP 8.1 Support - [#174](https://github.com/spatie/schema-org/pull/174)
|
||||
|
||||
## 3.10.0 - 2022-03-04
|
||||
|
||||
- Drop PHP 7.4 Support - [#174](https://github.com/spatie/schema-org/pull/174)
|
||||
- Add PHP 8.1 Support - [#174](https://github.com/spatie/schema-org/pull/174)
|
||||
|
||||
## 3.9.0 - 2022-02-10
|
||||
|
||||
- Add support for Laravel 9 - [#176](https://github.com/spatie/schema-org/pull/176)
|
||||
|
||||
## 3.8.0 - 2021-10-20
|
||||
|
||||
- Upgrade to schema.org:v13.0 - [#167](https://github.com/spatie/schema-org/pull/167)
|
||||
|
||||
## 3.7.0 - 2021-10-06
|
||||
|
||||
- Upgrade to schema.org:v12.0 - [#170](https://github.com/spatie/schema-org/pull/170)
|
||||
|
||||
## 3.6.0 - 2021-10-06
|
||||
|
||||
- Upgrade to schema.org:v11.01 - [#169](https://github.com/spatie/schema-org/pull/169)
|
||||
|
||||
## 3.5.0 - 2021-10-05
|
||||
|
||||
- Upgrade to schema.org:v10 - [#166](https://github.com/spatie/schema-org/pull/166)
|
||||
- Drop PHP 7.3 support - [#165](https://github.com/spatie/schema-org/pull/165)
|
||||
- Add PHP 8.1 support - [#165](https://github.com/spatie/schema-org/pull/165)
|
||||
|
||||
## 3.4.0 - 2021-05-10
|
||||
|
||||
- Add custom `\Spatie\SchemaOrg\Graph` context support - [#160](https://github.com/spatie/schema-org/pull/160)
|
||||
|
||||
## 3.3.0 - 2021-01-02
|
||||
|
||||
- Fix `@id` and `identifier` serialization for typed identifiers - [#157](https://github.com/spatie/schema-org/pull/157)
|
||||
|
||||
## 3.2.1 - 2020-11-28
|
||||
|
||||
- Add PHP 8.0 support
|
||||
|
||||
## 3.2.0 - 2020-11-05
|
||||
|
||||
- Add `\Spatie\SchemaOrg\ReferencedType` to reference types in schemas and graphs - [#155](https://github.com/spatie/schema-org/pull/155)
|
||||
|
||||
## 3.1.1 - 2020-10-22
|
||||
|
||||
- Fix empty string property values - [#153](https://github.com/spatie/schema-org/pull/153)
|
||||
|
||||
## 3.1.0 - 2020-10-07
|
||||
|
||||
- Add Multi-Typed-Entity support `\Spatie\SchemaOrg\MultiTypedEntity` - [#148](https://github.com/spatie/schema-org/pull/148)
|
||||
|
||||
## 3.0.0 - 2020-08-27
|
||||
|
||||
- Upgrade to schema.org:v9 which includes all extensions
|
||||
|
||||
## 2.16.0 - 2020-08-27
|
||||
|
||||
- Upgrade to schema.org:v8
|
||||
|
||||
## 2.15.0 - 2020-08-27
|
||||
|
||||
Generated files with new JSON-LD source - some methods have been dropped which aren't part of the corresponding types but were listed in old RDFa - it's not a breaking release because of the same schema.org version and changes only because of schema file inconsistencies.
|
||||
|
||||
## 2.14.1 - 2020-06-10
|
||||
|
||||
- Fix `\BadMethodCallException` message thrown in `\Spatie\SchemaOrg\Graph`
|
||||
- Fix `\Spatie\SchemaOrg\Graph` docblock `@method` annotations
|
||||
|
||||
## 2.14.0 - 2020-06-03
|
||||
|
||||
- Add identifiers to `\Spatie\SchemaOrg\Graph` nodes [#124](https://github.com/spatie/schema-org/pull/124)
|
||||
|
||||
## 2.13.0 - 2020-04-17
|
||||
|
||||
- RDFa update
|
||||
|
||||
## 2.12.1 - 2020-03-17
|
||||
|
||||
- Fix after malformed RDFa import [twitter.com/ScreamingDev](https://twitter.com/ScreamingDev/status/1239859471335243779)
|
||||
|
||||
## 2.12.0 - 2020-01-23
|
||||
|
||||
- RDFa update
|
||||
|
||||
## 2.11.1 - 2019-11-18
|
||||
|
||||
- Fix types to implement their own contract
|
||||
|
||||
## 2.11.0 - 2019-11-18
|
||||
|
||||
- Use contracts as type-hints in doc-blocks
|
||||
|
||||
## 2.10.0 - 2019-11-12
|
||||
|
||||
- Add PHP 7.4 support
|
||||
- Fix `Graph` magic call
|
||||
- RDFa update
|
||||
|
||||
## 2.9.0 - 2019-10-06
|
||||
|
||||
- Drop PHP 7.2 support
|
||||
|
||||
## 2.8.0 - 2019-10-06
|
||||
|
||||
- Drop PHP 7.1 support
|
||||
|
||||
## 2.7.0 - 2019-10-06
|
||||
|
||||
- Drop PHP 7.0 support
|
||||
|
||||
## 2.6.0 - 2019-09-26
|
||||
|
||||
- Fix identifier serialization
|
||||
|
||||
## 2.5.0 - 2019-09-25
|
||||
|
||||
- Add reflection support
|
||||
|
||||
## 2.4.0 - 2019-09-25
|
||||
|
||||
- RDFa update
|
||||
|
||||
## 2.3.0 - 2019-08-07
|
||||
|
||||
- Fix null values
|
||||
|
||||
## 2.2.1 - 2019-05-28
|
||||
|
||||
- Fix pending Types
|
||||
|
||||
## 2.2.0 - 2019-05-28
|
||||
|
||||
- RDFa update
|
||||
|
||||
## 2.1.0 - 2019-01-18
|
||||
|
||||
- Add `Graph` support
|
||||
|
||||
## 2.0.3 - 2018-10-18
|
||||
|
||||
- Convert invalid objects that have a `__toString` method to strings
|
||||
|
||||
## 2.0.2 - 2018-06-25
|
||||
|
||||
- Fix previous release
|
||||
|
||||
## 2.0.1 - 2018-06-25
|
||||
|
||||
- Use `https` protocol for Schema.org links
|
||||
|
||||
## 2.0.0 - 2018-06-08
|
||||
|
||||
- Supports multiple type inheritance. Types no longer extend each other, they all are a direct extension of `BaseType`
|
||||
- This release probably doesn't contain any breaking changes for most people. It breaks all type checks (e.g. `instanceof LocalBusiness`), but doesn't change the way types are generated
|
||||
|
||||
## 1.7.0 - 2018-06-08
|
||||
|
||||
- Regenerated types
|
||||
|
||||
## 1.6.0 - 2018-05-22
|
||||
|
||||
- Added: `Enumeration` child types with constants, e.g. `Spatie\Schema\DayOfWeek::Monday`.
|
||||
|
||||
## 1.5.0 - 2018-05-03
|
||||
|
||||
- Added: All types are now json serializable and have array access
|
||||
|
||||
## 1.4.2 - 2018-03-28
|
||||
|
||||
- Fixed: Date format in schemas is now ISO8601 (`DateTime::ATOM`)
|
||||
|
||||
## 1.4.1 - 2017-12-11
|
||||
|
||||
- Fixed: `@param` hints for numbers (`float|int`)
|
||||
|
||||
## 1.4.0 - 2017-09-26
|
||||
|
||||
- Added: `BaseType::addProperties` method to add multiple properties in one go
|
||||
- Fixed: `@param` hints now also have array versions of the accepted data types, like `string|string[]`
|
||||
|
||||
## 1.3.0 - 2017-05-08
|
||||
|
||||
- Added: `__call` catches unknown methods and calls `setProperty` under the hood, using the method name as the property name and the first argument as property value
|
||||
- Added (by updating generated types): `CreativeWork::accessMode`, `CreativeWork::accessModeSufficient`, `CreativeWork::accessibilitySummary`, `Event::audience`, `FoodEstablishment::hasMenu`
|
||||
- Removed (by updating generated types, note that this isn't breaking because `__call` will catch your method calls): `Menu::menuAddOn`
|
||||
|
||||
## 1.2.1 - 2017-02-20
|
||||
|
||||
- Fixed: New lines are considered and no longer break docs
|
||||
- Fixed: HTML line breaks are removed from the docs
|
||||
|
||||
## 1.2.0 - 2017-02-07
|
||||
|
||||
- Added: Objects that implements `DateTimeInterface` are now formatted to an ISO 8601 compliant string
|
||||
- Fixed: Some properties were missing due to some whitespace parsing issues
|
||||
|
||||
## 1.1.0 - 2017-01-03
|
||||
|
||||
- Added: `if` function to conditionally modify the schema
|
||||
|
||||
## 1.0.1 - 2017-01-03
|
||||
|
||||
- Fixed: Arrays of properties are now correctly serialized when converted to ld+json
|
||||
|
||||
## 1.0.0 - 2016-12-06
|
||||
|
||||
- Initial release
|
||||
21
vendor/spatie/schema-org/LICENSE.md
vendored
Normal file
21
vendor/spatie/schema-org/LICENSE.md
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Spatie bvba <info@spatie.be>
|
||||
|
||||
> 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.
|
||||
352
vendor/spatie/schema-org/README.md
vendored
Normal file
352
vendor/spatie/schema-org/README.md
vendored
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
|
||||
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/support-ukraine.svg?t=1" />](https://supportukrainenow.org)
|
||||
|
||||
# A Fluent Builder For Schema.org Types And ld+json Generator
|
||||
|
||||
[](https://packagist.org/packages/spatie/schema-org)
|
||||
[](LICENSE.md)
|
||||
[](https://github.com/spatie/schema-org/actions?query=workflow%3Arun-tests)
|
||||
[](https://styleci.io/repos/74684096)
|
||||
[](https://packagist.org/packages/spatie/schema-org)
|
||||
|
||||
`spatie/schema-org` provides a fluent builder for **all** Schema.org types and their properties. The code in `src` is generated from Schema.org's [JSON-LD standards file](https://raw.githubusercontent.com/schemaorg/schemaorg/main/data/releases/13.0/schemaorg-all-https.jsonld), so it provides objects and methods for the entire core vocabulary. The classes and methods are also fully documented as a quick reference.
|
||||
|
||||
```php
|
||||
use Spatie\SchemaOrg\Schema;
|
||||
|
||||
$localBusiness = Schema::localBusiness()
|
||||
->name('Spatie')
|
||||
->email('info@spatie.be')
|
||||
->contactPoint(Schema::contactPoint()->areaServed('Worldwide'));
|
||||
|
||||
echo $localBusiness->toScript();
|
||||
```
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https:\/\/schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"name": "Spatie",
|
||||
"email": "info@spatie.be",
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"areaServed": "Worldwide"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Support us
|
||||
|
||||
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/schema-org.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/schema-org)
|
||||
|
||||
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
|
||||
|
||||
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
|
||||
|
||||
## Installation
|
||||
|
||||
You can install the package via composer:
|
||||
|
||||
``` bash
|
||||
composer require spatie/schema-org
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
All types can be instantiated through the `Spatie\SchemaOrg\Schema` factory class, or with the `new` keyword.
|
||||
|
||||
``` php
|
||||
$localBusiness = Schema::localBusiness()->name('Spatie');
|
||||
|
||||
// Is equivalent to:
|
||||
|
||||
$localBusiness = new LocalBusiness();
|
||||
$localBusiness->name('Spatie');
|
||||
```
|
||||
|
||||
> *All types also accept arrays of the expected data type, for example `sameAs` accepts a string or an array of strings.*
|
||||
|
||||
All types also implement the SPL's `ArrayAccess` for accessing the properties via array notation:
|
||||
|
||||
```php
|
||||
$anotherLocalBusiness = new LocalBusiness();
|
||||
var_dump(isset($anotherLocalBusiness['name'])); // => false
|
||||
$anotherLocalBusiness['name'] = 'Spatie';
|
||||
var_dump(isset($anotherLocalBusiness['name'])); // => true
|
||||
var_dump($anotherLocalBusiness['name']); // => 'Spatie'
|
||||
unset($anotherLocalBusiness['name']);
|
||||
var_dump(isset($anotherLocalBusiness['name'])); // => false
|
||||
```
|
||||
|
||||
Types can be converted to an array or rendered to a script.
|
||||
|
||||
```php
|
||||
$localBusiness->toArray();
|
||||
|
||||
echo $localBusiness->toScript();
|
||||
|
||||
echo $localBusiness; // Same output as `toScript()`
|
||||
```
|
||||
|
||||
Additionally, all types can be converted to a plain JSON string by just calling `json_encode()` with your object:
|
||||
|
||||
```php
|
||||
echo json_encode($localBusiness);
|
||||
```
|
||||
|
||||
I recommend double checking your structured data with [Google's structured data testing tool](https://search.google.com/structured-data/testing-tool).
|
||||
|
||||
### Enumerations
|
||||
|
||||
As of v1.6.0, all [Enumeration](https://schema.org/Enumeration) child types are available as classes with constants.
|
||||
|
||||
```php
|
||||
Schema::book()->bookFormat(Spatie\SchemaOrg\BookFormatType::Hardcover);
|
||||
```
|
||||
|
||||
There's no full API documentation for types and properties. You can refer to [the source](https://github.com/spatie/schema-org/tree/master/src) or to [the schema.org website](https://schema.org).
|
||||
|
||||
If you don't want to break the chain of a large schema object, you can use the `if` method to conditionally modify the schema.
|
||||
|
||||
```php
|
||||
use Spatie\SchemaOrg\LocalBusiness;
|
||||
use Spatie\SchemaOrg\Schema;
|
||||
|
||||
$business = ['name' => 'Spatie'];
|
||||
|
||||
$localBusiness = Schema::localBusiness()
|
||||
->name($business['name'])
|
||||
->if(isset($business['email']), function (LocalBusiness $schema) use ($business) {
|
||||
$schema->email($business['email']);
|
||||
});
|
||||
```
|
||||
|
||||
### Identifier
|
||||
|
||||
As of v2.6.0 the `identifier` key is replaced by `@id` for simple string identifiers. This is due to the definition for the `ld+json` syntax.
|
||||
|
||||
> All schema.org syntaxes already have built-in representation for URIs and URLs, e.g. in Microdata 'itemid', in RDFa 1.1, 'resource', **in JSON-LD, '@id'.**
|
||||
>
|
||||
> — [schema.org/docs](https://schema.org/docs/datamodel.html#identifierBg) // [PR#102](https://github.com/spatie/schema-org/pull/102) // [PR#157](https://github.com/spatie/schema-org/pull/157)
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
If you'd need to set a custom property, you can use the `setProperty` method.
|
||||
|
||||
```php
|
||||
$localBusiness->setProperty('foo', 'bar');
|
||||
```
|
||||
|
||||
If you'd need to retrieve a property, you can use the `getProperty` method. You can optionally pass in a second parameter to provide a default value.
|
||||
|
||||
```php
|
||||
$localBusiness->getProperty('name'); // 'Spatie'
|
||||
$localBusiness->getProperty('bar'); // null
|
||||
$localBusiness->getProperty('bar', 'baz'); // 'baz'
|
||||
```
|
||||
|
||||
All properties can be retrieved as an array with the `getProperties` method.
|
||||
|
||||
```php
|
||||
$localBusiness->getProperties(); // ['name' => 'Spatie', ...]
|
||||
```
|
||||
|
||||
Multiple properties can be set at once using the `addProperties` method.
|
||||
|
||||
```php
|
||||
$localBusiness->addProperties(['name' => 'value', 'foo' => 'bar']);
|
||||
```
|
||||
|
||||
Context and type can be retrieved with the `getContext` and `getType` methods.
|
||||
|
||||
```php
|
||||
$localBusiness->getContext(); // 'https://schema.org'
|
||||
$localBusiness->getType(); // 'LocalBusiness'
|
||||
```
|
||||
|
||||
### Graph - multiple items
|
||||
|
||||
The Graph has a lot of methods and utilities - the type-safe and simplest way is to use the overloaded methods of the `Spatie\SchemaOrg\Schema` class itself. These methods will get an already created or new instance of the requested schema.
|
||||
|
||||
```php
|
||||
$graph = new Graph();
|
||||
|
||||
// Create a product and prelink organization
|
||||
$graph
|
||||
->product()
|
||||
->name('My cool Product')
|
||||
->brand($graph->organization());
|
||||
|
||||
// Hide the organization from the created script tag
|
||||
$graph->hide(\Spatie\SchemaOrg\Organization::class);
|
||||
|
||||
// Somewhere else fill out the organization
|
||||
$graph
|
||||
->organization()
|
||||
->name('My awesome Company');
|
||||
|
||||
// Render graph to script tag
|
||||
echo $graph;
|
||||
```
|
||||
|
||||
With these tools the graph is a collection of all available schemas, can link these schemas with each other and prevent helper schemas from being rendered in the script-tag.
|
||||
|
||||
### Graph Node identifiers
|
||||
|
||||
Sometimes you have to keep track of multiple Graph nodes of the same type - for example multiple `Person` nodes for different people in your Organization.
|
||||
To do so you are able to use node identifiers on your graph instance.
|
||||
If you don't provide an identifier a reserved keyword `default` identifier will be used.
|
||||
|
||||
```php
|
||||
use Spatie\SchemaOrg\Graph;
|
||||
use Spatie\SchemaOrg\Person;
|
||||
|
||||
$graph = new Graph();
|
||||
|
||||
// add a Person using chaining
|
||||
$graph->person('freekmurze')
|
||||
->givenName('Freek')
|
||||
->familyName('Van der Herten')
|
||||
->alternateName('freekmurze');
|
||||
|
||||
// add a Person using closure
|
||||
$graph->person('sebastiandedeyne', function(Person $sebastian, Graph $graph): void {
|
||||
$sebastian
|
||||
->givenName('Sebastian')
|
||||
->familyName('De Deyne')
|
||||
->alternateName('sebastiandedeyne');
|
||||
});
|
||||
|
||||
// add a person using closure and second call with same identifier
|
||||
$graph->person(
|
||||
'gummibeer',
|
||||
fn(Person $gummibeer) => $gummibeer->alternateName('gummibeer')
|
||||
);
|
||||
$graph->person('gummibeer')
|
||||
->givenName('Tom')
|
||||
->familyName('Witkowski');
|
||||
|
||||
$graph->person('random')->name('Random Person');
|
||||
|
||||
// hide the random person from Graph
|
||||
$graph->hide(Person::class, 'random');
|
||||
|
||||
echo json_encode($graph);
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"@context":"https:\/\/schema.org",
|
||||
"@graph":[
|
||||
{
|
||||
"@type":"Person",
|
||||
"givenName":"Freek",
|
||||
"familyName":"Van der Herten",
|
||||
"alternateName":"freekmurze"
|
||||
},
|
||||
{
|
||||
"@type":"Person",
|
||||
"givenName":"Sebastian",
|
||||
"familyName":"De Deyne",
|
||||
"alternateName":"sebastiandedeyne"
|
||||
},
|
||||
{
|
||||
"@type":"Person",
|
||||
"alternateName":"gummibeer",
|
||||
"givenName":"Tom",
|
||||
"familyName":"Witkowski"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Multi Typed Entities
|
||||
|
||||
Schema.org allows [multi typed entities](https://github.com/schemaorg/schemaorg/wiki/How-to-use-Multi-Typed-Entities-or-MTEs) - to use them with this package you can use the `MultiTypedEntity` class - which works similar to the graph.
|
||||
|
||||
```php
|
||||
$mte = new MultiTypedEntity();
|
||||
$mte->hotelRoom()->name('The Presidential Suite');
|
||||
$mte->product()->offers(
|
||||
Schema::offer()
|
||||
->name('One Night')
|
||||
->price(100000.00)
|
||||
->priceCurrency('USD')
|
||||
);
|
||||
$mte->product(function (Product $product) {
|
||||
$product->aggregateRating(
|
||||
Schema::aggregateRating()
|
||||
->bestRating(5)
|
||||
->worstRating(4)
|
||||
);
|
||||
});
|
||||
|
||||
echo json_encode($mte);
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"@context":"https:\/\/schema.org",
|
||||
"@type":[
|
||||
"HotelRoom",
|
||||
"Product"
|
||||
],
|
||||
"name":"The Presidential Suite",
|
||||
"offers":{
|
||||
"@type":"Offer",
|
||||
"name":"One Night",
|
||||
"price":100000,
|
||||
"priceCurrency":"USD"
|
||||
},
|
||||
"aggregateRating":{
|
||||
"@type":"AggregateRating",
|
||||
"bestRating":5,
|
||||
"worstRating":4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There isn't a real rule in place how the properties are merged. It only uses `array_merge()` behind the scenes. So you should avoid defining the same property on different types in the MTE or be sure that all properties hold the same value that it's not important which property is used at the end.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- The `Float` type isn't available since it's a reserved keyword in PHP
|
||||
- The `Physician` type isn't available since it extends a type from the `health` extension spec
|
||||
|
||||
## Changelog
|
||||
|
||||
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
|
||||
|
||||
## Testing
|
||||
|
||||
``` bash
|
||||
$ composer test
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## Security
|
||||
|
||||
If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker.
|
||||
|
||||
## Postcardware
|
||||
|
||||
You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.
|
||||
|
||||
Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium.
|
||||
|
||||
We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards).
|
||||
|
||||
## Credits
|
||||
|
||||
- [Sebastian De Deyne](https://github.com/sebastiandedeyne)
|
||||
- [Tom Witkowski](https://github.com/Gummibeer)
|
||||
- [All Contributors](../../contributors)
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
|
||||
59
vendor/spatie/schema-org/composer.json
vendored
Normal file
59
vendor/spatie/schema-org/composer.json
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "spatie/schema-org",
|
||||
"description": "A fluent builder Schema.org types and ld+json generator",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"spatie",
|
||||
"schema-org"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian De Deyne",
|
||||
"email": "sebastian@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Tom Witkowski",
|
||||
"email": "dev.gummibeer@gmail.com",
|
||||
"homepage": "https://gummibeer.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"homepage": "https://github.com/spatie/schema-org",
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.6",
|
||||
"graham-campbell/analyzer": "^3.0.5",
|
||||
"illuminate/collections": "^8.62.0",
|
||||
"league/flysystem": "^2.3.0 || ^3.0",
|
||||
"pestphp/pest": "^1.21",
|
||||
"symfony/console": "^5.3.7 || 6.0",
|
||||
"twig/twig": "^3.3.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\SchemaOrg\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Spatie\\SchemaOrg\\Generator\\": "generator",
|
||||
"Spatie\\SchemaOrg\\Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"scripts": {
|
||||
"fix": "@php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes",
|
||||
"generate": "@php ./generate.php",
|
||||
"test": "@php vendor/bin/pest"
|
||||
}
|
||||
}
|
||||
12
vendor/spatie/schema-org/generate.php
vendored
Normal file
12
vendor/spatie/schema-org/generate.php
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (file_exists(__DIR__.'/vendor/autoload.php')) {
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
} else {
|
||||
require __DIR__.'/../../autoload.php';
|
||||
}
|
||||
|
||||
$application = new Spatie\SchemaOrg\Generator\Console\Application();
|
||||
|
||||
$application->run();
|
||||
21
vendor/spatie/schema-org/generator/Console/Application.php
vendored
Normal file
21
vendor/spatie/schema-org/generator/Console/Application.php
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Console;
|
||||
|
||||
use Symfony\Component\Console\Application as ConsoleApplication;
|
||||
|
||||
class Application extends ConsoleApplication
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Spatie\SchemaOrg package generator', '1.0.0');
|
||||
|
||||
$this->add(new GenerateCommand());
|
||||
$this->setDefaultCommand('generate', true);
|
||||
}
|
||||
|
||||
public function getLongVersion()
|
||||
{
|
||||
return parent::getLongVersion().' by <comment>Spatie</comment>';
|
||||
}
|
||||
}
|
||||
47
vendor/spatie/schema-org/generator/Console/GenerateCommand.php
vendored
Normal file
47
vendor/spatie/schema-org/generator/Console/GenerateCommand.php
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Console;
|
||||
|
||||
use Spatie\SchemaOrg\Generator\Definitions;
|
||||
use Spatie\SchemaOrg\Generator\PackageGenerator;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class GenerateCommand extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('generate')
|
||||
->setDescription('Generate the package code from the schema.org docs')
|
||||
->addOption('local', 'l', InputOption::VALUE_NONE, 'Use a cached version of the source');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Symfony\Component\Console\Input\InputInterface $input
|
||||
* @param \Symfony\Component\Console\Output\OutputInterface $output
|
||||
* @return int
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$output->writeln('Generating package code...');
|
||||
|
||||
$generator = new PackageGenerator();
|
||||
|
||||
$definitions = new Definitions([
|
||||
'core' => 'https://raw.githubusercontent.com/schemaorg/schemaorg/main/data/releases/24.0/schemaorg-all-https.jsonld',
|
||||
]);
|
||||
|
||||
if (! $input->getOption('local')) {
|
||||
$definitions->preload();
|
||||
}
|
||||
|
||||
$generator->generate($definitions);
|
||||
|
||||
$output->writeln('Done!');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
18
vendor/spatie/schema-org/generator/Constant.php
vendored
Normal file
18
vendor/spatie/schema-org/generator/Constant.php
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator;
|
||||
|
||||
class Constant
|
||||
{
|
||||
public string $name;
|
||||
|
||||
public string $description;
|
||||
|
||||
public string $value;
|
||||
|
||||
public string $type;
|
||||
|
||||
public ?string $partOf;
|
||||
|
||||
public ?string $source;
|
||||
}
|
||||
50
vendor/spatie/schema-org/generator/Definitions.php
vendored
Normal file
50
vendor/spatie/schema-org/generator/Definitions.php
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use OutOfBoundsException;
|
||||
use Spatie\SchemaOrg\Generator\Parser\JsonLdParser;
|
||||
|
||||
class Definitions
|
||||
{
|
||||
protected array $sources;
|
||||
|
||||
protected string $tempDir = __DIR__.'/temp';
|
||||
|
||||
public function __construct(array $sources)
|
||||
{
|
||||
$this->sources = $sources;
|
||||
}
|
||||
|
||||
public function preload(): void
|
||||
{
|
||||
foreach ($this->sources as $sourceId => $sourcePath) {
|
||||
$this->loadSource($sourceId, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function query(string $selector): Collection
|
||||
{
|
||||
return (new JsonLdParser($this->loadSource('core')))->filter($selector);
|
||||
}
|
||||
|
||||
protected function loadSource(string $sourceId, bool $fromCache = true): string
|
||||
{
|
||||
if (! isset($this->sources[$sourceId])) {
|
||||
throw new OutOfBoundsException("Source `{$sourceId}` doesn't exist");
|
||||
}
|
||||
|
||||
$cachePath = $this->tempDir.'/'.$sourceId.'.jsonld';
|
||||
|
||||
if ($fromCache && file_exists($cachePath)) {
|
||||
return file_get_contents($cachePath);
|
||||
}
|
||||
|
||||
$jsonLd = file_get_contents($this->sources[$sourceId]);
|
||||
|
||||
file_put_contents($cachePath, $jsonLd);
|
||||
|
||||
return $jsonLd;
|
||||
}
|
||||
}
|
||||
27
vendor/spatie/schema-org/generator/PackageGenerator.php
vendored
Normal file
27
vendor/spatie/schema-org/generator/PackageGenerator.php
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator;
|
||||
|
||||
use Spatie\SchemaOrg\Generator\Parser\DefinitionParser;
|
||||
use Spatie\SchemaOrg\Generator\Writer\Filesystem;
|
||||
|
||||
class PackageGenerator
|
||||
{
|
||||
public function generate(Definitions $definitions)
|
||||
{
|
||||
$types = (new DefinitionParser())->parse($definitions);
|
||||
|
||||
$filesystem = new Filesystem(__DIR__.'/..');
|
||||
|
||||
$filesystem->clear();
|
||||
|
||||
$filesystem->cloneStaticFiles();
|
||||
|
||||
$types->each(function (Type $type) use ($filesystem, $types) {
|
||||
$type->setTypeCollection($types);
|
||||
$filesystem->createType($type);
|
||||
});
|
||||
|
||||
$filesystem->createBuilderClass($types);
|
||||
}
|
||||
}
|
||||
55
vendor/spatie/schema-org/generator/Parser/DefinitionParser.php
vendored
Normal file
55
vendor/spatie/schema-org/generator/Parser/DefinitionParser.php
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Parser;
|
||||
|
||||
use Spatie\SchemaOrg\Generator\Constant;
|
||||
use Spatie\SchemaOrg\Generator\Definitions;
|
||||
use Spatie\SchemaOrg\Generator\Parser\Tasks\ParseConstant;
|
||||
use Spatie\SchemaOrg\Generator\Parser\Tasks\ParseProperty;
|
||||
use Spatie\SchemaOrg\Generator\Parser\Tasks\ParseType;
|
||||
use Spatie\SchemaOrg\Generator\Property;
|
||||
use Spatie\SchemaOrg\Generator\Type;
|
||||
use Spatie\SchemaOrg\Generator\TypeCollection;
|
||||
|
||||
class DefinitionParser
|
||||
{
|
||||
public function parse(Definitions $definitions): TypeCollection
|
||||
{
|
||||
$types = $definitions
|
||||
->query('rdfs:Class')
|
||||
->map(static function (array $jsonLdArray): ?Type {
|
||||
return call_user_func(ParseType::fromJsonLdArray($jsonLdArray));
|
||||
})
|
||||
->filter();
|
||||
|
||||
$properties = $definitions
|
||||
->query('rdf:Property')
|
||||
->map(static function (array $jsonLdArray): ?Property {
|
||||
return call_user_func(ParseProperty::fromJsonLdArray($jsonLdArray));
|
||||
})
|
||||
->filter();
|
||||
|
||||
$constants = collect([]);
|
||||
|
||||
foreach ($types as $type) {
|
||||
$constants = $constants->merge(
|
||||
$definitions
|
||||
->query($type->resource)
|
||||
->map(static function (array $constant) use ($type): ?Constant {
|
||||
$constant = call_user_func(ParseConstant::fromJsonLdArray($constant));
|
||||
|
||||
if (is_null($constant)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$constant->type = $type->name;
|
||||
|
||||
return $constant;
|
||||
})
|
||||
->filter()
|
||||
);
|
||||
}
|
||||
|
||||
return new TypeCollection($types, $properties, $constants);
|
||||
}
|
||||
}
|
||||
23
vendor/spatie/schema-org/generator/Parser/JsonLdParser.php
vendored
Normal file
23
vendor/spatie/schema-org/generator/Parser/JsonLdParser.php
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Parser;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class JsonLdParser
|
||||
{
|
||||
protected Collection $jsonLdGraph;
|
||||
|
||||
public function __construct(string $rawJsonLd)
|
||||
{
|
||||
$parsedJson = json_decode($rawJsonLd, true);
|
||||
$this->jsonLdGraph = collect($parsedJson['@graph']);
|
||||
}
|
||||
|
||||
public function filter(string $selector): Collection
|
||||
{
|
||||
return $this->jsonLdGraph->filter(static function ($schema) use ($selector): bool {
|
||||
return array_key_exists('@type', $schema) && $schema['@type'] === $selector;
|
||||
});
|
||||
}
|
||||
}
|
||||
29
vendor/spatie/schema-org/generator/Parser/Tasks/ParseConstant.php
vendored
Normal file
29
vendor/spatie/schema-org/generator/Parser/Tasks/ParseConstant.php
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Parser\Tasks;
|
||||
|
||||
use Spatie\SchemaOrg\Generator\Constant;
|
||||
|
||||
class ParseConstant extends Task
|
||||
{
|
||||
public function __invoke(): ?Constant
|
||||
{
|
||||
$constant = new Constant();
|
||||
|
||||
$constant->name = preg_replace('/\s+/', '_', $this->getDefinitionProperty('rdfs:label'));
|
||||
|
||||
if (empty($constant->name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$constant->description = $this->getDefinitionProperty('rdfs:comment');
|
||||
|
||||
$constant->value = $this->getResource();
|
||||
|
||||
$constant->partOf = $this->definition['schema:isPartOf']['@id'] ?? null;
|
||||
|
||||
$constant->source = $this->definition['schema:source']['@id'] ?? null;
|
||||
|
||||
return $constant;
|
||||
}
|
||||
}
|
||||
68
vendor/spatie/schema-org/generator/Parser/Tasks/ParseProperty.php
vendored
Normal file
68
vendor/spatie/schema-org/generator/Parser/Tasks/ParseProperty.php
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Parser\Tasks;
|
||||
|
||||
use Spatie\SchemaOrg\Generator\Property;
|
||||
|
||||
class ParseProperty extends Task
|
||||
{
|
||||
public function __invoke(): ?Property
|
||||
{
|
||||
$property = new Property();
|
||||
|
||||
$property->name = $this->getDefinitionProperty('rdfs:label');
|
||||
|
||||
if (empty($property->name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$property->description = $this->getDefinitionProperty('rdfs:comment');
|
||||
|
||||
$property->resource = $this->getResource();
|
||||
|
||||
$this->getWrappedDefinitionProperty('schema:domainIncludes')
|
||||
->each(function (array $domain) use ($property): void {
|
||||
$property->addType($this->getResourceName($domain));
|
||||
});
|
||||
|
||||
$this->getWrappedDefinitionProperty('schema:rangeIncludes')
|
||||
->each(function (array $range) use ($property): void {
|
||||
$property->addRanges(
|
||||
$this->castRangesToTypes($this->getResourceName($range))
|
||||
);
|
||||
});
|
||||
|
||||
$property->partOf = $this->definition['schema:isPartOf']['@id'] ?? null;
|
||||
|
||||
$property->source = $this->definition['schema:source']['@id'] ?? null;
|
||||
|
||||
return $property;
|
||||
}
|
||||
|
||||
private function castRangesToTypes(string $range): array
|
||||
{
|
||||
switch ($range) {
|
||||
case 'Boolean':
|
||||
return ['bool'];
|
||||
case 'False':
|
||||
return ['false'];
|
||||
case 'True':
|
||||
return ['true'];
|
||||
case 'Date':
|
||||
case 'Time':
|
||||
case 'DateTime':
|
||||
return ['\DateTimeInterface'];
|
||||
case 'Text':
|
||||
case 'URL':
|
||||
return ['string'];
|
||||
case 'Number':
|
||||
return ['float', 'int'];
|
||||
case 'Float':
|
||||
return ['float'];
|
||||
case 'Integer':
|
||||
return ['int'];
|
||||
default:
|
||||
return [$range];
|
||||
}
|
||||
}
|
||||
}
|
||||
48
vendor/spatie/schema-org/generator/Parser/Tasks/ParseType.php
vendored
Normal file
48
vendor/spatie/schema-org/generator/Parser/Tasks/ParseType.php
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Parser\Tasks;
|
||||
|
||||
use Spatie\SchemaOrg\Generator\Type;
|
||||
|
||||
class ParseType extends Task
|
||||
{
|
||||
public function __invoke(): ?Type
|
||||
{
|
||||
$type = new Type();
|
||||
|
||||
$type->className = $type->name = $this->getDefinitionProperty('rdfs:label');
|
||||
|
||||
if ($type->className === '3DModel') {
|
||||
$type->className = 'ThreeDimensionalModel';
|
||||
}
|
||||
|
||||
if (in_array($type->name, ['', 'DataType', 'Float', 'Integer', 'URL', 'Class'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$subClassOf = $this->getWrappedDefinitionProperty('rdfs:subClassOf');
|
||||
|
||||
if ($subClassOf->count() > 0) {
|
||||
$type->parents = $subClassOf
|
||||
->map(function (array $parent): string {
|
||||
return $this->getResourceName($parent);
|
||||
})
|
||||
->filter()
|
||||
->toArray();
|
||||
|
||||
if (empty($type->parents)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$type->description = $this->getDefinitionProperty('rdfs:comment');
|
||||
|
||||
$type->resource = $this->getResource();
|
||||
|
||||
$type->partOf = $this->definition['schema:isPartOf']['@id'] ?? null;
|
||||
|
||||
$type->source = $this->definition['schema:source']['@id'] ?? null;
|
||||
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
56
vendor/spatie/schema-org/generator/Parser/Tasks/Task.php
vendored
Normal file
56
vendor/spatie/schema-org/generator/Parser/Tasks/Task.php
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Parser\Tasks;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
abstract class Task
|
||||
{
|
||||
protected array $definition;
|
||||
|
||||
public function __construct(array $definition)
|
||||
{
|
||||
$this->definition = $definition;
|
||||
}
|
||||
|
||||
public static function fromJsonLdArray(array $jsonLdArray): self
|
||||
{
|
||||
return new static($jsonLdArray);
|
||||
}
|
||||
|
||||
protected function getDefinitionProperty(string $selector): string
|
||||
{
|
||||
$property = $this->definition[$selector] ?? '';
|
||||
|
||||
return is_array($property) && array_key_exists('@language', $property) && array_key_exists('@value', $property)
|
||||
? $property['@value']
|
||||
: $property;
|
||||
}
|
||||
|
||||
protected function getWrappedDefinitionProperty(string $selector): Collection
|
||||
{
|
||||
$property = $this->definition[$selector] ?? [];
|
||||
|
||||
if (count($property) === 1) {
|
||||
$property = [$property];
|
||||
}
|
||||
|
||||
return collect($property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the schema resource URL.
|
||||
*/
|
||||
protected function getResource(?array $schemaResource = null): string
|
||||
{
|
||||
return $schemaResource['@id'] ?? $this->definition['@id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Schema resource name.
|
||||
*/
|
||||
protected function getResourceName(?array $schemaResource = null): string
|
||||
{
|
||||
return str_replace('schema:', '', $this->getResource($schemaResource));
|
||||
}
|
||||
}
|
||||
46
vendor/spatie/schema-org/generator/Property.php
vendored
Normal file
46
vendor/spatie/schema-org/generator/Property.php
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator;
|
||||
|
||||
class Property
|
||||
{
|
||||
public string $name;
|
||||
|
||||
public string $description;
|
||||
|
||||
public string $resource;
|
||||
|
||||
/** @var Type[] */
|
||||
public array $types = [];
|
||||
|
||||
/** @var string[] */
|
||||
public array $ranges = [];
|
||||
|
||||
public bool $pending = false;
|
||||
|
||||
public ?string $partOf;
|
||||
|
||||
public ?string $source;
|
||||
|
||||
public function addType(string $type): void
|
||||
{
|
||||
$this->types[] = $type;
|
||||
}
|
||||
|
||||
public function addRanges(array $ranges): void
|
||||
{
|
||||
foreach ($ranges as $range) {
|
||||
$this->addRange($range);
|
||||
}
|
||||
|
||||
sort($this->ranges);
|
||||
}
|
||||
|
||||
private function addRange(string $range): void
|
||||
{
|
||||
$this->ranges[] = $range;
|
||||
$this->ranges[] = "{$range}[]";
|
||||
|
||||
$this->ranges = array_unique($this->ranges);
|
||||
}
|
||||
}
|
||||
76
vendor/spatie/schema-org/generator/Type.php
vendored
Normal file
76
vendor/spatie/schema-org/generator/Type.php
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator;
|
||||
|
||||
class Type
|
||||
{
|
||||
public string $name;
|
||||
|
||||
public string $className;
|
||||
|
||||
/** @var string[] */
|
||||
public array $parents = [];
|
||||
|
||||
public string $description;
|
||||
|
||||
/** @var Property[] */
|
||||
public array $properties = [];
|
||||
|
||||
/** @var Constant[] */
|
||||
public array $constants = [];
|
||||
|
||||
public string $resource;
|
||||
|
||||
public ?string $partOf;
|
||||
|
||||
public ?string $source;
|
||||
|
||||
protected bool $parentsLoaded = false;
|
||||
|
||||
public function addProperty(Property $property): void
|
||||
{
|
||||
$this->properties[$property->name] = $property;
|
||||
|
||||
ksort($this->properties);
|
||||
}
|
||||
|
||||
public function addConstant(Constant $constant): void
|
||||
{
|
||||
$this->constants[$constant->name] = $constant;
|
||||
|
||||
ksort($this->constants);
|
||||
}
|
||||
|
||||
public function setTypeCollection(TypeCollection $typeCollection): void
|
||||
{
|
||||
if ($this->parentsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->parentsLoaded = true;
|
||||
|
||||
if (empty($this->parents)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$types = $typeCollection->toArray();
|
||||
|
||||
foreach ($this->parents as $parentType) {
|
||||
if (! isset($types[$parentType])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var Type $parent */
|
||||
$parent = $types[$parentType];
|
||||
$parent->setTypeCollection($typeCollection);
|
||||
|
||||
$this->parents = array_unique(array_merge($this->parents, $parent->parents));
|
||||
|
||||
sort($this->parents);
|
||||
|
||||
foreach ($parent->properties as $property) {
|
||||
$this->addProperty($property);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
vendor/spatie/schema-org/generator/TypeCollection.php
vendored
Normal file
86
vendor/spatie/schema-org/generator/TypeCollection.php
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class TypeCollection
|
||||
{
|
||||
/** @var iterable|Collection|Type[]
|
||||
*/
|
||||
private $types = [];
|
||||
|
||||
/**
|
||||
* @param iterable|Type[] $types
|
||||
* @param iterable|Property[] $properties
|
||||
* @param iterable|Constant[] $constants
|
||||
*/
|
||||
public function __construct(iterable $types, iterable $properties, iterable $constants)
|
||||
{
|
||||
// Initially map the types to named keys and sort them.
|
||||
$this->types = collect($types)
|
||||
->mapWithKeys(static function (Type $type): array {
|
||||
return [$type->name => $type];
|
||||
})
|
||||
->sortKeys();
|
||||
|
||||
// Grab list of defined types.
|
||||
$definedTypes = $this->types->keys()->toArray();
|
||||
|
||||
// Filter out every type's parents that aren't defined then cast to array.
|
||||
$this->types->each(static function (Type $type) use ($definedTypes): void {
|
||||
$type->parents = array_filter($type->parents, static function (string $parentType) use ($definedTypes): bool {
|
||||
return in_array($parentType, $definedTypes, true);
|
||||
});
|
||||
});
|
||||
|
||||
foreach ($properties as $property) {
|
||||
foreach ($property->ranges as $range) {
|
||||
if (
|
||||
strpos($range, '[]') === false
|
||||
&& ! in_array($range, ['bool', 'false', 'true', '\DateTimeInterface', 'string', 'float', 'int'])
|
||||
&& empty($this->types[$range])
|
||||
) {
|
||||
$property->pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->addProperty($property);
|
||||
}
|
||||
|
||||
foreach ($constants as $constant) {
|
||||
$this->addConstant($constant);
|
||||
}
|
||||
}
|
||||
|
||||
private function addProperty(Property $property): void
|
||||
{
|
||||
foreach ($property->types as $type) {
|
||||
if (! isset($this->types[$type])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->types[$type]->addProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
private function addConstant(Constant $constant): void
|
||||
{
|
||||
if (isset($this->types[$constant->type])) {
|
||||
$this->types[$constant->type]->addConstant($constant);
|
||||
}
|
||||
}
|
||||
|
||||
public function each(Closure $callable): void
|
||||
{
|
||||
foreach ($this->types as $type) {
|
||||
$callable($type);
|
||||
}
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return collect($this->types)->all();
|
||||
}
|
||||
}
|
||||
94
vendor/spatie/schema-org/generator/Writer/Filesystem.php
vendored
Normal file
94
vendor/spatie/schema-org/generator/Writer/Filesystem.php
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Writer;
|
||||
|
||||
use League\Flysystem\Filesystem as Flysystem;
|
||||
use League\Flysystem\Local\LocalFilesystemAdapter;
|
||||
use Spatie\SchemaOrg\Generator\Type;
|
||||
use Spatie\SchemaOrg\Generator\TypeCollection;
|
||||
|
||||
class Filesystem
|
||||
{
|
||||
/** @var \League\Flysystem\Filesystem */
|
||||
protected $flysystem;
|
||||
|
||||
/** @var \Spatie\SchemaOrg\Generator\Writer\Template */
|
||||
protected $contractTemplate;
|
||||
|
||||
/** @var \Spatie\SchemaOrg\Generator\Writer\Template */
|
||||
protected $typeTemplate;
|
||||
|
||||
/** @var \Spatie\SchemaOrg\Generator\Writer\Template */
|
||||
protected $builderClassTemplate;
|
||||
|
||||
/** @var \Spatie\SchemaOrg\Generator\Writer\Template */
|
||||
protected $graphClassTemplate;
|
||||
|
||||
/** @var \Spatie\SchemaOrg\Generator\Writer\Template */
|
||||
protected $multiTypedEntityClassTemplate;
|
||||
|
||||
public function __construct(string $root)
|
||||
{
|
||||
$adapter = new LocalFilesystemAdapter($root);
|
||||
$this->flysystem = new Flysystem($adapter);
|
||||
|
||||
$this->contractTemplate = new Template('Contract.php.twig');
|
||||
$this->typeTemplate = new Template('Type.php.twig');
|
||||
$this->builderClassTemplate = new Template('Schema.php.twig');
|
||||
$this->graphClassTemplate = new Template('Graph.php.twig');
|
||||
$this->multiTypedEntityClassTemplate = new Template('MultiTypedEntity.php.twig');
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$this->flysystem->deleteDirectory('src');
|
||||
$this->flysystem->createDirectory('src');
|
||||
}
|
||||
|
||||
public function cloneStaticFiles()
|
||||
{
|
||||
$files = $this->flysystem->listContents('generator/templates/static', true);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file['type'] !== 'file') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->flysystem->write(
|
||||
str_replace('generator/templates/static', 'src', $file['path']),
|
||||
$this->flysystem->read($file['path'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function createType(Type $type)
|
||||
{
|
||||
$this->flysystem->write(
|
||||
"src/Contracts/{$type->className}Contract.php",
|
||||
$this->contractTemplate->render(['type' => $type])
|
||||
);
|
||||
|
||||
$this->flysystem->write(
|
||||
"src/{$type->className}.php",
|
||||
$this->typeTemplate->render(['type' => $type])
|
||||
);
|
||||
}
|
||||
|
||||
public function createBuilderClass(TypeCollection $types)
|
||||
{
|
||||
$this->flysystem->write(
|
||||
'src/Schema.php',
|
||||
$this->builderClassTemplate->render(['types' => $types->toArray()])
|
||||
);
|
||||
|
||||
$this->flysystem->write(
|
||||
'src/Graph.php',
|
||||
$this->graphClassTemplate->render(['types' => $types->toArray()])
|
||||
);
|
||||
|
||||
$this->flysystem->write(
|
||||
'src/MultiTypedEntity.php',
|
||||
$this->multiTypedEntityClassTemplate->render(['types' => $types->toArray()])
|
||||
);
|
||||
}
|
||||
}
|
||||
74
vendor/spatie/schema-org/generator/Writer/Filters.php
vendored
Normal file
74
vendor/spatie/schema-org/generator/Writer/Filters.php
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Writer;
|
||||
|
||||
class Filters
|
||||
{
|
||||
public static function doc($text, array $options = []): string
|
||||
{
|
||||
$indentation = $options[0] ?? 0;
|
||||
$docblockPrefix = str_repeat(' ', $indentation * 4).' * ';
|
||||
|
||||
$lineLength = 80 - 3 - ($indentation * 4);
|
||||
|
||||
// Remove HTML breaks
|
||||
$text = str_replace(['<br/>', '<br />'], '', $text);
|
||||
$text = str_replace(['&', '<', '>', '"'], ['&', '<', '>', '"'], $text);
|
||||
|
||||
// HTML code block to MD code
|
||||
$text = str_replace(['<code>', '</code>'], '```', $text);
|
||||
|
||||
// Emphasis to MD bold
|
||||
$text = str_replace(['<em>', '</em>'], '*', $text);
|
||||
// Strong to bold
|
||||
$text = str_replace(['<strong>', '</strong>'], '__', $text);
|
||||
|
||||
// Parse lists into markdown
|
||||
$text = str_replace(["<ul>\n", '</ul>', '</li>'], '', $text);
|
||||
$text = str_replace('<li>', '* ', $text);
|
||||
|
||||
// Replace any remote links first...
|
||||
$text = preg_replace('/<a href="([\.0-9A-Za-z\:\/\-\_#\+]+)">([0-9a-zA-Z\s\-\_]*)<\/a>/', '[$2]($1)', $text);
|
||||
|
||||
// Next replace any local links...
|
||||
$text = preg_replace('/<a class="localLink" href="[a-zA-Z0-9\.\/\:\#\+]+">([a-zA-Z\s]*)<\/a>/', '[[$1]]', $text);
|
||||
|
||||
$text = rtrim($text);
|
||||
|
||||
$text = preg_replace('/(\n|\\\n)/', "\n".$docblockPrefix, $text);
|
||||
|
||||
return wordwrap($text, $lineLength, "\n".$docblockPrefix);
|
||||
}
|
||||
|
||||
public static function lcfirst($text): string
|
||||
{
|
||||
return lcfirst($text);
|
||||
}
|
||||
|
||||
public static function param(array $ranges): string
|
||||
{
|
||||
return implode('|', array_map(function (string $type) {
|
||||
$isArray = strpos($type, '[]') !== false;
|
||||
$baseType = str_replace('[]', '', $type);
|
||||
|
||||
if (in_array($baseType, ['bool', 'false', 'true', 'string', 'float', 'int'])) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (substr($type, 0, 1) === '\\') {
|
||||
return $type;
|
||||
}
|
||||
|
||||
return "\\Spatie\\SchemaOrg\\Contracts\\{$baseType}Contract".($isArray ? '[]' : '');
|
||||
}, $ranges));
|
||||
}
|
||||
|
||||
public static function fixLink(string $input): string
|
||||
{
|
||||
if (! str_starts_with($input, 'schema:')) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
return str_replace('schema:', 'https://schema.org/', $input);
|
||||
}
|
||||
}
|
||||
56
vendor/spatie/schema-org/generator/Writer/Template.php
vendored
Normal file
56
vendor/spatie/schema-org/generator/Writer/Template.php
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Generator\Writer;
|
||||
|
||||
use Twig\Environment as TwigEnvironment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
class Template
|
||||
{
|
||||
protected TwigEnvironment $twig;
|
||||
|
||||
/** @var string */
|
||||
protected $template;
|
||||
|
||||
public function __construct(string $template)
|
||||
{
|
||||
$this->template = $template;
|
||||
$this->twig = $this->createTwigEnvironment();
|
||||
}
|
||||
|
||||
public function render(array $context): string
|
||||
{
|
||||
return $this->twig
|
||||
->load($this->template)
|
||||
->render($context);
|
||||
}
|
||||
|
||||
protected function createTwigEnvironment(): TwigEnvironment
|
||||
{
|
||||
$loader = new FilesystemLoader(__DIR__.'/../templates/twig');
|
||||
|
||||
$twig = new TwigEnvironment($loader, [
|
||||
'cache' => false,
|
||||
'autoescape' => false,
|
||||
]);
|
||||
|
||||
$twig->addFilter(
|
||||
new TwigFilter('doc', [Filters::class, 'doc'], ['is_variadic' => true])
|
||||
);
|
||||
|
||||
$twig->addFilter(
|
||||
new TwigFilter('param', [Filters::class, 'param'])
|
||||
);
|
||||
|
||||
$twig->addFilter(
|
||||
new TwigFilter('lcfirst', [Filters::class, 'lcfirst'])
|
||||
);
|
||||
|
||||
$twig->addFilter(
|
||||
new TwigFilter('fixLink', [Filters::class, 'fixLink'])
|
||||
);
|
||||
|
||||
return $twig;
|
||||
}
|
||||
}
|
||||
2
vendor/spatie/schema-org/generator/temp/.gitignore
vendored
Normal file
2
vendor/spatie/schema-org/generator/temp/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
180
vendor/spatie/schema-org/generator/templates/static/BaseType.php
vendored
Normal file
180
vendor/spatie/schema-org/generator/templates/static/BaseType.php
vendored
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg;
|
||||
|
||||
use ArrayAccess;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use JsonSerializable;
|
||||
use ReflectionClass;
|
||||
use Spatie\SchemaOrg\Exceptions\InvalidProperty;
|
||||
|
||||
abstract class BaseType implements Type, ArrayAccess, JsonSerializable
|
||||
{
|
||||
/** @var array */
|
||||
protected $properties = [];
|
||||
|
||||
/** @var string */
|
||||
protected $nonce = '';
|
||||
|
||||
public function getContext(): string
|
||||
{
|
||||
return 'https://schema.org';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return (new ReflectionClass($this))->getShortName();
|
||||
}
|
||||
|
||||
public function setProperty(string $property, $value)
|
||||
{
|
||||
if ($value !== null && $value !== '') {
|
||||
$this->properties[$property] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addProperties(array $properties)
|
||||
{
|
||||
foreach ($properties as $property => $value) {
|
||||
$this->setProperty($property, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function if($condition, $callback)
|
||||
{
|
||||
if ($condition) {
|
||||
$callback($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setNonce(string $nonce)
|
||||
{
|
||||
$this->nonce = $nonce;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProperty(string $property, $default = null)
|
||||
{
|
||||
return $this->properties[$property] ?? $default;
|
||||
}
|
||||
|
||||
public function getProperties(): array
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReferencedType|static
|
||||
*/
|
||||
public function referenced()
|
||||
{
|
||||
return new ReferencedType($this);
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return array_key_exists($offset, $this->properties);
|
||||
}
|
||||
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->getProperty($offset);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->setProperty($offset, $value);
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->properties[$offset]);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$this->serializeIdentifier();
|
||||
$properties = $this->serializeProperty($this->getProperties());
|
||||
|
||||
return [
|
||||
'@context' => $this->getContext(),
|
||||
'@type' => $this->getType(),
|
||||
] + $properties;
|
||||
}
|
||||
|
||||
protected function serializeProperty($property)
|
||||
{
|
||||
if (is_array($property)) {
|
||||
return array_map([$this, 'serializeProperty'], $property);
|
||||
}
|
||||
|
||||
if ($property instanceof Type) {
|
||||
$property = $property->toArray();
|
||||
unset($property['@context']);
|
||||
}
|
||||
|
||||
if ($property instanceof DateTimeInterface) {
|
||||
$property = $property->format(DateTime::ATOM);
|
||||
}
|
||||
|
||||
if (is_object($property) && method_exists($property, '__toString')) {
|
||||
$property = (string) $property;
|
||||
}
|
||||
|
||||
if (is_object($property)) {
|
||||
throw new InvalidProperty();
|
||||
}
|
||||
|
||||
return $property;
|
||||
}
|
||||
|
||||
protected function serializeIdentifier()
|
||||
{
|
||||
if (
|
||||
isset($this['identifier'])
|
||||
&& ! $this['identifier'] instanceof Type
|
||||
) {
|
||||
$this->setProperty('@id', $this['identifier']);
|
||||
unset($this['identifier']);
|
||||
}
|
||||
}
|
||||
|
||||
public function nonceAttr(): string
|
||||
{
|
||||
if ($this->nonce) {
|
||||
$attr = ' nonce="'.$this->nonce.'"';
|
||||
} else {
|
||||
$attr = '';
|
||||
}
|
||||
|
||||
return $attr;
|
||||
}
|
||||
|
||||
public function toScript(): string
|
||||
{
|
||||
return '<script type="application/ld+json"'.$this->nonceAttr().'>'.json_encode($this->toArray(), JSON_UNESCAPED_UNICODE).'</script>';
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
public function __call(string $method, array $arguments)
|
||||
{
|
||||
return $this->setProperty($method, $arguments[0] ?? '');
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toScript();
|
||||
}
|
||||
}
|
||||
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/InvalidProperty.php
vendored
Normal file
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/InvalidProperty.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidProperty extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/InvalidType.php
vendored
Normal file
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/InvalidType.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class InvalidType extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/TypeAlreadyInGraph.php
vendored
Normal file
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/TypeAlreadyInGraph.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class TypeAlreadyInGraph extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class TypeAlreadyInMultiTypedEntity extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/TypeNotInGraph.php
vendored
Normal file
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/TypeNotInGraph.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class TypeNotInGraph extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/TypeNotInMultiTypedEntity.php
vendored
Normal file
9
vendor/spatie/schema-org/generator/templates/static/Exceptions/TypeNotInMultiTypedEntity.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class TypeNotInMultiTypedEntity extends InvalidArgumentException
|
||||
{
|
||||
}
|
||||
38
vendor/spatie/schema-org/generator/templates/static/ReferencedType.php
vendored
Normal file
38
vendor/spatie/schema-org/generator/templates/static/ReferencedType.php
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class ReferencedType implements Type, JsonSerializable
|
||||
{
|
||||
/** @var Type */
|
||||
protected $type;
|
||||
|
||||
public function __construct(Type $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'@id' => $this->type->toArray()['@id'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
public function toScript(): string
|
||||
{
|
||||
return $this->type->toScript();
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toScript();
|
||||
}
|
||||
}
|
||||
30
vendor/spatie/schema-org/generator/templates/static/Type.php
vendored
Normal file
30
vendor/spatie/schema-org/generator/templates/static/Type.php
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Spatie\SchemaOrg;
|
||||
|
||||
interface Type
|
||||
{
|
||||
/**
|
||||
* Return an array representation of the type. If the array contains child
|
||||
* types, their context needs to be stripped if it's the same.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array;
|
||||
|
||||
/**
|
||||
* Create a json-ld script tag for this type, built from the data that
|
||||
* `toArray` returns.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toScript(): string;
|
||||
|
||||
/**
|
||||
* Create a json-ld script tag for this type, built from the data that
|
||||
* `toArray` returns.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue