-
Notifications
You must be signed in to change notification settings - Fork 0
SEO
Server-rendered Blade is the whole reason Atelier is shaped the way it is, so the SEO layer is not an add-on. This page covers what ships, what you configure, and how to put URLs in the sitemap that Atelier knows nothing about.
Rendered into the head, per locale, with no configuration:
-
<title>, falling back to the page title when no meta title is set <meta name="description">-
<link rel="canonical">, the page's own URL unless you override it -
<link rel="alternate" hreflang>between every locale - Open Graph:
og:type,og:title,og:description,og:url,og:image - Twitter:
summary_large_imagewhen there is a share image,summarywhen there is not
Previews carry noindex, nofollow as both a meta tag and an X-Robots-Tag header, and are
never cached.
atelier::partials.meta. Point atelier.layout at your own Blade view without including
it and every page renders perfectly with no head at all. See
Using your own layout.
Page settings, a tab per locale:
| Field | Notes |
|---|---|
| Meta title | Around 60 characters. Falls back to the page title. |
| Meta description | Around 155 characters. |
| Social share image | 1200 by 630 is the safe size. |
| Hide from search engines | Adds noindex and drops the URL from the sitemap. |
| Tell search engines not to follow its links | Adds nofollow. Independent of the above. |
| Canonical URL | Empty means the page's own URL. |
The robots tag is only emitted when it has something to say. index, follow is what every
crawler assumes, so a tag repeating it is noise.
Indexing is decided per locale, not per page. An English page can be listed while its
Arabic translation is hidden, and the hreflang alternate pointing at the hidden one drops
with it, because an alternate pointing at a noindexed URL is the same mistake as listing it.
/sitemap.xml, generated per request from the pages table. Every published, indexable page
in every locale, with xhtml:link alternates and lastmod from the publish time. Drafts,
unpublished pages and previews never appear.
Open it in a browser and a stylesheet renders it as a table of URLs, dates and locales. Crawlers ignore the stylesheet and read the XML underneath.
There is no cache to clear and nothing to regenerate on publish.
A real client site is rarely only Atelier pages. There is usually a blog, a services catalogue or a careers section with its own model, its own panel resource and its own routes. Those URLs belong in the same sitemap, and Atelier cannot discover them, so you hand them over.
Say the site has a Post model with its own Filament resource, on its own nav tab, with
routes that have nothing to do with Atelier:
// routes/web.php
Route::get('/blog/{post:slug}', [BlogController::class, 'show'])->name('blog.show');// app/Models/Post.php
public function scopePublished(Builder $query): void
{
$query->whereNotNull('published_at')->where('published_at', '<=', now());
}Register a source when you register the plugin:
// app/Providers/Filament/AdminPanelProvider.php
use App\Models\Post;
use Safi\Atelier\AtelierPlugin;
use Safi\Atelier\Blocks\DefaultBlocks;
->plugins([
AtelierPlugin::make()
->blocks(DefaultBlocks::all())
->sitemap([
fn () => Post::published()->get()->map(fn (Post $post) => [
'loc' => route('blog.show', $post),
'lastmod' => $post->updated_at,
]),
]),
])That is the whole feature. Every published post now appears in /sitemap.xml alongside the
Atelier pages, with its own last-modified date.
The short form is a plain URL string, which is enough for a handful of fixed routes:
->sitemap([
fn () => [
route('pricing'),
route('contact'),
],
])The long form adds lastmod and per-locale alternates:
->sitemap([
fn () => Post::published()->get()->map(fn (Post $post) => [
'loc' => route('blog.show', $post),
'lastmod' => $post->updated_at,
'alternates' => [
'en' => route('blog.show', $post),
'ar' => route('blog.show', ['post' => $post, 'locale' => 'ar']),
],
]),
])lastmod takes a string or any DateTimeInterface, and is normalised to the ATOM format
sitemaps expect. Anything falsy is left out rather than emitted empty.
A closure in a panel provider stops being the right home quickly. Pass the name of an invokable class instead, resolved from the container, so it can take dependencies and be tested on its own:
->sitemap([
App\Sitemap\PostUrls::class,
App\Sitemap\ServiceUrls::class,
])namespace App\Sitemap;
use App\Models\Post;
class PostUrls
{
public function __invoke(): iterable
{
return Post::published()
->latest('published_at')
->cursor()
->map(fn (Post $post) => [
'loc' => route('blog.show', $post),
'lastmod' => $post->updated_at,
]);
}
}-
Sources run when the sitemap is requested, never at boot. Registering one costs
nothing until somebody asks for
/sitemap.xml, so a source is free to query. -
Atelier does not filter your entries. It will not check whether a post is published,
indexable or reachable, because only your model knows that. Filter in the source. The
published()scope above is doing the real work. -
URLs deduplicate on
loc, first one wins. Listing something Atelier already knows about is harmless. - A source that throws takes the sitemap down with it. That is deliberate. A sitemap quietly missing half a site looks fine and stops your blog being indexed; one that fails is a bug somebody notices.
Nowhere, today. Sources are code, and their URLs appear only in the XML. Open
/sitemap.xml in a browser to check a source is returning what you expect.
Every page emits a JSON-LD graph: the organisation, the site, the page, breadcrumbs, and whatever the page is about. There is a page type select, an FAQ editor that does not depend on blocks, and a way for your own routes to share the same graph.
It has its own page: Structured data.
Served at /robots.txt, pointing at the sitemap and disallowing the preview route and the
panel:
User-agent: *
Disallow: /atelier/preview/
Disallow: /admin/
Sitemap: https://example.com/sitemap.xml
Set the panel path with atelier.robots.disallow_panel, or null to leave it crawlable.
public/robots.txt, and a file on disk is served by the web
server before any route runs. So this route does nothing until you delete that file, or
copy the Sitemap: line into it. A package cannot tell which one you meant.
Changing a published page's slug writes a 301 from the old URL, and the public route consults those before it 404s. Nothing to configure.
The redirect stores the page rather than a target slug, so a page renamed twice sends both old URLs to wherever it lives now, with no chain to follow. A new page claiming a freed slug drops the redirect from it, because whoever claims a slug owns it. An unpublished target 404s instead: sending someone to a 404 is worse than the 404 itself.
Every page exists at /{slug} and /{locale}/{slug}, with hreflang pointing between
them and dir="rtl" plus the Arabic font stack on the RTL side. One tree, one section
order, translated text.
hreflang="x-default" is not emitted yet.
-
ReviewandAggregateRating. The testimonials block could produce them, and deliberately does not: Google ignores reviews a business publishes about itself, and the block has no rating field to aggregate. -
openingHoursSpecificationon a LocalBusiness. Needs a day-and-time repeater. -
Nodes from your own models. A blog resource cannot yet contribute
Articlenodes for its own routes, the way it can already contribute sitemap URLs. -
x-defaulton the hreflang set. - A sitemap index. One sitemap, and the format's limit is 50,000 URLs.
-
changefreqandpriority. Deliberate: Google has said publicly it ignores both.
Building
Running a site
Reference