diff --git a/CHANGELOG.md b/CHANGELOG.md
index f03a880..bcd36b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,25 @@
All notable changes to this project will be documented in this file.
+## [2.1.0] - Unreleased
+
+### Added
+- `Relaticle\Ink\Support\BlogListingSeo` helper for building per-page `SEOData` for listings. Headless consumers can call `BlogListingSeo::forIndex/forCategory/forTag` from their own controllers.
+- Auto-emit `FAQPage` JSON-LD on post pages when content contains an `## FAQ` H2 followed by `### Question?` / answer-paragraph pairs. Controlled by `schema.faq_auto` config (default `true`).
+- Auto-emit `HowTo` JSON-LD on post pages when content contains a `## Steps` H2 followed by `### Step Name` / paragraph pairs. Opt-in via `schema.howto_auto` config (default `false`).
+- `Relaticle\Ink\Support\SchemaExtractor` helper for FAQ + HowTo HTML parsing.
+- Auto-emit `Blog` and `CollectionPage` JSON-LD on listing routes (`blog.index` emits both; `blog.category` and `blog.tag` emit `CollectionPage`).
+- Numbered, aria-labeled pagination view at `ink::pagination.blog`. Listing pages (index/category/tag) use it by default. Publish via `php artisan vendor:publish --tag=ink-views` to customize.
+- `wire:navigate` on the `` post-link and pagination links for SPA-feel navigation in Livewire 4 hosts. No-op when Livewire is not present.
+- `Post::search($term)` query scope. Defaults to a portable LIKE search across title/excerpt/content. Override via `search.callback` config for Postgres FTS, MySQL FULLTEXT, Scout, etc.
+- The shipped `/blog` route now honors `?q=` for search. The existing `BlogListingSeo::forIndex(searchQuery:)` call ensures `noindex,follow` is set on search result URLs.
+- `BlogSearch` Livewire component (``) with URL-synced `?q=` query, 400ms debounce, and empty state. Theme-agnostic — publish the view at `resources/views/vendor/ink/livewire/blog-search.blade.php` to restyle.
+
+### Fixed
+- `BlogSitemapGenerator` now includes `/blog/tag/{slug}` URLs when the tags feature is enabled and the tag has published posts.
+- Listing routes (`/blog`, `/blog/category/{slug}`, `/blog/tag/{slug}`) now emit per-page canonical URLs and page-aware titles. Previously every paginated page canonicalized to page 1, causing Google to treat pages 2+ as duplicate content.
+- Shipped `show` and `preview` routes now call `seo()->for($post)` so post-attached SEO (BlogPosting + BreadcrumbList + FAQPage JSON-LD) actually renders in public-routes mode. Previously the schema only worked for consumers who overrode the controller.
+
## [2.0.0] - Unreleased
### Changed (BREAKING)
diff --git a/README.md b/README.md
index b364152..ea6bbf4 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,9 @@ Filament-native content publishing for blog, docs, and AI-citable articles. Ship
## Features
- **Filament Admin** — PostResource and CategoryResource with markdown editor, draft/published/scheduled UX, SEO fields, featured images, and bulk publish/unpublish/schedule actions
-- **SEO Components** — Meta tags, Open Graph, Twitter Cards, JSON-LD structured data, RSS feed, canonical URLs
+- **SEO Components** — Meta tags, Open Graph, Twitter Cards, RSS feed, per-page canonicals on paginated listings
+- **JSON-LD Schema** — `BlogPosting` + `BreadcrumbList` on post pages, `FAQPage` and `HowTo` auto-detected from content (opt-in), `Blog` + `CollectionPage` on listings
+- **Search** — Portable `Post::search()` scope (LIKE by default, override for FTS / Scout), drop-in `BlogSearch` Livewire component with `?q=` URL sync
- **13 MCP Tools** — Full CRUD for posts and categories via Model Context Protocol, with markdown sanitization (HTML stripped, unsafe links blocked)
- **Publishable UI Components** — Post card, header, body, related posts, category badge, preview banner — all with dark mode
- **Two install modes**
diff --git a/config/ink.php b/config/ink.php
index 94c2e90..4f1af95 100644
--- a/config/ink.php
+++ b/config/ink.php
@@ -32,6 +32,15 @@
'logo' => null,
],
+ 'schema' => [
+ 'faq_auto' => true,
+ 'howto_auto' => false,
+ ],
+
+ 'search' => [
+ 'callback' => null,
+ ],
+
'tables' => [
'posts' => 'blog_posts',
'categories' => 'blog_categories',
diff --git a/docs/content/2.essentials/1.blade-components.md b/docs/content/2.essentials/1.blade-components.md
index 468d745..d8f675d 100644
--- a/docs/content/2.essentials/1.blade-components.md
+++ b/docs/content/2.essentials/1.blade-components.md
@@ -100,3 +100,17 @@ Sticky amber banner for draft previews. Pushes `noindex` meta tag.
```blade
```
+
+## Pagination
+
+The package ships a numbered, aria-labeled pagination view used by the index/category/tag pages. To use it in your own views:
+
+```blade
+{{ $posts->links('ink::pagination.blog') }}
+```
+
+The view emits `aria-label="Blog pagination"` on the nav, `aria-current="page"` on the active page, and `wire:navigate` on every link.
+
+## Search (Livewire)
+
+Drop `` anywhere. It reads `?q=` from the URL, debounces 400ms, and renders up to 20 matching posts. Empty queries show no results until the user types.
diff --git a/docs/content/2.essentials/4.configuration.md b/docs/content/2.essentials/4.configuration.md
index 9bb531b..d7f398c 100644
--- a/docs/content/2.essentials/4.configuration.md
+++ b/docs/content/2.essentials/4.configuration.md
@@ -82,6 +82,31 @@ return [
'media_library' => false,
],
+ /*
+ |--------------------------------------------------------------------------
+ | Schema auto-emission
+ |--------------------------------------------------------------------------
+ | Detect FAQ and HowTo sections in post content and emit JSON-LD schema
+ | automatically. FAQ detection looks for an `## FAQ` H2 followed by H3
+ | question / paragraph answer pairs. HowTo detection looks for a `## Steps`
+ | H2 followed by H3 step headings.
+ */
+ 'schema' => [
+ 'faq_auto' => true,
+ 'howto_auto' => false,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Search
+ |--------------------------------------------------------------------------
+ | Defaults to a portable LIKE search across title/excerpt/content.
+ | Override `callback` to use Postgres FTS, MySQL FULLTEXT, Scout, etc.
+ */
+ 'search' => [
+ 'callback' => null,
+ ],
+
/*
|--------------------------------------------------------------------------
| RSS feed metadata
diff --git a/docs/content/2.essentials/7.search.md b/docs/content/2.essentials/7.search.md
new file mode 100644
index 0000000..968d14a
--- /dev/null
+++ b/docs/content/2.essentials/7.search.md
@@ -0,0 +1,55 @@
+---
+title: Search
+description: Portable search across blog posts with a config-driven backend hook.
+navigation:
+ icon: i-lucide-search
+---
+
+The package ships a `Post::search($term)` query scope and a `BlogSearch` Livewire component.
+
+## Default behavior
+
+`Post::search($term)` does a `LIKE` search across `title`, `excerpt`, and `content`. Works on every database driver out of the box.
+
+```php
+$posts = Post::query()
+ ->published()
+ ->search($request->query('q', ''))
+ ->latest('published_at')
+ ->paginate(12);
+```
+
+Empty / whitespace-only terms are no-ops — the scope leaves the query unchanged. LIKE wildcards in the term are escaped.
+
+## Override the backend
+
+Set `search.callback` in `config/ink.php` to use Postgres FTS, MySQL FULLTEXT, Scout, Meilisearch, or anything else:
+
+```php
+'search' => [
+ 'callback' => function (\Illuminate\Database\Eloquent\Builder $query, string $term): void {
+ $query->whereRaw(
+ "to_tsvector('english', title || ' ' || excerpt || ' ' || content) @@ plainto_tsquery('english', ?)",
+ [$term]
+ );
+ },
+],
+```
+
+Whatever the callback applies becomes the search strategy. Empty terms still short-circuit before the callback fires.
+
+## Livewire component
+
+Use `` anywhere. It reads `?q=` from the URL, debounces 400ms, and renders up to 20 matches with an empty state.
+
+To restyle, publish the view:
+
+```bash
+php artisan vendor:publish --tag=ink-views
+```
+
+Then edit `resources/views/vendor/ink/livewire/blog-search.blade.php`.
+
+## SEO
+
+When the shipped `/blog` route handles `?q=`, the package emits `` on the search result page. Search queries should not be indexed as separate URLs.
diff --git a/docs/content/2.essentials/8.schema.md b/docs/content/2.essentials/8.schema.md
new file mode 100644
index 0000000..edc3de5
--- /dev/null
+++ b/docs/content/2.essentials/8.schema.md
@@ -0,0 +1,64 @@
+---
+title: Schema
+description: Automatic JSON-LD schema emission for posts and listings.
+navigation:
+ icon: i-lucide-code-2
+---
+
+The package emits JSON-LD schema automatically for blog content. Each schema type can be toggled in `config/ink.php`.
+
+## What's emitted by default
+
+| Schema | Where | Toggle |
+|---|---|---|
+| `BlogPosting` | Post pages | Always on (via `Post::getDynamicSEOData()`) |
+| `BreadcrumbList` | Post pages | Always on |
+| `FAQPage` | Post pages with `## FAQ` section | `schema.faq_auto` (default `true`) |
+| `HowTo` | Post pages with `## Steps` section | `schema.howto_auto` (default `false`) |
+| `Blog` | `/blog` index | Always when public-routes mode is on |
+| `CollectionPage` | `/blog`, `/blog/category/{slug}`, `/blog/tag/{slug}` | Always when public-routes mode is on |
+
+## FAQPage conventions
+
+To trigger FAQPage emission, include a `## FAQ` heading followed by `### Question?` and a paragraph answer:
+
+```markdown
+## FAQ
+
+### Does the package auto-emit schema?
+Yes — FAQ schema is on by default.
+
+### Can I turn it off?
+Set `schema.faq_auto` to `false`.
+```
+
+## HowTo conventions
+
+To trigger HowTo emission, opt in via `schema.howto_auto = true` and write a `## Steps` section with `### Step Name` headings:
+
+```markdown
+## Steps
+
+### Install the package
+Run `composer require relaticle/ink`.
+
+### Publish the config
+Run `php artisan vendor:publish --tag=ink-config`.
+```
+
+Each `### heading` becomes a `HowToStep` with auto-incremented `position`.
+
+## Disabling auto-detection
+
+```php
+'schema' => [
+ 'faq_auto' => false,
+ 'howto_auto' => false,
+],
+```
+
+The `BlogPosting` + `BreadcrumbList` schemas remain on regardless — they're considered baseline for any blog.
+
+## Validate your schema
+
+After publishing, paste a post URL into [Google Rich Results Test](https://search.google.com/test/rich-results) or the [Schema.org Validator](https://validator.schema.org/) to confirm zero errors.
diff --git a/docs/content/2.essentials/9.listing-seo.md b/docs/content/2.essentials/9.listing-seo.md
new file mode 100644
index 0000000..3b78e51
--- /dev/null
+++ b/docs/content/2.essentials/9.listing-seo.md
@@ -0,0 +1,48 @@
+---
+title: Listing SEO
+description: Per-page canonicals, titles, and metadata for listing routes.
+navigation:
+ icon: i-lucide-list
+---
+
+The package's listing routes (`/blog`, `/blog/category/{slug}`, `/blog/tag/{slug}`) emit per-page canonical URLs, titles, and metadata via `BlogListingSeo`.
+
+## Behavior
+
+| Route | Page 1 canonical | Page N canonical |
+|---|---|---|
+| `/blog` | `/blog` | `/blog?page=N` |
+| `/blog?q=term` | `/blog` | `noindex,follow` |
+| `/blog/category/guides` | `/blog/category/guides` | `/blog/category/guides?page=N` |
+| `/blog/tag/filament` | `/blog/tag/filament` | `/blog/tag/filament?page=N` |
+
+Titles include `— Page N` from page 2 onward.
+
+## Headless consumers
+
+If you write your own controllers in headless mode, call the helper directly:
+
+```php
+use Relaticle\Ink\Support\BlogListingSeo;
+
+public function index(Request $request)
+{
+ $page = (int) $request->query('page', 1);
+ seo()->for(BlogListingSeo::forIndex(page: $page, searchQuery: $request->query('q')));
+
+ return view('blog.index', [...]);
+}
+```
+
+Available factories:
+- `BlogListingSeo::forIndex(int $page = 1, ?string $searchQuery = null): SEOData`
+- `BlogListingSeo::forCategory(Category $category, int $page = 1): SEOData`
+- `BlogListingSeo::forTag(Tag $tag, int $page = 1): SEOData`
+
+## Pagination
+
+The package ships a numbered, aria-labeled pagination view at `ink::pagination.blog`. Listing pages use it by default. Publish to customize:
+
+```bash
+php artisan vendor:publish --tag=ink-views
+```
diff --git a/resources/views/components/post-card.blade.php b/resources/views/components/post-card.blade.php
index 31631fa..f470d12 100644
--- a/resources/views/components/post-card.blade.php
+++ b/resources/views/components/post-card.blade.php
@@ -1,4 +1,4 @@
-
+