seo fixes

This commit is contained in:
2026-08-09 04:19:21 +03:00
parent df2128ebff
commit bed4c48900
19 changed files with 368 additions and 25 deletions
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
class ApiDocsController extends Controller
{
public function show(): View
{
$markdown = str_replace('{{BASE_URL}}', config('app.url'), $this->markdown());
return view('api-docs', [
'bodyHtml' => Str::markdown($markdown, [
'html_input' => 'strip',
'allow_unsafe_links' => false,
]),
'title' => 'API Documentation — '.config('app.name'),
'description' => 'REST API reference for creating posts, categories, tags, and AI tool listings on '.config('app.name').'.',
'canonical' => route('api.docs'),
]);
}
public function llmsTxt(): Response
{
$baseUrl = config('app.url');
$text = <<<TXT
# {$this->appName()}
> {$this->appName()} is a blog and AI tools directory with a public JSON REST API for programmatic publishing.
This site welcomes AI agents that have been given valid credentials by the site operator. See the API documentation for authentication and endpoint details.
## Docs
- [API documentation]({$baseUrl}/api-docs): full REST API reference (auth, posts, categories, tags, tools)
- [Sitemap]({$baseUrl}/sitemap.xml)
- [RSS feed]({$baseUrl}/blog/feed.xml)
TXT;
return response($text, 200)->header('Content-Type', 'text/plain; charset=UTF-8');
}
private function markdown(): string
{
return file_get_contents(resource_path('docs/api.md'));
}
private function appName(): string
{
return config('app.name');
}
}
+2 -2
View File
@@ -20,8 +20,8 @@ class BlogController extends Controller
return view('blog.index', [
'posts' => $posts,
'title' => config('app.name').' — Blog',
'description' => 'Latest articles from '.config('app.name').'.',
'title' => config('app.name').' — AI Tools Directory & Blog for Developers',
'description' => 'Discover and compare AI tools, plus in-depth articles on AI-assisted development, automation, and building with AI agents.',
'canonical' => route('blog.index'),
]);
}
+1 -1
View File
@@ -72,7 +72,7 @@ class ToolController extends Controller
'tool' => $tool,
'related' => $related,
'title' => $tool->name.' — AI Tools — '.config('app.name'),
'description' => str($tool->description)->limit(160)->toString(),
'description' => str($tool->description)->limit(155, '')->toString(),
'canonical' => route('tools.show', $tool),
'image' => $tool->logo_url,
]);
+16 -4
View File
@@ -79,10 +79,22 @@ class Post extends Model
protected function bodyHtml(): Attribute
{
return Attribute::get(fn () => Str::markdown($this->body, [
'html_input' => 'strip',
'allow_unsafe_links' => false,
]));
return Attribute::get(function () {
$html = Str::markdown($this->body, [
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
// The page template already renders the post title as an <h1>. If the
// Markdown body also opens with a top-level heading repeating that
// title, drop it so the page doesn't ship two identical <h1>s.
return preg_replace_callback(
'/^\s*<h1>(.*?)<\/h1>/is',
fn ($matches) => trim(strip_tags($matches[1])) === trim($this->title) ? '' : $matches[0],
$html,
limit: 1
);
});
}
protected function excerptOrSummary(): Attribute
+7
View File
@@ -3,6 +3,7 @@
namespace App\Providers;
use App\Models\Page;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@@ -21,6 +22,12 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
// Always generate absolute URLs (route(), url(), canonical tags, sitemap,
// JSON-LD, ...) from the configured APP_URL, not the incoming request's
// Host header. Without this, www vs apex requests each produce
// self-referencing canonical URLs for identical content.
URL::forceRootUrl(config('app.url'));
View::composer('components.layouts.app', function ($view) {
$view->with('footerPages', Page::published()->inFooter()->get());
});