SEO : add tombi mori plugin
This commit is contained in:
parent
df2843123f
commit
8f9e75126e
64 changed files with 3719 additions and 44 deletions
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);
|
||||
}
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue