From bed4c48900f9bcb4c4cedf70234f4a61000a8298 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 9 Aug 2026 04:19:21 +0300 Subject: [PATCH] seo fixes --- app/Http/Controllers/ApiDocsController.php | 56 ++++ app/Http/Controllers/BlogController.php | 4 +- app/Http/Controllers/ToolController.php | 2 +- app/Models/Post.php | 20 +- app/Providers/AppServiceProvider.php | 7 + public/images/og-default.png | Bin 0 -> 44763 bytes public/robots.txt | 2 + resources/docs/api.md | 247 ++++++++++++++++++ resources/views/api-docs.blade.php | 17 ++ resources/views/blog/category.blade.php | 2 +- resources/views/blog/index.blade.php | 2 +- resources/views/blog/show.blade.php | 2 +- resources/views/blog/tag.blade.php | 2 +- .../views/components/layouts/app.blade.php | 19 +- resources/views/pages/show.blade.php | 2 +- resources/views/tools/category.blade.php | 2 +- resources/views/tools/index.blade.php | 2 +- resources/views/tools/show.blade.php | 2 +- routes/web.php | 3 + 19 files changed, 368 insertions(+), 25 deletions(-) create mode 100644 app/Http/Controllers/ApiDocsController.php create mode 100644 public/images/og-default.png create mode 100644 resources/docs/api.md create mode 100644 resources/views/api-docs.blade.php diff --git a/app/Http/Controllers/ApiDocsController.php b/app/Http/Controllers/ApiDocsController.php new file mode 100644 index 0000000..68bc676 --- /dev/null +++ b/app/Http/Controllers/ApiDocsController.php @@ -0,0 +1,56 @@ +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 = <<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'); + } +} diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php index c3db739..76e6f61 100644 --- a/app/Http/Controllers/BlogController.php +++ b/app/Http/Controllers/BlogController.php @@ -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'), ]); } diff --git a/app/Http/Controllers/ToolController.php b/app/Http/Controllers/ToolController.php index 6571447..b087cd0 100644 --- a/app/Http/Controllers/ToolController.php +++ b/app/Http/Controllers/ToolController.php @@ -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, ]); diff --git a/app/Models/Post.php b/app/Models/Post.php index f52a9f1..06caba8 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -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

. If the + // Markdown body also opens with a top-level heading repeating that + // title, drop it so the page doesn't ship two identical

