본문으로 건너뛰기

SEO Provider 개발 가이드

Seo plugin은 기본 site 설정만으로도 동작하지만, 콘텐츠 모델과 독립사이트 scope가 의미 있는 SEO를 제공하려면 provider를 등록해야 합니다.

Entity SEO Provider​

Board Post, Commerce Product, Article 같은 콘텐츠 모델은 SeoMetaProviderInterface로 page/entity SEO를 제공합니다.

<?php

declare(strict_types=1);

namespace App\Sites\Newspaper\Seo;

use App\Plugins\Seo\Contracts\SeoMetaProviderInterface;
use App\Plugins\Seo\Data\SeoContext;
use App\Sites\Newspaper\Models\Article;

final class ArticleSeoProvider implements SeoMetaProviderInterface
{
public function supports(mixed $subject): bool
{
return $subject instanceof Article;
}

public function metaFor(mixed $subject, SeoContext $base): SeoContext
{
return $base->merge([
'title' => $subject->title,
'description' => $subject->excerpt,
'canonical_url' => route('articles.show', $subject, absolute: true),
'article' => [
'headline' => $subject->title,
'datePublished' => $subject->published_at?->toAtomString(),
'author' => ['type' => 'Person', 'name' => $subject->author_name],
],
]);
}
}

기존 모델이 seoMeta()를 제공하면 HasSeoMetaProvider bridge가 읽을 수 있습니다. 단, provider는 raw HTML이나 stringified JSON-LD를 반환하지 않습니다.

Scope Provider​

동적 scope/page 설정은 SeoScopeProviderInterface로 제공합니다.

<?php

declare(strict_types=1);

namespace App\Sites\Scripture\Seo;

use App\Plugins\Seo\Contracts\SeoScopeProviderInterface;
use App\Plugins\Seo\Data\SeoScopeMatch;

final class ScriptureScopeProvider implements SeoScopeProviderInterface
{
public function supports(SeoScopeMatch $match): bool
{
return $match->siteSlug === 'scripture';
}

public function scopeSettings(SeoScopeMatch $match): ?array
{
return match ($match->scopeSlug) {
'dictionary' => [
'title' => '성경 사전',
'description' => '성경 용어와 원어 정보를 찾는 사전입니다.',
],
'typing' => [
'title' => '성경 타자연습',
'description' => '성경 구절로 타자 연습을 진행합니다.',
],
default => null,
};
}

public function pageSettings(SeoScopeMatch $match): ?array
{
return null;
}
}

등록:

app(\App\Plugins\Seo\Services\Scope\SeoScopeProviderRegistry::class)
->register(new \App\Sites\Scripture\Seo\ScriptureScopeProvider());

Sitemap Provider​

콘텐츠 URL은 SeoSitemapProviderInterface로 sitemap에 공급합니다.

<?php

declare(strict_types=1);

namespace App\Sites\Newspaper\Seo;

use App\Plugins\Seo\Contracts\SeoSitemapProviderInterface;
use App\Plugins\Seo\Data\SeoContext;
use App\Plugins\Seo\Data\SeoSitemapUrl;
use App\Sites\Newspaper\Models\Article;
use Illuminate\Http\Request;

final class ArticleSitemapProvider implements SeoSitemapProviderInterface
{
public function urls(SeoContext $context, Request $request): iterable
{
foreach (Article::query()->published()->cursor() as $article) {
yield new SeoSitemapUrl(
loc: route('articles.show', $article, absolute: true),
lastmod: $article->updated_at?->toDateString(),
changefreq: 'daily',
priority: 0.8,
);
}
}
}

등록:

app(\App\Plugins\Seo\Services\Sitemap\SitemapRegistry::class)
->register(new \App\Sites\Newspaper\Seo\ArticleSitemapProvider());

Structured Data Provider​

schema.org JSON-LD는 StructuredDataProviderInterface로 PHP array를 반환합니다.

<?php

declare(strict_types=1);

namespace App\Sites\Newspaper\Seo;

use App\Plugins\Seo\Contracts\StructuredDataProviderInterface;
use App\Plugins\Seo\Data\SeoContext;

final class NewspaperArticleStructuredDataProvider implements StructuredDataProviderInterface
{
public function graph(SeoContext $context): array
{
$data = $context->toArray();
$article = $data['article'] ?? null;

if (! is_array($article)) {
return [];
}

return [[
'@type' => 'NewsArticle',
'@id' => ($data['canonical_url'] ?? '').'#article',
'headline' => $article['headline'] ?? $data['title'] ?? null,
'datePublished' => $article['datePublished'] ?? null,
'author' => $article['author'] ?? null,
]];
}
}

등록:

app(\App\Plugins\Seo\Services\StructuredData\StructuredDataRegistry::class)
->register(new \App\Sites\Newspaper\Seo\NewspaperArticleStructuredDataProvider());

예시 파일​

plugin 폴더의 docs/examples를 참고하세요.

  • BoardPostStructuredDataProviderExample.php
  • CommerceProductStructuredDataProviderExample.php
  • NewspaperArticleStructuredDataProviderExample.php
  • ScriptureScopeProviderExample.php
  • ScriptureStructuredDataProviderExample.php

테스트​

SEO plugin 테스트는 _template 격리 DB를 사용하는 make test로 실행합니다.

make test NAME=_template ARGS="app/Plugins/Seo/tests/Unit"

provider를 SaaS에 붙이면 해당 SaaS의 route/controller feature test도 함께 추가하세요.