SEO additions index now

This commit is contained in:
Mark Gregory
2026-10-06 15:15:53 +01:00
parent 93f640b6bc
commit b3ca947d7d
6 changed files with 160 additions and 2 deletions
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use App\Models\CmsPage;
use App\Services\IndexNowService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Route;
use Throwable;
final class SubmitIndexNow extends Command
{
protected $signature = 'indexnow:submit {urls?* : Absolute URLs to submit} {--all : Submit all public indexable SonarDesk URLs}';
protected $description = 'Submit public SonarDesk URLs to IndexNow';
public function handle(IndexNowService $indexNow): int
{
if (! config('indexnow.enabled')) {
$this->warn('IndexNow is disabled. Set INDEXNOW_ENABLED=true to enable submissions.');
return self::FAILURE;
}
$urls = $this->argument('urls');
if ($this->option('all') || $urls === []) {
$urls = $this->allPublicUrls();
}
try {
$response = $indexNow->submit($urls);
} catch (Throwable $e) {
$this->error($e->getMessage());
return self::FAILURE;
}
if ($response === null || ! $response->successful()) {
$this->error('IndexNow rejected the submission'.($response ? ' (HTTP '.$response->status().').' : '.'));
return self::FAILURE;
}
$this->info('Submitted '.count($urls).' URL(s) to IndexNow (HTTP '.$response->status().').');
return self::SUCCESS;
}
/** @return array<int, string> */
private function allPublicUrls(): array
{
$routes = ['home', 'features', 'buy', 'release', 'support', 'privacy', 'terms', 'refund', 'cookies'];
$urls = collect($routes)->filter(fn (string $name) => Route::has($name))->map(fn (string $name) => route($name));
$pages = CmsPage::query()
->where('is_published', true)
->where('robots_index', true)
->where(fn ($query) => $query->whereNull('published_at')->orWhere('published_at', '<=', now()))
->pluck('slug')
->map(fn (string $slug) => route('cms.show', ['slug' => $slug]));
return $urls->merge($pages)->unique()->values()->all();
}
}
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\CmsPage;
use App\Services\IndexNowService;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
@@ -12,11 +13,24 @@ final class CmsPageController extends Controller
{
public function index() { return view('admin.cms.index', ['pages' => CmsPage::latest()->paginate(25)]); }
public function create() { return view('admin.cms.form', ['page' => new CmsPage()]); }
public function store(Request $request) { $page = CmsPage::create($this->validated($request)); return redirect()->route('admin.cms.edit', $page)->with('status', 'Page created.'); }
public function store(Request $request, IndexNowService $indexNow) { $page = CmsPage::create($this->validated($request)); $this->submitIfIndexable($page, $indexNow); return redirect()->route('admin.cms.edit', $page)->with('status', 'Page created.'); }
public function edit(CmsPage $page) { return view('admin.cms.form', compact('page')); }
public function update(Request $request, CmsPage $page) { $page->update($this->validated($request, $page)); return back()->with('status', 'Page updated.'); }
public function update(Request $request, CmsPage $page, IndexNowService $indexNow) { $page->update($this->validated($request, $page)); $this->submitIfIndexable($page, $indexNow); return back()->with('status', 'Page updated.'); }
public function destroy(CmsPage $page) { $page->delete(); return redirect()->route('admin.cms.index')->with('status', 'Page deleted.'); }
private function submitIfIndexable(CmsPage $page, IndexNowService $indexNow): void
{
if (! $page->is_published || ! $page->robots_index || ($page->published_at && $page->published_at->isFuture())) {
return;
}
try {
$indexNow->submit([route('cms.show', ['slug' => $page->slug])]);
} catch (\Throwable $e) {
report($e);
}
}
private function validated(Request $request, ?CmsPage $page = null): array
{
$request->merge(['slug' => Str::slug($request->input('slug') ?: $request->input('title', ''))]);
+12
View File
@@ -37,6 +37,18 @@ final class SeoController extends Controller
->header('Cache-Control', 'public, max-age=3600');
}
public function indexNowKey(string $key): Response
{
$configured = trim((string) config('indexnow.key'));
abort_unless(config('indexnow.enabled') && $configured !== '' && hash_equals($configured, $key), 404);
return response($configured, 200, [
'Content-Type' => 'text/plain; charset=UTF-8',
'Cache-Control' => 'public, max-age=86400',
]);
}
public function robots(): Response
{
$indexable = app()->environment('production') && SiteSetting::booleanFor('seo_robots_enabled', true);
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Services;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
final class IndexNowService
{
/** @param array<int, string> $urls */
public function submit(array $urls): ?Response
{
if (! config('indexnow.enabled')) {
Log::info('IndexNow submission skipped: disabled.');
return null;
}
$key = trim((string) config('indexnow.key'));
if ($key === '') {
throw new InvalidArgumentException('INDEXNOW_KEY is not configured.');
}
$urls = collect($urls)
->filter(fn ($url) => is_string($url) && filter_var($url, FILTER_VALIDATE_URL))
->unique()
->values()
->all();
if ($urls === []) {
throw new InvalidArgumentException('No valid URLs were supplied to IndexNow.');
}
$host = parse_url($urls[0], PHP_URL_HOST);
if (! is_string($host) || $host === '') {
throw new InvalidArgumentException('Could not determine the IndexNow host.');
}
foreach ($urls as $url) {
if (parse_url($url, PHP_URL_HOST) !== $host) {
throw new InvalidArgumentException('All IndexNow URLs must use the same host.');
}
}
$response = Http::asJson()
->acceptJson()
->timeout(10)
->retry(2, 500)
->post((string) config('indexnow.endpoint'), [
'host' => $host,
'key' => $key,
'keyLocation' => 'https://'.$host.'/'.$key.'.txt',
'urlList' => $urls,
]);
if ($response->successful()) {
Log::info('IndexNow URLs submitted.', ['host' => $host, 'count' => count($urls), 'status' => $response->status()]);
} else {
Log::warning('IndexNow submission failed.', ['host' => $host, 'count' => count($urls), 'status' => $response->status(), 'body' => $response->body()]);
}
return $response;
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
return [
'enabled' => (bool) env('INDEXNOW_ENABLED', false),
'key' => env('INDEXNOW_KEY'),
'endpoint' => env('INDEXNOW_ENDPOINT', 'https://api.indexnow.org/indexnow'),
];
+1
View File
@@ -18,6 +18,7 @@ use App\Http\Middleware\AdminOnly;
use Illuminate\Support\Facades\Route;
Route::get('/sitemap.xml', [SeoController::class, 'sitemap'])->name('sitemap');
Route::get('/{key}.txt', [SeoController::class, 'indexNowKey'])->where('key', '[A-Fa-f0-9]{8,128}')->name('indexnow.key');
Route::get('/robots.txt', [SeoController::class, 'robots'])->name('robots');
Route::get('/', fn () => view('portal.welcome', ['latestRelease' => \App\Models\SonarDeskRelease::published()->where('channel', 'stable')->latest('published_at')->first()]))->name('home');