s. + return preg_replace_callback( + '/^\s*

(.*?)<\/h1>/is', + fn ($matches) => trim(strip_tags($matches[1])) === trim($this->title) ? '' : $matches[0], + $html, + limit: 1 + ); + }); } protected function excerptOrSummary(): Attribute diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ce44a55..43992ad 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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()); }); diff --git a/public/images/og-default.png b/public/images/og-default.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb7c6ae2bfed606f2101161990155633c6f17e9 GIT binary patch literal 44763 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 literal 0 HcmV?d00001 diff --git a/public/robots.txt b/public/robots.txt index 7cae0be..ee9e225 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -3,3 +3,5 @@ Allow: / Disallow: /admin Sitemap: /sitemap.xml + +# AI agents: see /llms.txt for how to use this site's API. diff --git a/resources/docs/api.md b/resources/docs/api.md new file mode 100644 index 0000000..eb3ae96 --- /dev/null +++ b/resources/docs/api.md @@ -0,0 +1,247 @@ +# API Documentation + +This site exposes a JSON REST API for creating and managing content — blog posts, categories, tags, and AI tool listings. It is designed to be used by both humans and AI agents that want to publish content programmatically. + +- **Base URL:** `{{BASE_URL}}` +- **Format:** JSON request and response bodies (`Content-Type: application/json`, `Accept: application/json`) +- **Auth:** Bearer token ([Laravel Sanctum](https://laravel.com/docs/sanctum) personal access tokens) + +All endpoints below are prefixed with `/api`. + +## Getting credentials + +There is no public sign-up endpoint. An account must be created first by the site administrator (via the admin panel at `/admin` or directly on the server), and its email/password shared with you (or the agent) out of band. Once you have credentials, exchange them for a token via `POST /api/login`. + +If you are an AI agent integrating with this API, ask the site operator for an account before attempting to authenticate. + +## Authentication + +### `POST /api/login` + +Exchanges an email/password for a bearer token. Rate limited to 10 requests per minute per IP. + +**Body** + +| Field | Type | Required | Notes | +|---|---|---|---| +| `email` | string | yes | | +| `password` | string | yes | | +| `device_name` | string | no | Label for the issued token (e.g. `"my-agent"`). Defaults to `"api"`. | + +**Request** + +```bash +curl -X POST {{BASE_URL}}/api/login \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{"email":"you@example.com","password":"secret","device_name":"my-agent"}' +``` + +**Response `200`** + +```json +{ + "user": { "id": 1, "name": "Jane Doe", "email": "you@example.com" }, + "token": "1|uQsMLuVYWop56oQlaZuiZwHfclmidLiX7u8fBk7Yb17c6954" +} +``` + +Send the token on every subsequent request: + +``` +Authorization: Bearer 1|uQsMLuVYWop56oQlaZuiZwHfclmidLiX7u8fBk7Yb17c6954 +``` + +The token does not expire automatically and grants full access to everything the user account can do — treat it like a password. Store it securely. + +### `POST /api/logout` + +Revokes the token used to make the request. Requires auth. + +### `GET /api/user` + +Returns the authenticated user. Requires auth. + +## Conventions + +- **All endpoints below require the `Authorization: Bearer ` header** unless noted otherwise. +- Resources are looked up **by numeric `id`** in the API (e.g. `/api/posts/42`), not by slug. Slugs are only used in the public-facing site URLs. +- List endpoints (`index`) are paginated 20 per page and return Laravel's standard pagination envelope: + + ```json + { + "data": [ /* array of resources */ ], + "links": { "first": "...", "last": "...", "prev": null, "next": "..." }, + "meta": { "current_page": 1, "last_page": 3, "per_page": 20, "total": 47, "...": "..." } + } + ``` + + Single-resource responses (`show`, `store`, `update`) are wrapped as `{ "data": { ... } }`. +- Successful deletes return `204 No Content`. +- Validation errors return `422` with Laravel's standard shape: + + ```json + { "message": "The title field is required.", "errors": { "title": ["The title field is required."] } } + ``` +- Missing/invalid token returns `401 { "message": "Unauthenticated." }`. +- Unknown resource ID returns `404`. + +## Posts + +Fields: `id`, `title`, `slug`, `excerpt`, `body` (Markdown), `featured_image`, `status` (`draft` | `published`), `published_at`, `meta_title`, `meta_description`, `author`, `category`, `tags`, `created_at`, `updated_at`. + +A post is only visible on the public site once **both** `status` is `published` **and** `published_at` is a timestamp in the past. A `published` post with a future `published_at` is scheduled but not yet live. + +**Timezone:** the server interprets any datetime you send (including `published_at`) as **UTC**, regardless of what timezone it was generated in. If you compute "now" on your own machine, convert it to UTC first — otherwise a post can silently end up scheduled hours in the future and won't appear on the site yet. + +### `GET /api/posts` + +Query params (all optional): `status` (`draft` or `published`), `category_id`. + +### `POST /api/posts` + +| Field | Type | Required | Notes | +|---|---|---|---| +| `title` | string | yes | max 255 | +| `slug` | string | no | must be unique; auto-generated from `title` if omitted | +| `body` | string | yes | Markdown | +| `excerpt` | string | no | max 255 | +| `category_id` | integer | no | must exist in `/api/categories` | +| `featured_image` | string | no | path/URL to an image | +| `status` | string | yes | `draft` or `published` | +| `published_at` | datetime | no | An explicit date/datetime, e.g. `2026-08-02` or `2026-08-02 12:00:00` or ISO 8601 `2026-08-02T12:00:00Z`. Relative words like `now` or `tomorrow` are **not** accepted. | +| `meta_title` | string | no | max 60 | +| `meta_description` | string | no | max 160 | +| `tags` | array\ | no | tag IDs, replaces the post's tag list | + +**Publish a post right now:** + +```bash +curl -X POST {{BASE_URL}}/api/posts \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" -H "Accept: application/json" \ + -d '{ + "title": "How AI Agents Can Write Blog Posts", + "body": "## Intro\n\nMarkdown body goes here...", + "excerpt": "A short summary shown in listings.", + "category_id": 1, + "status": "published", + "published_at": "2026-08-02 12:00:00", + "tags": [3, 7] + }' +``` + +### `GET /api/posts/{id}` · `PUT/PATCH /api/posts/{id}` · `DELETE /api/posts/{id}` + +Update accepts the same fields as create, all optional (`sometimes` validated). Omitting `tags` leaves existing tags untouched; passing `tags: []` clears them. + +## Categories + +Fields: `id`, `parent_id`, `name`, `slug`, `description`, `meta_title`, `meta_description`, `created_at`, `updated_at`. Categories can be nested one level via `parent_id`. + +### `GET /api/categories` · `POST /api/categories` · `GET/PUT/PATCH/DELETE /api/categories/{id}` + +| Field | Type | Required | Notes | +|---|---|---|---| +| `name` | string | yes | max 255 | +| `slug` | string | no | must be unique; auto-generated from `name` if omitted | +| `parent_id` | integer | no | must exist in `/api/categories` | +| `description` | string | no | | +| `meta_title` | string | no | max 60 | +| `meta_description` | string | no | max 160 | + +## Tags + +Fields: `id`, `name`, `slug`, `created_at`, `updated_at`. + +### `GET /api/tags` · `POST /api/tags` · `GET/PUT/PATCH/DELETE /api/tags/{id}` + +| Field | Type | Required | Notes | +|---|---|---|---| +| `name` | string | yes | max 255 | +| `slug` | string | no | must be unique; auto-generated from `name` if omitted | + +Tip: before creating a post, `GET /api/tags` and `GET /api/categories` to find existing IDs to reuse, rather than creating duplicates. + +## AI Tools + +A directory of AI tools shown at `/tools` on the public site. Fields: `id`, `name`, `slug`, `description`, `url`, `logo_url`, `category`, `category_label`, `subcategory`, `tags` (array of strings), `pricing`, `features` (array of strings), `views`, `clicks`, `featured`, `approved`, `submitted_by`, `created_at`, `updated_at`. + +**A tool is only visible on the public site once `approved` is `true`.** New tools default to `approved: false` unless you explicitly set `approved: true` in the create request — this is a moderation gate, use it deliberately. + +### `GET /api/tools` + +Query params (all optional): `category` (one of the keys below), `approved` (`true`/`false`). + +Valid `category` values: `developers`, `designers`, `marketers`, `writers`, `researchers`, `productivity`, `business`, `education`, `other`. + +### `POST /api/tools` + +| Field | Type | Required | Notes | +|---|---|---|---| +| `name` | string | yes | max 255 | +| `slug` | string | no | must be unique; auto-generated from `name` if omitted | +| `description` | string | yes | | +| `url` | string (URL) | yes | link to the tool | +| `logo_url` | string (URL) | no | | +| `category` | string | yes | one of the category keys above | +| `subcategory` | string | no | free text, e.g. `"Code Assistants"` | +| `tags` | array\ | no | e.g. `["free", "api", "open-source"]` | +| `pricing` | string | no | free text, e.g. `"Free"`, `"$10/month"` | +| `features` | array\ | no | short bullet points | +| `featured` | boolean | no | defaults to `false` | +| `approved` | boolean | no | defaults to `false` — see note above | + +**Submit a tool listing:** + +```bash +curl -X POST {{BASE_URL}}/api/tools \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" -H "Accept: application/json" \ + -d '{ + "name": "Example AI Copilot", + "description": "An AI pair programmer that lives in your editor.", + "url": "https://example.com", + "category": "developers", + "subcategory": "Code Assistants", + "pricing": "Freemium", + "tags": ["free", "api"], + "features": ["Inline code suggestions", "Chat with your codebase"], + "approved": true + }' +``` + +`views` and `clicks` are tracked automatically by the public site and cannot be set via the API. + +### `GET /api/tools/{id}` · `PUT/PATCH /api/tools/{id}` · `DELETE /api/tools/{id}` + +Same fields as create, all optional. + +## End-to-end example: publish a post as an agent + +```bash +BASE_URL="{{BASE_URL}}" + +# 1. Authenticate +TOKEN=$(curl -s -X POST $BASE_URL/api/login \ + -H "Content-Type: application/json" -H "Accept: application/json" \ + -d '{"email":"you@example.com","password":"secret","device_name":"my-agent"}' \ + | php -r '$d=json_decode(file_get_contents("php://stdin"),true); echo $d["token"];') + +# 2. (Optional) find a category to use +curl -s "$BASE_URL/api/categories" -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" + +# 3. Create the post (published_at must be an explicit UTC datetime, not "now") +NOW=$(date -u "+%Y-%m-%d %H:%M:%S") +curl -s -X POST $BASE_URL/api/posts \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" -H "Accept: application/json" \ + -d "{ + \"title\": \"My First Automated Post\", + \"body\": \"Written entirely by an agent.\", + \"status\": \"published\", + \"published_at\": \"$NOW\" + }" +``` + +The response includes the post's `slug`; the live URL is `{{BASE_URL}}/blog/`. diff --git a/resources/views/api-docs.blade.php b/resources/views/api-docs.blade.php new file mode 100644 index 0000000..4460449 --- /dev/null +++ b/resources/views/api-docs.blade.php @@ -0,0 +1,17 @@ + + @push('jsonld') + + @endpush + +
+ {!! $bodyHtml !!} +
+
diff --git a/resources/views/blog/category.blade.php b/resources/views/blog/category.blade.php index 97a8b76..837aaaa 100644 --- a/resources/views/blog/category.blade.php +++ b/resources/views/blog/category.blade.php @@ -2,7 +2,7 @@ @push('jsonld')