chore: update kirby-seo plugin to v2.0.0-alpha.12
Update plugin from v1.1.2 to v2.0.0-alpha.12 for Kirby 5 compatibility. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff215de723
commit
04a14a7f1f
70 changed files with 6142 additions and 3 deletions
316
site/plugins/kirby-seo/classes/IndexNow.php
Normal file
316
site/plugins/kirby-seo/classes/IndexNow.php
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Http\Remote;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
use function is_bool;
|
||||
|
||||
class IndexNow
|
||||
{
|
||||
protected static string|null $key = null;
|
||||
|
||||
protected Page $page;
|
||||
protected array $urls = [];
|
||||
protected bool $collected = false;
|
||||
|
||||
public function __construct(Page $page)
|
||||
{
|
||||
$this->page = $page;
|
||||
|
||||
// always add the current page if it's indexable
|
||||
if ($this->isIndexable($page)) {
|
||||
$this->urls[] = $page->url();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect URLs to be indexed based on rules
|
||||
*/
|
||||
public function collect(): self
|
||||
{
|
||||
if ($this->collected) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$rules = Seo::option('indexnow.rules') ?? [];
|
||||
|
||||
foreach ($rules as $pattern => $invalidations) {
|
||||
if (!$this->matchesPattern($pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->collectFromRule($invalidations);
|
||||
}
|
||||
|
||||
$this->urls = array_unique($this->urls);
|
||||
$this->collected = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collected urls
|
||||
*/
|
||||
public function urls(): array
|
||||
{
|
||||
if (!$this->collected) {
|
||||
$this->collect();
|
||||
}
|
||||
|
||||
return $this->urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the collected urls
|
||||
*/
|
||||
public function request(): bool
|
||||
{
|
||||
if (!$this->collected) {
|
||||
$this->collect();
|
||||
}
|
||||
|
||||
return static::send($this->urls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to send urls to indexnow api
|
||||
*/
|
||||
public static function send(array $urls): bool
|
||||
{
|
||||
if (!Seo::option('indexnow.enabled') || empty($urls)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$firstUrl = $urls[0];
|
||||
$parsedUrl = parse_url($firstUrl);
|
||||
$host = $parsedUrl['host'];
|
||||
$scheme = $parsedUrl['scheme'] ?? 'https';
|
||||
$path = $parsedUrl['path'] ?? '';
|
||||
|
||||
// don't send requests for local development environments
|
||||
if (App::instance()->environment()->isLocal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get base path (everything before the page path)
|
||||
$basePath = '';
|
||||
if ($path && $path !== '/') {
|
||||
// find the base path by comparing with site url
|
||||
$siteUrl = App::instance()->site()->url();
|
||||
$siteParsed = parse_url($siteUrl);
|
||||
$basePath = $siteParsed['path'] ?? '';
|
||||
}
|
||||
|
||||
$searchEngine = Seo::option('indexnow.searchEngine');
|
||||
$searchEngine = rtrim($searchEngine, '/');
|
||||
if (!str_contains($searchEngine, '/indexnow')) {
|
||||
$searchEngine .= '/indexnow';
|
||||
}
|
||||
|
||||
$domainUrls = array_filter($urls, fn ($url) => parse_url($url, PHP_URL_HOST) === $host);
|
||||
|
||||
// split into batches of 10,000 (IndexNow limit)
|
||||
$batches = array_chunk(array_values(array_unique($domainUrls)), 10000);
|
||||
$allSuccessful = true;
|
||||
$key = static::key();
|
||||
|
||||
foreach ($batches as $batch) {
|
||||
try {
|
||||
$response = Remote::post($searchEngine, [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json; charset=utf-8',
|
||||
'User-Agent' => Seo::userAgent()
|
||||
],
|
||||
'data' => json_encode([
|
||||
'host' => $host,
|
||||
'key' => $key,
|
||||
'keyLocation' => "{$scheme}://{$host}{$basePath}/indexnow-{$key}.txt",
|
||||
'urlList' => $batch
|
||||
])
|
||||
]);
|
||||
|
||||
if ($response->code() > 299) {
|
||||
$allSuccessful = false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$allSuccessful = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $allSuccessful;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or generate the indexnow key
|
||||
* Stored in cache so it persists across requests
|
||||
*/
|
||||
public static function key(): string
|
||||
{
|
||||
return static::$key ??= App::instance()->cache('tobimori.seo.indexnow')->getOrSet('key', fn () => Str::random(32, 'hexLower'), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provided key matches the stored key
|
||||
* Used by the route to verify ownership
|
||||
*/
|
||||
public static function verifyKey(string $providedKey): bool
|
||||
{
|
||||
return $providedKey === static::key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if page matches a pattern (url glob/regex or template)
|
||||
*/
|
||||
protected function matchesPattern(string $pattern): bool
|
||||
{
|
||||
if ($pattern === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// url pattern
|
||||
if (str_contains($pattern, '/')) {
|
||||
return $this->matchesUrlPattern($pattern, $this->page->url());
|
||||
}
|
||||
|
||||
// template pattern
|
||||
return $this->page->intendedTemplate()->name() === $pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match url pattern (glob style)
|
||||
*/
|
||||
protected function matchesUrlPattern(string $pattern, string $url): bool
|
||||
{
|
||||
// convert glob to regex
|
||||
$pattern = str_replace(
|
||||
['*', '?', '[', ']'],
|
||||
['.*', '.', '\[', '\]'],
|
||||
$pattern
|
||||
);
|
||||
|
||||
return preg_match("#^{$pattern}$#", parse_url($url, PHP_URL_PATH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect urls based on invalidation rules
|
||||
*/
|
||||
protected function collectFromRule(array $rule): void
|
||||
{
|
||||
// parent(s)
|
||||
if (isset($rule['parent'])) {
|
||||
$this->collectParents($rule['parent']);
|
||||
}
|
||||
|
||||
// children
|
||||
if (isset($rule['children'])) {
|
||||
$this->collectChildren($rule['children']);
|
||||
}
|
||||
|
||||
// siblings
|
||||
if (isset($rule['siblings']) && $rule['siblings'] === true) {
|
||||
$this->collectSiblings();
|
||||
}
|
||||
|
||||
// specific urls
|
||||
if (isset($rule['urls'])) {
|
||||
foreach ($rule['urls'] as $url) {
|
||||
$this->urls[] = url($url);
|
||||
}
|
||||
}
|
||||
|
||||
// pages by template
|
||||
if (isset($rule['templates'])) {
|
||||
$this->collectByTemplates($rule['templates']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect parent urls
|
||||
*/
|
||||
protected function collectParents($levels): void
|
||||
{
|
||||
$parent = $this->page->parent();
|
||||
$count = is_bool($levels) ? 1 : $levels;
|
||||
$language = App::instance()->language();
|
||||
|
||||
while ($parent && $count > 0) {
|
||||
if ($this->isIndexable($parent)) {
|
||||
$this->urls[] = $parent->url($language?->code());
|
||||
}
|
||||
$parent = $parent->parent();
|
||||
$count--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect children urls
|
||||
*/
|
||||
protected function collectChildren($depth): void
|
||||
{
|
||||
$maxDepth = is_bool($depth) ? null : $depth;
|
||||
$language = App::instance()->language();
|
||||
|
||||
$collectRecursive = function ($page, $currentDepth = 0) use (&$collectRecursive, $maxDepth, $language) {
|
||||
if ($maxDepth !== null && $currentDepth >= $maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($page->children() as $child) {
|
||||
if ($this->isIndexable($child)) {
|
||||
$this->urls[] = $child->url($language?->code());
|
||||
}
|
||||
$collectRecursive($child, $currentDepth + 1);
|
||||
}
|
||||
};
|
||||
|
||||
$collectRecursive($this->page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect sibling urls
|
||||
*/
|
||||
protected function collectSiblings(): void
|
||||
{
|
||||
if (!$this->page->parent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$language = App::instance()->language();
|
||||
|
||||
foreach ($this->page->siblings() as $sibling) {
|
||||
if ($this->isIndexable($sibling)) {
|
||||
$this->urls[] = $sibling->url($language?->code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect urls by template names
|
||||
*/
|
||||
protected function collectByTemplates(array $templates): void
|
||||
{
|
||||
$language = App::instance()->language();
|
||||
|
||||
$pages = $this->page->site()->index()
|
||||
->filterBy('intendedTemplate', 'in', $templates)
|
||||
->filter($this->isIndexable(...));
|
||||
|
||||
foreach ($pages as $page) {
|
||||
$this->urls[] = $page->url($language?->code());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is indexable (robots allow + listed)
|
||||
*/
|
||||
protected function isIndexable(Page $page): bool
|
||||
{
|
||||
return $page->isListed()
|
||||
&& $page->robots() !== 'noindex'
|
||||
&& $page->robots() !== 'none';
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue