From 9afc57aeea00a70ed3891bac2deaf9eb6c4a6a97 Mon Sep 17 00:00:00 2001 From: Quentin Gabriele Date: Sat, 25 May 2024 16:29:28 +0200 Subject: [PATCH 01/17] refactor schema, tags and internal logic --- src/Schema/ArticleSchema.php | 3 + src/Schema/BreadcrumbListSchema.php | 3 + src/Schema/CustomSchema.php | 31 ++ src/Schema/Schema.php | 11 +- src/SchemaCollection.php | 79 +++++ src/Support/LinkTag.php | 6 +- src/Support/MetaContentTag.php | 6 +- src/Support/MetaTag.php | 6 +- src/Support/OpenGraphTag.php | 7 +- src/Support/SchemaTagCollection.php | 21 +- src/Support/SitemapTag.php | 12 +- src/Support/Tag.php | 35 ++- src/Support/TwitterCardTag.php | 7 +- src/TagCollection.php | 2 +- src/Tags/DescriptionTag.php | 4 +- src/Tags/TitleTag.php | 7 +- tests/Feature/JSON-LD/ArticleTest.php | 42 +-- tests/Feature/JSON-LD/BreadcrumbListTest.php | 58 ++-- .../Feature/JSON-LD/SchemaCollectionTest.php | 272 ++++++++++++++++++ tests/Feature/Tags/SitemapTagTest.php | 2 +- tests/TestCase.php | 21 ++ tests/Unit/Schema/ArticleSchemaTest.php | 84 +++--- tests/Unit/Schema/CustomSchemaTest.php | 12 + 23 files changed, 591 insertions(+), 140 deletions(-) create mode 100644 src/Schema/CustomSchema.php create mode 100644 tests/Feature/JSON-LD/SchemaCollectionTest.php create mode 100644 tests/Unit/Schema/CustomSchemaTest.php diff --git a/src/Schema/ArticleSchema.php b/src/Schema/ArticleSchema.php index f70587c..4bf0b05 100644 --- a/src/Schema/ArticleSchema.php +++ b/src/Schema/ArticleSchema.php @@ -7,6 +7,9 @@ use Illuminate\Support\HtmlString; use RalphJSmit\Laravel\SEO\Support\SEOData; +/** + * @deprecated Use CustomSchema paradigm + */ class ArticleSchema extends Schema { public array $authors = []; diff --git a/src/Schema/BreadcrumbListSchema.php b/src/Schema/BreadcrumbListSchema.php index 7e48ed1..3251036 100644 --- a/src/Schema/BreadcrumbListSchema.php +++ b/src/Schema/BreadcrumbListSchema.php @@ -6,6 +6,9 @@ use Illuminate\Support\HtmlString; use RalphJSmit\Laravel\SEO\Support\SEOData; +/** + * @deprecated Use CustomSchema paradigm + */ class BreadcrumbListSchema extends Schema { public Collection $breadcrumbs; diff --git a/src/Schema/CustomSchema.php b/src/Schema/CustomSchema.php new file mode 100644 index 0000000..3eaebcd --- /dev/null +++ b/src/Schema/CustomSchema.php @@ -0,0 +1,31 @@ + 'application/ld+json', + ]; + + public null|string|HtmlString $inner; + + function __construct(iterable|Arrayable $inner) + { + $this->inner = new HtmlString( + collect($inner)->toJson() + ); + } +} diff --git a/src/Schema/Schema.php b/src/Schema/Schema.php index 265a9d3..1fd1bc1 100644 --- a/src/Schema/Schema.php +++ b/src/Schema/Schema.php @@ -9,24 +9,23 @@ use RalphJSmit\Laravel\SEO\Support\SEOData; use RalphJSmit\Laravel\SEO\Support\Tag; +/** + * @deprecated Use CustomSchema paradigm + */ abstract class Schema extends Tag { use Pipeable; + public string $tag = 'script'; + public array $attributes = [ 'type' => 'application/ld+json', ]; - public string $context = 'https://schema.org/'; - public Collection $markup; public array $markupTransformers = []; - public string $tag = 'script'; - - public HtmlString $inner; - public function __construct(SEOData $SEOData, array $markupBuilders = []) { $this->initializeMarkup($SEOData, $markupBuilders); diff --git a/src/SchemaCollection.php b/src/SchemaCollection.php index 3d9c6cb..306660c 100644 --- a/src/SchemaCollection.php +++ b/src/SchemaCollection.php @@ -3,12 +3,19 @@ namespace RalphJSmit\Laravel\SEO; use Closure; +use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Collection; use RalphJSmit\Laravel\SEO\Schema\ArticleSchema; use RalphJSmit\Laravel\SEO\Schema\BreadcrumbListSchema; use RalphJSmit\Laravel\SEO\Schema\FaqPageSchema; use RalphJSmit\Laravel\SEO\Schema\Schema; +use RalphJSmit\Laravel\SEO\Support\SEOData; +/** + * @template TKey of array-key + * + * @extends Collection + */ class SchemaCollection extends Collection { protected array $dictionary = [ @@ -19,6 +26,9 @@ class SchemaCollection extends Collection public array $markup = []; + /** + * @deprecated use withArticle instead + */ public function addArticle(?Closure $builder = null): static { $this->markup[$this->dictionary['article']][] = $builder ?: fn (Schema $schema): Schema => $schema; @@ -40,6 +50,75 @@ public function addFaqPage(?Closure $builder = null): static return $this; } + /** + * @param null|(Closure(SEOData $SEOData, Collection $article): Collection) $builder + */ + function withArticle(null|array|Closure $builder = null): static + { + return $this->add(function (SEOData $SEOData) use ($builder) { + $schema = collect([ + '@context' => 'https://schema.org', + '@type' => 'Article', + 'mainEntityOfPage' => [ + '@type' => 'WebPage', + '@id' => $SEOData->url, + ], + 'datePublished' => $SEOData->published_time->toIso8601String(), + 'dateModified' => $SEOData->modified_time->toIso8601String(), + 'headline' => $SEOData->title, + 'description' => $SEOData->description, + 'image' => $SEOData->image, + ])->when($SEOData->author, fn (Collection $schema) => $schema->put('author', [[ + '@type' => 'Person', + 'name' => $SEOData->author, + ]])); + + if ($builder) { + $schema = $builder($SEOData, $schema); + } + + return $schema->filter(); + }); + } + + /** + * @param null|(Closure(SEOData $SEOData, Collection $breadcrumbList): Collection) $builder + */ + function withBreadcrumbList(null|array|Closure $builder = null): static + { + return $this->add(function (SEOData $SEOData) use ($builder) { + $schema = collect([ + '@context' => 'https://schema.org', + '@type' => 'BreadcrumbList', + 'itemListElement' => collect([ + [ + '@type' => 'ListItem', + 'name' => $SEOData->title, + 'item' => $SEOData->url, + 'position' => 1, + ] + ]), + ]); + + if ($builder) { + $schema = $builder($SEOData, $schema); + + /** + * Make sure position are in the right order after builder manipulation + */ + $schema->put( + 'itemListElement', + collect($schema->get('itemListElement', [])) + ->values() + ->map(fn (array $item, int $key) => [...$item, 'position' => $key + 1]) + ); + } + + + return $schema->filter(); + }); + } + public static function initialize(): static { return new static(); diff --git a/src/Support/LinkTag.php b/src/Support/LinkTag.php index 2523a03..fd5a100 100644 --- a/src/Support/LinkTag.php +++ b/src/Support/LinkTag.php @@ -7,8 +7,10 @@ class LinkTag extends Tag public string $tag = 'link'; public function __construct( - public string $rel, - public string $href, + string $rel, + string $href, ) { + $this->attributes['rel'] = $rel; + $this->attributes['href'] = $href; } } diff --git a/src/Support/MetaContentTag.php b/src/Support/MetaContentTag.php index 86f2164..9fe3011 100644 --- a/src/Support/MetaContentTag.php +++ b/src/Support/MetaContentTag.php @@ -7,8 +7,10 @@ class MetaContentTag extends Tag public string $tag = 'meta'; public function __construct( - public string $property, - public string $content, + string $property, + string $content, ) { + $this->attributes['property'] = $property; + $this->attributes['content'] = $content; } } diff --git a/src/Support/MetaTag.php b/src/Support/MetaTag.php index e895727..35ae356 100644 --- a/src/Support/MetaTag.php +++ b/src/Support/MetaTag.php @@ -7,8 +7,10 @@ class MetaTag extends Tag public string $tag = 'meta'; public function __construct( - public string $name, - public string $content, + string $name, + string $content, ) { + $this->attributes['name'] = $name; + $this->attributes['content'] = $content; } } diff --git a/src/Support/OpenGraphTag.php b/src/Support/OpenGraphTag.php index 20acb61..1cd2a2e 100644 --- a/src/Support/OpenGraphTag.php +++ b/src/Support/OpenGraphTag.php @@ -9,9 +9,12 @@ class OpenGraphTag extends Tag public string $tag = 'meta'; public function __construct( - public string $property, - public string $content, + string $property, + string $content, ) { + $this->attributes['property'] = $property; + $this->attributes['content'] = $content; + $this->attributesPipeline[] = function (Collection $collection) { return $collection->mapWithKeys(function ($value, $key) { if ($key === 'property') { diff --git a/src/Support/SchemaTagCollection.php b/src/Support/SchemaTagCollection.php index df52c01..3546868 100644 --- a/src/Support/SchemaTagCollection.php +++ b/src/Support/SchemaTagCollection.php @@ -2,24 +2,33 @@ namespace RalphJSmit\Laravel\SEO\Support; +use Closure; use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Collection; -use RalphJSmit\Laravel\SEO\SchemaCollection; +use RalphJSmit\Laravel\SEO\Schema\CustomSchema; class SchemaTagCollection extends Collection implements Renderable { use RenderableCollection; - public static function initialize(SEOData $SEOData, ?SchemaCollection $schema = null): ?static + public static function initialize(?SEOData $SEOData = null): ?static { - $collection = new static(); + $schemas = $SEOData?->schema; - if (! $schema) { + if (!$schemas) { return null; } - foreach ($schema->markup as $markupClass => $markupBuilders) { - $collection = $collection->push(new $markupClass($SEOData, $markupBuilders)); + $collection = new static(); + + foreach ($schemas as $schema) { + $collection->push( + new CustomSchema(value($schema, $SEOData)) + ); + } + + foreach ($schemas->markup as $markupClass => $markupBuilders) { + $collection->push(new $markupClass($SEOData, $markupBuilders)); } return $collection; diff --git a/src/Support/SitemapTag.php b/src/Support/SitemapTag.php index c839532..e04fa6c 100644 --- a/src/Support/SitemapTag.php +++ b/src/Support/SitemapTag.php @@ -4,14 +4,16 @@ class SitemapTag extends LinkTag { - public string $rel = 'sitemap'; + public array $attributes = [ + 'type' => 'application/xml', + 'rel' => 'sitemap', + 'title' => 'Sitemap', + ]; - public string $type = 'application/xml'; - - public string $title = 'Sitemap'; public function __construct( - public string $href + string $href ) { + $this->attributes['href'] = $href; } } diff --git a/src/Support/Tag.php b/src/Support/Tag.php index 704ea2e..38ba4d5 100644 --- a/src/Support/Tag.php +++ b/src/Support/Tag.php @@ -5,17 +5,28 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\View\View; use Illuminate\Support\Collection; +use Illuminate\Support\HtmlString; +/** + * A representation of a HTML tag + */ abstract class Tag implements Renderable { - protected static array $reservedAttributes = [ - 'tag', - 'inner', - 'attributesPipeline', - ]; - + /** + * The HTML tag + */ public string $tag; + /** + * The HTML attributes of the tag + */ + public array $attributes = []; + + /** + * The content of the tag + */ + public null|string|HtmlString $inner = null; + public array $attributesPipeline = []; public function render(): View @@ -23,19 +34,15 @@ public function render(): View return view('seo::tags.tag', [ 'tag' => $this->tag, 'attributes' => $this->collectAttributes(), - 'inner' => $this->inner ?? null, + 'inner' => $this->inner, ]); } public function collectAttributes(): Collection { - return collect($this->attributes ?? get_object_vars($this)) - ->except(static::$reservedAttributes) - ->pipe(function (Collection $attributes) { - $reservedAttributes = $attributes->only('property', 'name', 'rel'); - - return $reservedAttributes->merge($attributes->except('property', 'name', 'rel')->sortKeys()); - }) + return collect($this->attributes) + ->map(fn ($attribute) => trim($attribute)) + ->sortKeysUsing(fn ($key) => -array_search($key, ['rel', 'hreflang', 'title', 'name', 'href', 'property', 'description', 'content'])) ->pipeThrough($this->attributesPipeline); } } diff --git a/src/Support/TwitterCardTag.php b/src/Support/TwitterCardTag.php index d3051cf..a8e8da6 100644 --- a/src/Support/TwitterCardTag.php +++ b/src/Support/TwitterCardTag.php @@ -9,9 +9,12 @@ class TwitterCardTag extends Tag public string $tag = 'meta'; public function __construct( - public string $name, - public string $content, + string $name, + string $content, ) { + $this->attributes['name'] = $name; + $this->attributes['content'] = $content; + $this->attributesPipeline[] = function (Collection $collection) { return $collection->mapWithKeys(function ($value, $key) { if ($key === 'name') { diff --git a/src/TagCollection.php b/src/TagCollection.php index 4b451a4..0c95f36 100644 --- a/src/TagCollection.php +++ b/src/TagCollection.php @@ -35,8 +35,8 @@ public static function initialize(?SEOData $SEOData = null): static FaviconTag::initialize($SEOData), OpenGraphTags::initialize($SEOData), TwitterCardTags::initialize($SEOData), - SchemaTagCollection::initialize($SEOData, $SEOData->schema), AlternateTags::initialize($SEOData), + SchemaTagCollection::initialize($SEOData), ])->reject(fn (?Renderable $item): bool => $item === null); foreach ($tags as $tag) { diff --git a/src/Tags/DescriptionTag.php b/src/Tags/DescriptionTag.php index 53b6e62..6b1e5df 100644 --- a/src/Tags/DescriptionTag.php +++ b/src/Tags/DescriptionTag.php @@ -11,13 +11,13 @@ public static function initialize(?SEOData $SEOData): ?MetaTag { $description = $SEOData?->description; - if (! $description) { + if (!$description) { return null; } return new MetaTag( name: 'description', - content: trim($description) + content: $description ); } } diff --git a/src/Tags/TitleTag.php b/src/Tags/TitleTag.php index 13bff8e..843617d 100644 --- a/src/Tags/TitleTag.php +++ b/src/Tags/TitleTag.php @@ -10,20 +10,21 @@ class TitleTag extends Tag public string $tag = 'title'; public function __construct( - public string $inner, + string $inner, ) { + $this->inner = trim($inner); } public static function initialize(?SEOData $SEOData): ?Tag { $title = $SEOData?->title; - if (! $title) { + if (!$title) { return null; } return new static( - inner: trim($title), + inner: $title, ); } } diff --git a/tests/Feature/JSON-LD/ArticleTest.php b/tests/Feature/JSON-LD/ArticleTest.php index 9b2d9f8..276e873 100644 --- a/tests/Feature/JSON-LD/ArticleTest.php +++ b/tests/Feature/JSON-LD/ArticleTest.php @@ -7,7 +7,7 @@ use function Pest\Laravel\get; beforeEach(function () { - if (! file_exists($dir = public_path('images'))) { + if (!file_exists($dir = public_path('images'))) { mkdir($dir, 0777, true); } @@ -39,28 +39,28 @@ ->assertSee('"application/ld+json"', false) ->assertSee( '', + 'image' => secure_url('images/twitter-1743x1743.jpg'), + ]) . '', false ); }); diff --git a/tests/Feature/JSON-LD/BreadcrumbListTest.php b/tests/Feature/JSON-LD/BreadcrumbListTest.php index 4f7c211..fed2759 100644 --- a/tests/Feature/JSON-LD/BreadcrumbListTest.php +++ b/tests/Feature/JSON-LD/BreadcrumbListTest.php @@ -37,36 +37,36 @@ ->assertSee('"application/ld+json"', false) ->assertSee( '', + ]) . '', false ); }); diff --git a/tests/Feature/JSON-LD/SchemaCollectionTest.php b/tests/Feature/JSON-LD/SchemaCollectionTest.php new file mode 100644 index 0000000..a51e476 --- /dev/null +++ b/tests/Feature/JSON-LD/SchemaCollectionTest.php @@ -0,0 +1,272 @@ +faqTestSchema; + + $page::$overrides = [ + 'schema' => SchemaCollection::make()->add($faqPageSchema), + ]; + + get(route('seo.test-page', ['page' => $page])) + ->assertSee('"application/ld+json"', false) + ->assertSee('', false); +}); + +it('can correctly render a custom JSON-LD Schemas markup from a function', function () { + $page = Page::create([]); + + $now = now(); + $yesterday = now()->yesterday(); + + $page::$overrides = [ + 'title' => 'Test title', + 'published_time' => $yesterday, + 'modified_time' => $now, + 'url' => 'https://example.com', + 'author' => 'Ralph J. Smit', + 'schema' => SchemaCollection::make() + ->add(fn (SEOData $SEOData) => [ + '@context' => 'https://schema.org', + '@type' => 'Article', + 'mainEntityOfPage' => [ + '@type' => 'WebPage', + '@id' => $SEOData->url, + ], + 'datePublished' => $SEOData->published_time->toIso8601String(), + 'dateModified' => $SEOData->modified_time->toIso8601String(), + 'headline' => $SEOData->title, + 'author' => [ + [ + '@type' => 'Person', + 'name' => $SEOData->author, + ] + ], + ]), + ]; + + get(route('seo.test-page', ['page' => $page])) + ->assertSee('"application/ld+json"', false) + ->assertSee( + '', + false + ); +}); + +it('can correctly render the JSON-LD Schema markup: Article', function () { + $created_at = now()->subDays(2); + $updated_at = now(); + + $page = Page::create([ + 'created_at' => $created_at, + 'updated_at' => $updated_at, + ]); + + $page::$overrides = [ + 'title' => 'Test title', + 'image' => 'images/twitter-1743x1743.jpg', + 'author' => 'Ralph J. Smit', + 'schema' => SchemaCollection::make()->withArticle(function (SEOData $SEOData, Collection $article) { + return $article->mergeRecursive([ + 'alternativeHeadline' => 'My alternative headline', + 'author' => [ + [ + '@type' => 'Person', + 'name' => 'Second author', + ] + ] + ]); + }), + ]; + + get(route('seo.test-page', ['page' => $page])) + ->assertSee('"application/ld+json"', false) + ->assertSee( + '', + false + ); +}); + + +it('can correctly render the JSON-LD Schema markup: BreadcrumbList', function () { + config()->set('seo.title.suffix', ' | Laravel SEO'); + + $page = Page::create([]); + + $page::$overrides = [ + 'title' => 'Test article', + 'enableTitleSuffix' => true, + 'url' => 'https://example.com/test/article', + 'schema' => SchemaCollection::make()->withBreadcrumbList(function (SEOData $SEOData, Collection $breadcrumb) { + $items = $breadcrumb->get('itemListElement', []); + + $breadcrumb->put( + 'itemListElement', + [ + [ + '@type' => 'ListItem', + 'name' => 'Homepage', + 'item' => 'https://example.com', + ], + [ + '@type' => 'ListItem', + 'name' => 'Category', + 'item' => 'https://example.com/test', + ], + ...$items, + [ + '@type' => 'ListItem', + 'name' => 'Subarticle', + 'item' => 'https://example.com/test/article/2', + ] + ], + ); + + return $breadcrumb; + }), + ]; + + get(route('seo.test-page', ['page' => $page])) + ->assertSee('"application/ld+json"', false) + ->assertSee( + '', + false + ); +}); + +it('can correctly render multiple custom JSON-LD Schemas markup', function () { + $page = Page::create([]); + + $faqPageSchema = $this->faqTestSchema; + + $now = now(); + $yesterday = now()->yesterday(); + + $page::$overrides = [ + 'title' => 'Test title', + 'published_time' => $yesterday, + 'modified_time' => $now, + 'url' => 'https://example.com', + 'author' => 'Ralph J. Smit', + 'schema' => SchemaCollection::make() + ->add($faqPageSchema) + ->add(fn (SEOData $SEOData) => [ + '@context' => 'https://schema.org', + '@type' => 'Article', + 'mainEntityOfPage' => [ + '@type' => 'WebPage', + '@id' => $SEOData->url, + ], + 'datePublished' => $SEOData->published_time->toIso8601String(), + 'dateModified' => $SEOData->modified_time->toIso8601String(), + 'headline' => $SEOData->title, + 'author' => [ + [ + '@type' => 'Person', + 'name' => $SEOData->author, + ] + ], + ]), + ]; + + get(route('seo.test-page', ['page' => $page])) + ->assertSee('"application/ld+json"', false) + ->assertSee('', false) + ->assertSee( + '', + false + ); +}); diff --git a/tests/Feature/Tags/SitemapTagTest.php b/tests/Feature/Tags/SitemapTagTest.php index 369ea1e..d7531fb 100644 --- a/tests/Feature/Tags/SitemapTagTest.php +++ b/tests/Feature/Tags/SitemapTagTest.php @@ -6,5 +6,5 @@ config()->set('seo.sitemap', '/storage/sitemap.xml'); get($url = route('seo.test-plain')) - ->assertSee('', false); + ->assertSee('', false); }); diff --git a/tests/TestCase.php b/tests/TestCase.php index 32cdbb4..36bca20 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,6 +9,27 @@ class TestCase extends Orchestra { + public array $faqTestSchema = [ + '@context' => 'https://schema.org', + '@type' => 'FAQPage', + 'mainEntity' => [ + '@type' => 'Question', + 'name' => 'Can this package add FaqPage to the schema?', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Yes!', + ], + ], + [ + '@type' => 'Question', + 'name' => 'Does it support multiple questions?', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Of course.', + ], + ], + ]; + protected function setUp(): void { parent::setUp(); diff --git a/tests/Unit/Schema/ArticleSchemaTest.php b/tests/Unit/Schema/ArticleSchemaTest.php index 67cbb6e..f54198d 100644 --- a/tests/Unit/Schema/ArticleSchemaTest.php +++ b/tests/Unit/Schema/ArticleSchemaTest.php @@ -18,29 +18,29 @@ }); it('can construct Schema Markup: Article', function () { - $articleSchema = new ArticleSchema($this->SEOData, []); + $articleSchema = new ArticleSchema($this->SEOData); expect((string) $articleSchema->render()) ->toBe( '' + $string = json_encode([ + '@context' => 'https://schema.org', + '@type' => 'Article', + 'mainEntityOfPage' => [ + '@type' => 'WebPage', + '@id' => 'https://example.com/test', + ], + 'datePublished' => now()->subDays(3)->toIso8601String(), + 'dateModified' => now()->toIso8601String(), + 'headline' => 'Test', + 'author' => [ + '@type' => 'Person', + 'name' => 'Ralph J. Smit', + ], + 'description' => 'Description', + 'image' => 'https://example.com/image.jpg', + 'articleBody' => '

Test

', + ]) . '' ); }); @@ -55,30 +55,30 @@ expect((string) $articleSchema->render())->toBe( '' + 'description' => 'Description', + 'image' => 'https://example.com/image.jpg', + 'articleBody' => '

Test

', + 'alternativeHeadline' => 'My alternative headline', + ]) . '' ); }); diff --git a/tests/Unit/Schema/CustomSchemaTest.php b/tests/Unit/Schema/CustomSchemaTest.php new file mode 100644 index 0000000..89f7232 --- /dev/null +++ b/tests/Unit/Schema/CustomSchemaTest.php @@ -0,0 +1,12 @@ +faqTestSchema); + + expect((string) $schema->render()) + ->toBe( + '' + ); +}); From 6366b36a5e8e995712708d9b38a1ee1302132d19 Mon Sep 17 00:00:00 2001 From: Quentin Gabriele Date: Sat, 25 May 2024 17:06:44 +0200 Subject: [PATCH 02/17] fix tag attributes order --- src/Support/AlternateTag.php | 3 ++- src/Support/Tag.php | 14 ++++++++++++- tests/Feature/Tags/AlternateTagTest.php | 4 ++-- tests/Feature/Tags/SitemapTagTest.php | 2 +- tests/Unit/Support/TagTest.php | 28 +++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/Support/TagTest.php diff --git a/src/Support/AlternateTag.php b/src/Support/AlternateTag.php index d32bf56..05bc7a9 100644 --- a/src/Support/AlternateTag.php +++ b/src/Support/AlternateTag.php @@ -5,9 +5,10 @@ class AlternateTag extends LinkTag { public function __construct( - public string $hreflang, + string $hreflang, string $href, ) { parent::__construct('alternate', $href); + $this->attributes['hreflang'] = $hreflang; } } diff --git a/src/Support/Tag.php b/src/Support/Tag.php index 38ba4d5..ccd20a0 100644 --- a/src/Support/Tag.php +++ b/src/Support/Tag.php @@ -12,6 +12,8 @@ */ abstract class Tag implements Renderable { + const ATTRIBUTES_ORDER = ['rel', 'hreflang', 'title', 'name', 'href', 'property', 'description', 'content']; + /** * The HTML tag */ @@ -42,7 +44,17 @@ public function collectAttributes(): Collection { return collect($this->attributes) ->map(fn ($attribute) => trim($attribute)) - ->sortKeysUsing(fn ($key) => -array_search($key, ['rel', 'hreflang', 'title', 'name', 'href', 'property', 'description', 'content'])) + ->sortKeysUsing(function ($a, $b) { + $indexA = array_search($a, static::ATTRIBUTES_ORDER); + $indexB = array_search($b, static::ATTRIBUTES_ORDER); + + return match (true) { + $indexB === $indexA => 0, // keep the order defined in $attributes if neither $a or $b are in ATTRIBUTES_ORDER + $indexA === false => 1, + $indexB === false => -1, + default => $indexA - $indexB + }; + }) ->pipeThrough($this->attributesPipeline); } } diff --git a/tests/Feature/Tags/AlternateTagTest.php b/tests/Feature/Tags/AlternateTagTest.php index 8906847..738d757 100644 --- a/tests/Feature/Tags/AlternateTagTest.php +++ b/tests/Feature/Tags/AlternateTagTest.php @@ -27,6 +27,6 @@ ]; get(route('seo.test-page', ['page' => $page])) - ->assertSee('', false) - ->assertSee('', false); + ->assertSee('', false) + ->assertSee('', false); }); diff --git a/tests/Feature/Tags/SitemapTagTest.php b/tests/Feature/Tags/SitemapTagTest.php index d7531fb..9fae4b6 100644 --- a/tests/Feature/Tags/SitemapTagTest.php +++ b/tests/Feature/Tags/SitemapTagTest.php @@ -6,5 +6,5 @@ config()->set('seo.sitemap', '/storage/sitemap.xml'); get($url = route('seo.test-plain')) - ->assertSee('', false); + ->assertSee('', false); }); diff --git a/tests/Unit/Support/TagTest.php b/tests/Unit/Support/TagTest.php new file mode 100644 index 0000000..229d327 --- /dev/null +++ b/tests/Unit/Support/TagTest.php @@ -0,0 +1,28 @@ +attributes = [ + 'hreflang' => "hreflang", + 'description' => "description", + 'title' => 'title', + 'content' => "content", + 'name' => 'name', + 'href' => 'href', + 'foo' => "foo", + 'property' => "property", + 'bar' => "bar", + 'rel' => "rel", + ]; + + expect((string) $tag->render()) + ->toBe(''); +}); From b46d5eb49252747ab3d53be8ca232ae2b8308e2e Mon Sep 17 00:00:00 2001 From: Quentin Gabriele Date: Sat, 25 May 2024 17:40:31 +0200 Subject: [PATCH 03/17] update readme and remove FAQPage --- README.md | 157 +++++++++++++++++--------- src/Schema/FaqPageSchema.php | 50 -------- src/SchemaCollection.php | 7 -- tests/Feature/JSON-LD/FaqPageTest.php | 59 ---------- 4 files changed, 101 insertions(+), 172 deletions(-) delete mode 100644 src/Schema/FaqPageSchema.php delete mode 100644 tests/Feature/JSON-LD/FaqPageTest.php diff --git a/README.md b/README.md index 2d7485e..11a947c 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ Currently there aren't that many SEO-packages for Laravel and the available ones are quite complex to set up and very decoupled from the database. They only provided you with helpers to generate the tags, but you still had to use those helpers: nothing was generated automatically and they almost do not work out of the box. -This package generates **valid and useful meta tags straight out-of-the-box**, with limited initial configuration, whilst still providing a simple, but powerful API to work with. It can generate: +This package generates **valid and useful meta tags straight out-of-the-box**, with limited initial configuration, while still providing a simple, but powerful API to work with. It can generate: 1. Title tag (with sitewide suffix) 2. Meta tags (author, description, image, robots, etc.) 3. OpenGraph Tags (Facebook, LinkedIn, etc.) 4. Twitter Tags -5. Structured data (Article, Breadcrumbs and FAQPage) +5. Structured data (Article, Breadcrumbs, FAQPage or any custom schema) 6. Favicon 7. Robots tag 8. Alternates links tag @@ -329,19 +329,55 @@ class Homepage extends Controller ## Generating JSON-LD structured data -This package can also **generate structured data** for you (also called schema markup). At the moment we support the following types: +This package can also **generate any structured data** for you (also called schema markup). +Structered data is a very vast subject so we highly recommend you to check the [Google documentation dedicated to it](https://developers.google.com/search/docs/appearance/structured-data/search-gallery). -1. `Article` -2. `BreadcrumbList` -3. `FAQPage` +### Adding your first schema -After generating the structured data it is always a good idea to [test your website with Google's rich result validator](https://search.google.com/test/rich-results). +Let's add the FAQPage schema markup to our website as an example: + +```php +use RalphJSmit\Laravel\SEO\SchemaCollection; + +public function getDynamicSEOData(): SEOData +{ + return new SEOData( + // ... + schema: SchemaCollection::make()->add(fn(SEOData $SEOData) => [ + '@context' => 'https://schema.org', + '@type' => 'FAQPage', + 'mainEntity' => [ + '@type' => 'Question', + 'name' => 'Your question goes here', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Your answer goes here', + ], + ], + ]), + ); +} +``` -However, you can easily send me a (draft) PR with your requested types and I'll (most probably) add them to the package. +> [!TIP] +> When adding a new schema, you can check the [documentation here](https://developers.google.com/search/docs/appearance/structured-data/faqpage) to know what keys to add. + +> [!TIP] +> After generating the structured data it is always a good idea to [test your website with Google's rich result validator](https://search.google.com/test/rich-results). + +### Pre-configured Schema: Article and BreadcrumbList + +To help you getting started with structured data, we added 2 preconfigured schema: + +1. `Article` +2. `BreadcrumbList` ### Article schema markup -To enable structured data, you need to use the `schema` property of the `SEOData` class. To generate `Article` schema markup, use the `->addArticle()` method: +You can add a pre-configured article with the `withArticle` method, this will generate a fully filled Article JSON schema using the values from your `SEOData` instance. + +> [!NOTE] +> Check the Google documentation about [Article](https://developers.google.com/search/docs/appearance/structured-data/article) ```php @@ -351,51 +387,76 @@ public function getDynamicSEOData(): SEOData { return new SEOData( // ... - schema: SchemaCollection::initialize()->addArticle(), + schema: SchemaCollection::make()->withArticle(), ); } ``` -You can pass a closure the the `->addArticle()` method to customize the individual schema markup. This closure will receive an instance of `ArticleSchema` as its argument. You can an additional author by using the `->addAuthor()` method: - -```php -SchemaCollection::initialize()->addArticle( - fn (ArticleSchema $article): ArticleSchema => $article->addAuthor('Second author') -); -``` - -You can completely customize the schema markup by using the `->markup()` method on the `ArticleSchema` instance: +You can also pass a closure to the `->withArticle()` method to customize the individual schema markup. ```php +use RalphJSmit\Laravel\SEO\SchemaCollection; +use RalphJSmit\Laravel\SEO\Support\SEOData; use Illuminate\Support\Collection; -SchemaCollection::initialize()->addArticle(function (ArticleSchema $article): ArticleSchema { - return $article->markup(function(Collection $markup): Collection { - return $markup->put('alternativeHeadline', $this->tagline); - }); -}); +public function getDynamicSEOData(): SEOData +{ + return new SEOData( + // ... + title: "A boring title" + schema: SchemaCollection::make()->withArticle(function(SEOData $SEOData, Collection $article){ + return $article->mergeRecursive([ + 'alternativeHeadline' => "Not {$SEOData->title}", // will be "Not A boring title" + 'author' => [ + [ + '@type' => 'Person', + 'name' => $this->moderator, + ] + ] + ]); + }), + ); +} ``` -At this point, I'm just unable to fluently support every possible version of the structured, so this is the perfect way to add an additional property to the output! - ### BreadcrumbList schema markup -You can also add `BreadcrumbList` schema markup by using the `->addBreadcrumbs()` function on the `SchemaCollection`: +You can also add `BreadcrumbList` schema markup by using the `->withBreadcrumbList()` function on the `SchemaCollection`. + +By default the schema will only contain the current url from `$SEOData->url`. ```php -SchemaCollection::initialize() - ->addBreadcrumbs(function (BreadcrumbListSchema $breadcrumbs): BreadcrumbListSchema { - return $breadcrumbs - ->prependBreadcrumbs([ - 'Homepage' => 'https://example.com', - 'Category' => 'https://example.com/test', - ]) - ->appendBreadcrumbs([ - 'Subarticle' => 'https://example.com/test/article/2', - ]) - ->markup(function(Collection $markup): Collection { - // ... - }); +use RalphJSmit\Laravel\SEO\SchemaCollection; +use RalphJSmit\Laravel\SEO\Support\SEOData; +use Illuminate\Support\Collection; + +SchemaCollection::make() + ->withBreadcrumbList(function (SEOData $SEOData, Collection $breadcrumbs) { + $items = $breadcrumb->get('itemListElement', []); + + $breadcrumb->put( + 'itemListElement', + [ + [ + '@type' => 'ListItem', + 'name' => 'Homepage', + 'item' => 'https://example.com', + ], + [ + '@type' => 'ListItem', + 'name' => 'Category', + 'item' => 'https://example.com/test', + ], + ...$items, + [ + '@type' => 'ListItem', + 'name' => 'Subarticle', + 'item' => 'https://example.com/test/article/2', + ] + ], + ); + + return $breadcrumb; }); ``` @@ -406,22 +467,6 @@ This code will generate `BreadcrumbList` JSON-LD structured data with the follow 3. [Current page] 4. Subarticle -### FAQPage schema markup - -You can also add `FAQPage` schema markup by using the `->addFaqPage()` function on the `SchemaCollection`: - -```php -use RalphJSmit\Laravel\SEO\Schema\FaqPageSchema; -use RalphJSmit\Laravel\SEO\SchemaCollection; - -SchemaCollection::initialize() - ->addFaqPage(function (FaqPageSchema $faqPage): FaqPageSchema { - return $faqPage - ->addQuestion(name: "Can this package add FaqPage to the schema?", acceptedAnswer: "Yes!") - ->addQuestion(name: "Does it support multiple questions?", acceptedAnswer: "Of course."); - }); -``` - ## Advanced usage Sometimes you may have advanced needs, that require you to apply your own logic to the `SEOData` class, just before it is used to generate the tags. diff --git a/src/Schema/FaqPageSchema.php b/src/Schema/FaqPageSchema.php deleted file mode 100644 index 4764d19..0000000 --- a/src/Schema/FaqPageSchema.php +++ /dev/null @@ -1,50 +0,0 @@ -questions[] = [ - '@type' => 'Question', - 'name' => $name, - 'acceptedAnswer' => [ - '@type' => 'Answer', - 'text' => $acceptedAnswer, - ], - ]; - - return $this; - } - - public function initializeMarkup(SEOData $SEOData, array $markupBuilders): void - { - // - } - - public function generateInner(): HtmlString - { - $inner = collect([ - '@context' => 'https://schema.org', - '@type' => $this->type, - 'mainEntity' => $this->questions, - ]) - ->pipeThrough($this->markupTransformers) - ->toJson(); - - return new HtmlString($inner); - } -} diff --git a/src/SchemaCollection.php b/src/SchemaCollection.php index 306660c..75384ca 100644 --- a/src/SchemaCollection.php +++ b/src/SchemaCollection.php @@ -43,13 +43,6 @@ public function addBreadcrumbs(?Closure $builder = null): static return $this; } - public function addFaqPage(?Closure $builder = null): static - { - $this->markup[$this->dictionary['faqPage']][] = $builder ?: fn (Schema $schema): Schema => $schema; - - return $this; - } - /** * @param null|(Closure(SEOData $SEOData, Collection $article): Collection) $builder */ diff --git a/tests/Feature/JSON-LD/FaqPageTest.php b/tests/Feature/JSON-LD/FaqPageTest.php deleted file mode 100644 index 951fc14..0000000 --- a/tests/Feature/JSON-LD/FaqPageTest.php +++ /dev/null @@ -1,59 +0,0 @@ -assertDontSee('"application/ld+json"') - ->assertDontSee('"@type": "FAQPage"'); -}); - -it('can correctly render the JSON-LD Schema markup: FaqPageTest', function () { - config()->set('seo.title.suffix', ' | Laravel SEO'); - - $page = Page::create([]); - - $page::$overrides = [ - 'title' => 'Test FAQ', - 'enableTitleSuffix' => true, - 'url' => 'https://example.com/test/faq', - 'schema' => SchemaCollection::initialize()->addFaqPage(function (FaqPageSchema $faqPage): FaqPageSchema { - return $faqPage - ->addQuestion(name: 'Can this package add FaqPage to the schema?', acceptedAnswer: 'Yes!') - ->addQuestion(name: 'Does it support multiple questions?', acceptedAnswer: 'Of course.'); - }), - ]; - - get(route('seo.test-page', ['page' => $page])) - ->assertSee('"application/ld+json"', false) - ->assertSee( - '', - false - ); -}); From 7be3ff41859d5d0a3951fff2e1363ea4e2770fb7 Mon Sep 17 00:00:00 2001 From: Quentin Gabriele Date: Sat, 25 May 2024 22:02:20 +0200 Subject: [PATCH 04/17] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11a947c..aac9a02 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ class Homepage extends Controller ## Generating JSON-LD structured data This package can also **generate any structured data** for you (also called schema markup). -Structered data is a very vast subject so we highly recommend you to check the [Google documentation dedicated to it](https://developers.google.com/search/docs/appearance/structured-data/search-gallery). +Structured data is a very vast subject so we highly recommend you to check the [Google documentation dedicated to it](https://developers.google.com/search/docs/appearance/structured-data/search-gallery). ### Adding your first schema From 0b78334cc45a3bfc552e854d82f78085b6bdcc5a Mon Sep 17 00:00:00 2001 From: Quentin Gabriele Date: Sat, 25 May 2024 22:08:28 +0200 Subject: [PATCH 05/17] fix readme --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index aac9a02..3069262 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This package generates **valid and useful meta tags straight out-of-the-box**, w 2. Meta tags (author, description, image, robots, etc.) 3. OpenGraph Tags (Facebook, LinkedIn, etc.) 4. Twitter Tags -5. Structured data (Article, Breadcrumbs, FAQPage or any custom schema) +5. Structured data (Article, Breadcrumbs, FAQPage, or any custom schema) 6. Favicon 7. Robots tag 8. Alternates links tag @@ -100,7 +100,7 @@ return [ /** * Use this setting to specify whether you want self-referencing `` tags to - * be added to the head of every page. There has been some debate whether this a good practice, but experts + * be added to the head of every page. There has been some debate whether this is a good practice, but experts * from Google and Yoast say that this is the best strategy. * See https://yoast.com/rel-canonical/. */ @@ -128,7 +128,7 @@ return [ /** * Use this setting to specify the path to the favicon for your website. The url to it will be generated using the `secure_url()` function, - * so make sure to make the favicon accessibly from the `public` folder. + * so make sure to make the favicon accessible from the `public` folder. * * You can use the following filetypes: ico, png, gif, jpeg, svg. */ @@ -140,7 +140,7 @@ return [ * was given. This will be very useful on pages where you don't have an Eloquent model for, or where you * don't want to hardcode the title. * - * For example, if you have a with the url '/foo/about-me', we'll automatically set the title to 'About me' and append the site suffix. + * For example, if you have an url with the path '/foo/about-me', we'll automatically set the title to 'About me' and append the site suffix. */ 'infer_title_from_url' => true, @@ -277,12 +277,12 @@ You are allowed to only override the properties you want and omit the other prop 5. `url` (by default it will be `url()->current()`) 6. `enableTitleSuffix` (should be `true` or `false`, this allows you to set a suffix in the `config/seo.php` file, which will be appended to every title) 7. `site_name` -8. `published_time` (should be a `Carbon` instance with the published time. By default this will be the `created_at` property of your model) -9. `modified_time` (should be a `Carbon` instance with the published time. By default this will be the `updated_at` property of your model) +8. `published_time` (should be a `Carbon` instance with the published time. By default, this will be the `created_at` property of your model) +9. `modified_time` (should be a `Carbon` instance with the published time. By default, this will be the `updated_at` property of your model) 10. `section` (should be the name of the section of your content. It is used for OpenGraph article tags and it could be something like the category of the post) 11. `tags` (should be an array with tags. It is used for the OpenGraph article tags) 12. `schema` (this should be a SchemaCollection instance, where you can configure the JSON-LD structured data schema tags) -13. `locale` (this should be the locale of the page. By default this is derived from `app()->getLocale()` and it looks like `en` or `nl`.) +13. `locale` (this should be the locale of the page. By default, this is derived from `app()->getLocale()` and it looks like `en` or `nl`.) 14. `robots` (should be a string with the content value of the robots meta tag, like `nofollow,noindex`). You can also use the `$SEOData->markAsNoIndex()` to prevent a page from being indexed. 15. `alternates` (should be an array of `AlternateTag`). Will render `` tags. @@ -330,7 +330,7 @@ class Homepage extends Controller ## Generating JSON-LD structured data This package can also **generate any structured data** for you (also called schema markup). -Structured data is a very vast subject so we highly recommend you to check the [Google documentation dedicated to it](https://developers.google.com/search/docs/appearance/structured-data/search-gallery). +Structured data is a very vast subject, so we highly recommend you to check the [Google documentation dedicated to it](https://developers.google.com/search/docs/appearance/structured-data/search-gallery). ### Adding your first schema @@ -363,11 +363,11 @@ public function getDynamicSEOData(): SEOData > When adding a new schema, you can check the [documentation here](https://developers.google.com/search/docs/appearance/structured-data/faqpage) to know what keys to add. > [!TIP] -> After generating the structured data it is always a good idea to [test your website with Google's rich result validator](https://search.google.com/test/rich-results). +> After generating the structured data, it is always a good idea to [test your website with Google's rich result validator](https://search.google.com/test/rich-results). ### Pre-configured Schema: Article and BreadcrumbList -To help you getting started with structured data, we added 2 preconfigured schema: +To help you get started with structured data, we added 2 preconfigured schema: 1. `Article` 2. `BreadcrumbList` @@ -423,7 +423,7 @@ public function getDynamicSEOData(): SEOData You can also add `BreadcrumbList` schema markup by using the `->withBreadcrumbList()` function on the `SchemaCollection`. -By default the schema will only contain the current url from `$SEOData->url`. +By default, the schema will only contain the current url from `$SEOData->url`. ```php use RalphJSmit\Laravel\SEO\SchemaCollection; @@ -469,7 +469,7 @@ This code will generate `BreadcrumbList` JSON-LD structured data with the follow ## Advanced usage -Sometimes you may have advanced needs, that require you to apply your own logic to the `SEOData` class, just before it is used to generate the tags. +Sometimes you may have advanced needs that require you to apply your own logic to the `SEOData` class, just before it is used to generate the tags. To accomplish this, you can use the `SEODataTransformer()` function on the `SEOManager` facade to register one or multiple closures that will be able to modify the `SEOData` instance at the last moment: @@ -489,7 +489,7 @@ SEOManager::SEODataTransformer(function (SEOData $SEOData): SEOData { ### Modifying tags before they are rendered -You can also **register closures that can modify the final collection of generated tags**, right before they are rendered. This is useful if you want to add custom tags to the output, or if you want to modify the output of the tags. +You can also **register closures that can modify the final collection of generated tags**, right before they are rendered. This is useful if you want to add custom tags to the output or if you want to modify the output of the tags. ```php SEOManager::tagTransformer(function (TagCollection $tags): TagCollection { From 3b6c7f73de07c4eb4132b5b2aa081fa4eaa55773 Mon Sep 17 00:00:00 2001 From: Quentin Gabriele Date: Sat, 25 May 2024 22:09:15 +0200 Subject: [PATCH 06/17] remove useless code --- src/Schema/CustomSchema.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Schema/CustomSchema.php b/src/Schema/CustomSchema.php index 3eaebcd..0a62668 100644 --- a/src/Schema/CustomSchema.php +++ b/src/Schema/CustomSchema.php @@ -20,8 +20,6 @@ class CustomSchema extends Tag 'type' => 'application/ld+json', ]; - public null|string|HtmlString $inner; - function __construct(iterable|Arrayable $inner) { $this->inner = new HtmlString( From fd85f3fe5decd67d0e79d83476314307cf6915f5 Mon Sep 17 00:00:00 2001 From: Quentin Gabriele Date: Sat, 25 May 2024 22:23:46 +0200 Subject: [PATCH 07/17] install pint and format --- composer.json | 84 ++++++++++--------- src/Schema/CustomSchema.php | 5 +- src/SchemaCollection.php | 13 ++- src/Support/SchemaTagCollection.php | 3 +- src/Support/SitemapTag.php | 1 - src/Support/Tag.php | 2 +- src/Tags/DescriptionTag.php | 2 +- src/Tags/TitleTag.php | 2 +- tests/Feature/JSON-LD/ArticleTest.php | 2 +- .../Feature/JSON-LD/SchemaCollectionTest.php | 15 ++-- tests/Unit/Support/TagTest.php | 18 ++-- 11 files changed, 70 insertions(+), 77 deletions(-) diff --git a/composer.json b/composer.json index 4b81210..7cb26e0 100644 --- a/composer.json +++ b/composer.json @@ -1,70 +1,72 @@ { - "name" : "ralphjsmit/laravel-seo", - "description" : "A package to handle the SEO in any Laravel application, big or small.", - "keywords" : [ + "name": "ralphjsmit/laravel-seo", + "description": "A package to handle the SEO in any Laravel application, big or small.", + "keywords": [ "ralphjsmit", "laravel", "laravel-seo" ], - "homepage" : "https://github.com/ralphjsmit/laravel-seo", - "license" : "MIT", - "authors" : [ + "homepage": "https://github.com/ralphjsmit/laravel-seo", + "license": "MIT", + "authors": [ { - "name" : "Ralph J. Smit", - "email" : "rjs@ralphjsmit.com", - "role" : "Developer" + "name": "Ralph J. Smit", + "email": "rjs@ralphjsmit.com", + "role": "Developer" } ], - "require" : { + "require": { "php": "^8.0", "illuminate/contracts": "^9.0|^10.0|^11.0", "ralphjsmit/laravel-helpers": "^1.9", "spatie/laravel-package-tools": "^1.9.2" }, - "require-dev" : { - "nesbot/carbon" : "^2.66|^3.0", - "nunomaduro/collision" : "^5.10|^6.0|^7.0|^8.0", - "orchestra/testbench" : "^7.0|^8.0|^9.0", - "pestphp/pest" : "^1.21|^2.0", - "pestphp/pest-plugin-laravel" : "^1.1|^2.0", - "phpunit/phpunit" : "^9.5|^10.5", - "spatie/laravel-ray" : "^1.26", - "spatie/pest-plugin-test-time" : "^1.0|^2.0" + "require-dev": { + "laravel/pint": "^1.16", + "nesbot/carbon": "^2.66|^3.0", + "nunomaduro/collision": "^5.10|^6.0|^7.0|^8.0", + "orchestra/testbench": "^7.0|^8.0|^9.0", + "pestphp/pest": "^1.21|^2.0", + "pestphp/pest-plugin-laravel": "^1.1|^2.0", + "phpunit/phpunit": "^9.5|^10.5", + "spatie/laravel-ray": "^1.26", + "spatie/pest-plugin-test-time": "^1.0|^2.0" }, - "autoload" : { - "psr-4" : { - "RalphJSmit\\Laravel\\SEO\\" : "src", - "RalphJSmit\\Laravel\\SEO\\Database\\Factories\\" : "database/factories" + "autoload": { + "psr-4": { + "RalphJSmit\\Laravel\\SEO\\": "src", + "RalphJSmit\\Laravel\\SEO\\Database\\Factories\\": "database/factories" }, - "files" : [ + "files": [ "src/helpers.php" ] }, - "autoload-dev" : { - "psr-4" : { - "RalphJSmit\\Laravel\\SEO\\Tests\\" : "tests" + "autoload-dev": { + "psr-4": { + "RalphJSmit\\Laravel\\SEO\\Tests\\": "tests" } }, - "scripts" : { - "test" : "vendor/bin/pest", - "test-coverage" : "vendor/bin/pest --coverage" + "scripts": { + "test": "vendor/bin/pest", + "test-coverage": "vendor/bin/pest --coverage", + "format": "vendor/bin/pint" }, - "config" : { - "sort-packages" : true, - "allow-plugins" : { - "pestphp/pest-plugin" : true + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true } }, - "extra" : { - "laravel" : { - "providers" : [ + "extra": { + "laravel": { + "providers": [ "RalphJSmit\\Laravel\\SEO\\LaravelSEOServiceProvider" ], - "aliases" : { - "SEOManager" : "RalphJSmit\\Laravel\\SEO\\Facades\\SEOManager" + "aliases": { + "SEOManager": "RalphJSmit\\Laravel\\SEO\\Facades\\SEOManager" } } }, - "minimum-stability" : "dev", - "prefer-stable" : true + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/src/Schema/CustomSchema.php b/src/Schema/CustomSchema.php index 0a62668..af87577 100644 --- a/src/Schema/CustomSchema.php +++ b/src/Schema/CustomSchema.php @@ -2,12 +2,9 @@ namespace RalphJSmit\Laravel\SEO\Schema; -use Closure; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; use RalphJSmit\Helpers\Laravel\Pipe\Pipeable; -use RalphJSmit\Laravel\SEO\Support\SEOData; use RalphJSmit\Laravel\SEO\Support\Tag; class CustomSchema extends Tag @@ -20,7 +17,7 @@ class CustomSchema extends Tag 'type' => 'application/ld+json', ]; - function __construct(iterable|Arrayable $inner) + public function __construct(iterable | Arrayable $inner) { $this->inner = new HtmlString( collect($inner)->toJson() diff --git a/src/SchemaCollection.php b/src/SchemaCollection.php index 75384ca..ec48479 100644 --- a/src/SchemaCollection.php +++ b/src/SchemaCollection.php @@ -13,7 +13,7 @@ /** * @template TKey of array-key - * + * * @extends Collection */ class SchemaCollection extends Collection @@ -44,9 +44,9 @@ public function addBreadcrumbs(?Closure $builder = null): static } /** - * @param null|(Closure(SEOData $SEOData, Collection $article): Collection) $builder + * @param null|(Closure(SEOData $SEOData, Collection $article): Collection) $builder */ - function withArticle(null|array|Closure $builder = null): static + public function withArticle(null | array | Closure $builder = null): static { return $this->add(function (SEOData $SEOData) use ($builder) { $schema = collect([ @@ -75,9 +75,9 @@ function withArticle(null|array|Closure $builder = null): static } /** - * @param null|(Closure(SEOData $SEOData, Collection $breadcrumbList): Collection) $builder + * @param null|(Closure(SEOData $SEOData, Collection $breadcrumbList): Collection) $builder */ - function withBreadcrumbList(null|array|Closure $builder = null): static + public function withBreadcrumbList(null | array | Closure $builder = null): static { return $this->add(function (SEOData $SEOData) use ($builder) { $schema = collect([ @@ -89,7 +89,7 @@ function withBreadcrumbList(null|array|Closure $builder = null): static 'name' => $SEOData->title, 'item' => $SEOData->url, 'position' => 1, - ] + ], ]), ]); @@ -107,7 +107,6 @@ function withBreadcrumbList(null|array|Closure $builder = null): static ); } - return $schema->filter(); }); } diff --git a/src/Support/SchemaTagCollection.php b/src/Support/SchemaTagCollection.php index 3546868..1f90de8 100644 --- a/src/Support/SchemaTagCollection.php +++ b/src/Support/SchemaTagCollection.php @@ -2,7 +2,6 @@ namespace RalphJSmit\Laravel\SEO\Support; -use Closure; use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Collection; use RalphJSmit\Laravel\SEO\Schema\CustomSchema; @@ -15,7 +14,7 @@ public static function initialize(?SEOData $SEOData = null): ?static { $schemas = $SEOData?->schema; - if (!$schemas) { + if (! $schemas) { return null; } diff --git a/src/Support/SitemapTag.php b/src/Support/SitemapTag.php index e04fa6c..c55944a 100644 --- a/src/Support/SitemapTag.php +++ b/src/Support/SitemapTag.php @@ -10,7 +10,6 @@ class SitemapTag extends LinkTag 'title' => 'Sitemap', ]; - public function __construct( string $href ) { diff --git a/src/Support/Tag.php b/src/Support/Tag.php index ccd20a0..838648a 100644 --- a/src/Support/Tag.php +++ b/src/Support/Tag.php @@ -27,7 +27,7 @@ abstract class Tag implements Renderable /** * The content of the tag */ - public null|string|HtmlString $inner = null; + public null | string | HtmlString $inner = null; public array $attributesPipeline = []; diff --git a/src/Tags/DescriptionTag.php b/src/Tags/DescriptionTag.php index 6b1e5df..caed7f3 100644 --- a/src/Tags/DescriptionTag.php +++ b/src/Tags/DescriptionTag.php @@ -11,7 +11,7 @@ public static function initialize(?SEOData $SEOData): ?MetaTag { $description = $SEOData?->description; - if (!$description) { + if (! $description) { return null; } diff --git a/src/Tags/TitleTag.php b/src/Tags/TitleTag.php index 843617d..64793d5 100644 --- a/src/Tags/TitleTag.php +++ b/src/Tags/TitleTag.php @@ -19,7 +19,7 @@ public static function initialize(?SEOData $SEOData): ?Tag { $title = $SEOData?->title; - if (!$title) { + if (! $title) { return null; } diff --git a/tests/Feature/JSON-LD/ArticleTest.php b/tests/Feature/JSON-LD/ArticleTest.php index 276e873..53352d9 100644 --- a/tests/Feature/JSON-LD/ArticleTest.php +++ b/tests/Feature/JSON-LD/ArticleTest.php @@ -7,7 +7,7 @@ use function Pest\Laravel\get; beforeEach(function () { - if (!file_exists($dir = public_path('images'))) { + if (! file_exists($dir = public_path('images'))) { mkdir($dir, 0777, true); } diff --git a/tests/Feature/JSON-LD/SchemaCollectionTest.php b/tests/Feature/JSON-LD/SchemaCollectionTest.php index a51e476..98860a7 100644 --- a/tests/Feature/JSON-LD/SchemaCollectionTest.php +++ b/tests/Feature/JSON-LD/SchemaCollectionTest.php @@ -48,7 +48,7 @@ [ '@type' => 'Person', 'name' => $SEOData->author, - ] + ], ], ]), ]; @@ -70,7 +70,7 @@ [ '@type' => 'Person', 'name' => 'Ralph J. Smit', - ] + ], ], ]) . '', false @@ -97,8 +97,8 @@ [ '@type' => 'Person', 'name' => 'Second author', - ] - ] + ], + ], ]); }), ]; @@ -134,7 +134,6 @@ ); }); - it('can correctly render the JSON-LD Schema markup: BreadcrumbList', function () { config()->set('seo.title.suffix', ' | Laravel SEO'); @@ -165,7 +164,7 @@ '@type' => 'ListItem', 'name' => 'Subarticle', 'item' => 'https://example.com/test/article/2', - ] + ], ], ); @@ -241,7 +240,7 @@ [ '@type' => 'Person', 'name' => $SEOData->author, - ] + ], ], ]), ]; @@ -264,7 +263,7 @@ [ '@type' => 'Person', 'name' => 'Ralph J. Smit', - ] + ], ], ]) . '', false diff --git a/tests/Unit/Support/TagTest.php b/tests/Unit/Support/TagTest.php index 229d327..cde0dcc 100644 --- a/tests/Unit/Support/TagTest.php +++ b/tests/Unit/Support/TagTest.php @@ -1,26 +1,24 @@ attributes = [ - 'hreflang' => "hreflang", - 'description' => "description", + 'hreflang' => 'hreflang', + 'description' => 'description', 'title' => 'title', - 'content' => "content", + 'content' => 'content', 'name' => 'name', 'href' => 'href', - 'foo' => "foo", - 'property' => "property", - 'bar' => "bar", - 'rel' => "rel", + 'foo' => 'foo', + 'property' => 'property', + 'bar' => 'bar', + 'rel' => 'rel', ]; expect((string) $tag->render()) From d1e3d521689db385090ca97dd84f454637064c00 Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 14:18:36 +0200 Subject: [PATCH 08/17] WIP keep using fluent methods now based on CustomSchema class --- README.md | 137 +++++++++++++++----------- src/Schema/ArticleSchema.php | 21 ++-- src/Schema/BreadcrumbListSchema.php | 14 +-- src/Schema/CustomPreDefinedSchema.php | 36 +++++++ src/Schema/FaqPageSchema.php | 47 +++++++++ src/Schema/Schema.php | 48 --------- src/SchemaCollection.php | 79 ++------------- src/Support/Tag.php | 11 ++- 8 files changed, 189 insertions(+), 204 deletions(-) create mode 100644 src/Schema/CustomPreDefinedSchema.php create mode 100644 src/Schema/FaqPageSchema.php delete mode 100644 src/Schema/Schema.php diff --git a/README.md b/README.md index 3069262..de5a996 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,10 @@ class Homepage extends Controller This package can also **generate any structured data** for you (also called schema markup). Structured data is a very vast subject, so we highly recommend you to check the [Google documentation dedicated to it](https://developers.google.com/search/docs/appearance/structured-data/search-gallery). +Structured data can be added in two ways: +- Construct custom arrays of the structured data format, which is then rendered in JSON with the correct tags on the right place by the package. +- Use one of the 2 pre-defined templates (`Article` and `BreadcrumbList`). + ### Adding your first schema Let's add the FAQPage schema markup to our website as an example: @@ -343,18 +347,21 @@ public function getDynamicSEOData(): SEOData { return new SEOData( // ... - schema: SchemaCollection::make()->add(fn(SEOData $SEOData) => [ - '@context' => 'https://schema.org', - '@type' => 'FAQPage', - 'mainEntity' => [ - '@type' => 'Question', - 'name' => 'Your question goes here', - 'acceptedAnswer' => [ - '@type' => 'Answer', - 'text' => 'Your answer goes here', + schema: SchemaCollection::make() + ->add(fn (SEOData $SEOData) => [ + // You could use the `$SEOData` to dynamically + // use any data about the current page. + '@context' => 'https://schema.org', + '@type' => 'FAQPage', + 'mainEntity' => [ + '@type' => 'Question', + 'name' => 'Your question goes here', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Your answer goes here', + ], ], - ], - ]), + ]), ); } ``` @@ -362,22 +369,17 @@ public function getDynamicSEOData(): SEOData > [!TIP] > When adding a new schema, you can check the [documentation here](https://developers.google.com/search/docs/appearance/structured-data/faqpage) to know what keys to add. -> [!TIP] -> After generating the structured data, it is always a good idea to [test your website with Google's rich result validator](https://search.google.com/test/rich-results). - ### Pre-configured Schema: Article and BreadcrumbList -To help you get started with structured data, we added 2 preconfigured schema: +To help you get started with structured data, we added 3 preconfigured schema that you can modify using fluent methods. The following types are available: 1. `Article` 2. `BreadcrumbList` +3. `FAQPage` ### Article schema markup -You can add a pre-configured article with the `withArticle` method, this will generate a fully filled Article JSON schema using the values from your `SEOData` instance. - -> [!NOTE] -> Check the Google documentation about [Article](https://developers.google.com/search/docs/appearance/structured-data/article) +To enable structured data, you need to use the schema property of the SEOData class. To automatically generate `Article` schema markup, use the `->addArticle()` method: ```php @@ -387,14 +389,16 @@ public function getDynamicSEOData(): SEOData { return new SEOData( // ... - schema: SchemaCollection::make()->withArticle(), + schema: SchemaCollection::make() + ->addArticle(), ); } ``` -You can also pass a closure to the `->withArticle()` method to customize the individual schema markup. +You can pass a closure to `->addArticle()` method to customize the individual schema markup. This closure will receive an instance of ArticleSchema as its argument. You can an additional author by using the `->addAuthor()` method: ```php +use RalphJSmit\Laravel\SEO\Schema\ArticleSchema; use RalphJSmit\Laravel\SEO\SchemaCollection; use RalphJSmit\Laravel\SEO\Support\SEOData; use Illuminate\Support\Collection; @@ -404,21 +408,25 @@ public function getDynamicSEOData(): SEOData return new SEOData( // ... title: "A boring title" - schema: SchemaCollection::make()->withArticle(function(SEOData $SEOData, Collection $article){ - return $article->mergeRecursive([ - 'alternativeHeadline' => "Not {$SEOData->title}", // will be "Not A boring title" - 'author' => [ - [ - '@type' => 'Person', - 'name' => $this->moderator, - ] - ] - ]); - }), + schema: SchemaCollection::make() + ->addArticle(function (ArticleSchema $article, SEOData $SEOData): ArticleSchema { + return $article + ->addAuthor($this->moderator) + ->markup(function (Collection $markup) use ($SEOData) : Collection { + return $markup + ->put('alternativeHeadline', "Not {$SEOData->title}") // Set/overwrite alternative headline property to `Will be "Not A boring title"` :) + ->mergeRecursive([ + //... + ]); + }); + }), ); } ``` +> [!TIP] +> Check the Google documentation about [Article](https://developers.google.com/search/docs/appearance/structured-data/article) for more information. + ### BreadcrumbList schema markup You can also add `BreadcrumbList` schema markup by using the `->withBreadcrumbList()` function on the `SchemaCollection`. @@ -430,33 +438,19 @@ use RalphJSmit\Laravel\SEO\SchemaCollection; use RalphJSmit\Laravel\SEO\Support\SEOData; use Illuminate\Support\Collection; -SchemaCollection::make() - ->withBreadcrumbList(function (SEOData $SEOData, Collection $breadcrumbs) { - $items = $breadcrumb->get('itemListElement', []); - - $breadcrumb->put( - 'itemListElement', - [ - [ - '@type' => 'ListItem', - 'name' => 'Homepage', - 'item' => 'https://example.com', - ], - [ - '@type' => 'ListItem', - 'name' => 'Category', - 'item' => 'https://example.com/test', - ], - ...$items, - [ - '@type' => 'ListItem', - 'name' => 'Subarticle', - 'item' => 'https://example.com/test/article/2', - ] - ], - ); - - return $breadcrumb; +SchemaCollection::initialize() + ->addBreadcrumbs(function (BreadcrumbListSchema $breadcrumbs, SEOData $SEOData): BreadcrumbListSchema { + return $breadcrumbs + ->prependBreadcrumbs([ + 'Homepage' => 'https://example.com', + 'Category' => 'https://example.com/test', + ]) + ->appendBreadcrumbs([ + 'Subarticle' => 'https://example.com/test/article/2', + ]) + ->markup(function (Collection $markup): Collection { + // ... + }); }); ``` @@ -467,6 +461,31 @@ This code will generate `BreadcrumbList` JSON-LD structured data with the follow 3. [Current page] 4. Subarticle +> [!TIP] +> Check the Google documentation about [BreadcrumbList](https://developers.google.com/search/docs/appearance/structured-data/breadcrumb) for more information. + +### FAQPage schema markup + +You can also add FAQPage schema markup by using the ->addFaqPage() function on the SchemaCollection: + +```php +use RalphJSmit\Laravel\SEO\Schema\FaqPageSchema; +use RalphJSmit\Laravel\SEO\SchemaCollection;use RalphJSmit\Laravel\SEO\Support\SEOData; + +SchemaCollection::initialize() + ->addFaqPage(function (FaqPageSchema $faqPage, SEOData $SEOData): FaqPageSchema { + return $faqPage + ->addQuestion(name: "Can this package add FaqPage to the schema?", acceptedAnswer: "Yes!") + ->addQuestion(name: "Does it support multiple questions?", acceptedAnswer: "Of course."); + }); +``` + +> [!TIP] +> Check the Google documentation about [Faq Page](https://developers.google.com/search/docs/appearance/structured-data/faqpage) for more information. + +> [!TIP] +> After generating the structured data, it is always a good idea to [test your website with Google's rich result validator](https://search.google.com/test/rich-results). + ## Advanced usage Sometimes you may have advanced needs that require you to apply your own logic to the `SEOData` class, just before it is used to generate the tags. diff --git a/src/Schema/ArticleSchema.php b/src/Schema/ArticleSchema.php index 4bf0b05..b55ded5 100644 --- a/src/Schema/ArticleSchema.php +++ b/src/Schema/ArticleSchema.php @@ -3,14 +3,11 @@ namespace RalphJSmit\Laravel\SEO\Schema; use Carbon\CarbonInterface; +use Closure; use Illuminate\Support\Collection; -use Illuminate\Support\HtmlString; use RalphJSmit\Laravel\SEO\Support\SEOData; -/** - * @deprecated Use CustomSchema paradigm - */ -class ArticleSchema extends Schema +class ArticleSchema extends CustomPreDefinedSchema { public array $authors = []; @@ -32,7 +29,7 @@ class ArticleSchema extends Schema public function addAuthor(string $authorName): static { - if (empty($this->authors)) { + if ( ! $this->authors) { $this->authors = [ '@type' => 'Person', 'name' => $authorName, @@ -52,7 +49,7 @@ public function addAuthor(string $authorName): static return $this; } - public function initializeMarkup(SEOData $SEOData, array $markupBuilders): void + public function initializeMarkup(SEOData $SEOData): void { $this->url = $SEOData->url; @@ -66,12 +63,12 @@ public function initializeMarkup(SEOData $SEOData, array $markupBuilders): void ]; foreach ($properties as $markupProperty => $SEODataProperty) { - if ($SEOData->{$SEODataProperty}) { + if ( $SEOData->{$SEODataProperty} ) { $this->{$markupProperty} = $SEOData->{$SEODataProperty}; } } - if ($SEOData->author) { + if ( $SEOData->author ) { $this->authors = [ '@type' => 'Person', 'name' => $SEOData->author, @@ -79,9 +76,9 @@ public function initializeMarkup(SEOData $SEOData, array $markupBuilders): void } } - public function generateInner(): HtmlString + public function generateInner(): Collection { - $inner = collect([ + return collect([ '@context' => 'https://schema.org', '@type' => $this->type, 'mainEntityOfPage' => [ @@ -98,7 +95,5 @@ public function generateInner(): HtmlString ->when($this->articleBody, fn (Collection $collection): Collection => $collection->put('articleBody', $this->articleBody)) ->pipeThrough($this->markupTransformers) ->toJson(); - - return new HtmlString($inner); } } diff --git a/src/Schema/BreadcrumbListSchema.php b/src/Schema/BreadcrumbListSchema.php index 3251036..9d118a1 100644 --- a/src/Schema/BreadcrumbListSchema.php +++ b/src/Schema/BreadcrumbListSchema.php @@ -2,14 +2,12 @@ namespace RalphJSmit\Laravel\SEO\Schema; +use Closure; use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; use RalphJSmit\Laravel\SEO\Support\SEOData; -/** - * @deprecated Use CustomSchema paradigm - */ -class BreadcrumbListSchema extends Schema +class BreadcrumbListSchema extends CustomPreDefinedSchema { public Collection $breadcrumbs; @@ -24,16 +22,16 @@ public function appendBreadcrumbs(array $breadcrumbs): static return $this; } - public function initializeMarkup(SEOData $SEOData, array $markupBuilders): void + public function initializeMarkup(SEOData $SEOData): void { $this->breadcrumbs = collect([ $SEOData->title => $SEOData->url, ]); } - public function generateInner(): HtmlString + public function generateInner(): Collection { - $inner = collect([ + return collect([ '@context' => 'https://schema.org', '@type' => $this->type, 'itemListElement' => $this->breadcrumbs @@ -48,8 +46,6 @@ public function generateInner(): HtmlString ]) ->pipeThrough($this->markupTransformers) ->toJson(); - - return new HtmlString($inner); } public function prependBreadcrumbs(array $breadcrumbs): static diff --git a/src/Schema/CustomPreDefinedSchema.php b/src/Schema/CustomPreDefinedSchema.php new file mode 100644 index 0000000..4e898ca --- /dev/null +++ b/src/Schema/CustomPreDefinedSchema.php @@ -0,0 +1,36 @@ +initializeMarkup($SEOData); + + // `$markupBuilders` are closures that modify this schema + // tag object and can call methods on it to change items... + foreach ($markupBuilders as $markupBuilder) { + $markupBuilder($this, $SEOData); + } + + parent::__construct($this->generateInner()); + } + + abstract public function initializeMarkup(SEOData $SEOData): void; + + abstract public function generateInner(): Collection; + + public function markup(Closure $transformer): static + { + $this->markupTransformers[] = $transformer; + + return $this; + } +} diff --git a/src/Schema/FaqPageSchema.php b/src/Schema/FaqPageSchema.php new file mode 100644 index 0000000..df68f62 --- /dev/null +++ b/src/Schema/FaqPageSchema.php @@ -0,0 +1,47 @@ +questions[] = [ + '@type' => 'Question', + 'name' => $name, + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => $acceptedAnswer, + ], + ]; + + return $this; + } + + public function initializeMarkup(SEOData $SEOData): void + { + $this->questions = new Collection(); + } + + public function generateInner(): Collection + { + return collect([ + '@context' => 'https://schema.org', + '@type' => $this->type, + 'mainEntity' => $this->questions, + ]) + ->pipeThrough($this->markupTransformers) + ->toJson(); + } +} \ No newline at end of file diff --git a/src/Schema/Schema.php b/src/Schema/Schema.php deleted file mode 100644 index 1fd1bc1..0000000 --- a/src/Schema/Schema.php +++ /dev/null @@ -1,48 +0,0 @@ - 'application/ld+json', - ]; - - public Collection $markup; - - public array $markupTransformers = []; - - public function __construct(SEOData $SEOData, array $markupBuilders = []) - { - $this->initializeMarkup($SEOData, $markupBuilders); - - $this->pipeThrough($markupBuilders); - - $this->inner = $this->generateInner(); - } - - abstract public function generateInner(): HtmlString; - - abstract public function initializeMarkup(SEOData $SEOData, array $markupBuilders): void; - - public function markup(Closure $transformer): static - { - $this->markupTransformers[] = $transformer; - - return $this; - } -} diff --git a/src/SchemaCollection.php b/src/SchemaCollection.php index ec48479..e647a23 100644 --- a/src/SchemaCollection.php +++ b/src/SchemaCollection.php @@ -8,7 +8,6 @@ use RalphJSmit\Laravel\SEO\Schema\ArticleSchema; use RalphJSmit\Laravel\SEO\Schema\BreadcrumbListSchema; use RalphJSmit\Laravel\SEO\Schema\FaqPageSchema; -use RalphJSmit\Laravel\SEO\Schema\Schema; use RalphJSmit\Laravel\SEO\Support\SEOData; /** @@ -20,95 +19,31 @@ class SchemaCollection extends Collection { protected array $dictionary = [ 'article' => ArticleSchema::class, - 'breadcrumbs' => BreadcrumbListSchema::class, - 'faqPage' => FaqPageSchema::class, + 'breadcrumb_list' => BreadcrumbListSchema::class, + 'faq_page' => FaqPageSchema::class, ]; public array $markup = []; - /** - * @deprecated use withArticle instead - */ public function addArticle(?Closure $builder = null): static { - $this->markup[$this->dictionary['article']][] = $builder ?: fn (Schema $schema): Schema => $schema; + $this->markup[$this->dictionary['article']][] = $builder ?: fn (ArticleSchema $schema): ArticleSchema => $schema; return $this; } public function addBreadcrumbs(?Closure $builder = null): static { - $this->markup[$this->dictionary['breadcrumbs']][] = $builder ?: fn (Schema $schema): Schema => $schema; + $this->markup[$this->dictionary['breadcrumb_list']][] = $builder ?: fn (BreadcrumbListSchema $schema): BreadcrumbListSchema => $schema; return $this; } - /** - * @param null|(Closure(SEOData $SEOData, Collection $article): Collection) $builder - */ - public function withArticle(null | array | Closure $builder = null): static + public function addFaqPage(?Closure $builder = null): static { - return $this->add(function (SEOData $SEOData) use ($builder) { - $schema = collect([ - '@context' => 'https://schema.org', - '@type' => 'Article', - 'mainEntityOfPage' => [ - '@type' => 'WebPage', - '@id' => $SEOData->url, - ], - 'datePublished' => $SEOData->published_time->toIso8601String(), - 'dateModified' => $SEOData->modified_time->toIso8601String(), - 'headline' => $SEOData->title, - 'description' => $SEOData->description, - 'image' => $SEOData->image, - ])->when($SEOData->author, fn (Collection $schema) => $schema->put('author', [[ - '@type' => 'Person', - 'name' => $SEOData->author, - ]])); + $this->markup[$this->dictionary['faq_page']][] = $builder ?: fn (FaqPageSchema $schema): FaqPageSchema => $schema; - if ($builder) { - $schema = $builder($SEOData, $schema); - } - - return $schema->filter(); - }); - } - - /** - * @param null|(Closure(SEOData $SEOData, Collection $breadcrumbList): Collection) $builder - */ - public function withBreadcrumbList(null | array | Closure $builder = null): static - { - return $this->add(function (SEOData $SEOData) use ($builder) { - $schema = collect([ - '@context' => 'https://schema.org', - '@type' => 'BreadcrumbList', - 'itemListElement' => collect([ - [ - '@type' => 'ListItem', - 'name' => $SEOData->title, - 'item' => $SEOData->url, - 'position' => 1, - ], - ]), - ]); - - if ($builder) { - $schema = $builder($SEOData, $schema); - - /** - * Make sure position are in the right order after builder manipulation - */ - $schema->put( - 'itemListElement', - collect($schema->get('itemListElement', [])) - ->values() - ->map(fn (array $item, int $key) => [...$item, 'position' => $key + 1]) - ); - } - - return $schema->filter(); - }); + return $this; } public static function initialize(): static diff --git a/src/Support/Tag.php b/src/Support/Tag.php index 838648a..4ebafd1 100644 --- a/src/Support/Tag.php +++ b/src/Support/Tag.php @@ -27,7 +27,7 @@ abstract class Tag implements Renderable /** * The content of the tag */ - public null | string | HtmlString $inner = null; + public null|string|HtmlString $inner = null; public array $attributesPipeline = []; @@ -36,7 +36,7 @@ public function render(): View return view('seo::tags.tag', [ 'tag' => $this->tag, 'attributes' => $this->collectAttributes(), - 'inner' => $this->inner, + 'inner' => $this->getInner(), ]); } @@ -48,7 +48,7 @@ public function collectAttributes(): Collection $indexA = array_search($a, static::ATTRIBUTES_ORDER); $indexB = array_search($b, static::ATTRIBUTES_ORDER); - return match (true) { + return match ( true ) { $indexB === $indexA => 0, // keep the order defined in $attributes if neither $a or $b are in ATTRIBUTES_ORDER $indexA === false => 1, $indexB === false => -1, @@ -57,4 +57,9 @@ public function collectAttributes(): Collection }) ->pipeThrough($this->attributesPipeline); } + + public function getInner(): null|string|HtmlString + { + return $this->inner; + } } From 6ec18e96f185c423c69f6ecd2489b4209b6996eb Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 14:29:28 +0200 Subject: [PATCH 09/17] Fix tests --- src/Schema/ArticleSchema.php | 3 +- src/Schema/BreadcrumbListSchema.php | 3 +- src/Schema/FaqPageSchema.php | 3 +- tests/Feature/JSON-LD/FaqPageTest.php | 59 ++++++ .../Feature/JSON-LD/SchemaCollectionTest.php | 169 +++++++++++------- 5 files changed, 163 insertions(+), 74 deletions(-) create mode 100644 tests/Feature/JSON-LD/FaqPageTest.php diff --git a/src/Schema/ArticleSchema.php b/src/Schema/ArticleSchema.php index b55ded5..498b953 100644 --- a/src/Schema/ArticleSchema.php +++ b/src/Schema/ArticleSchema.php @@ -93,7 +93,6 @@ public function generateInner(): Collection ->when($this->description, fn (Collection $collection): Collection => $collection->put('description', $this->description)) ->when($this->image, fn (Collection $collection): Collection => $collection->put('image', $this->image)) ->when($this->articleBody, fn (Collection $collection): Collection => $collection->put('articleBody', $this->articleBody)) - ->pipeThrough($this->markupTransformers) - ->toJson(); + ->pipeThrough($this->markupTransformers); } } diff --git a/src/Schema/BreadcrumbListSchema.php b/src/Schema/BreadcrumbListSchema.php index 9d118a1..79e709f 100644 --- a/src/Schema/BreadcrumbListSchema.php +++ b/src/Schema/BreadcrumbListSchema.php @@ -44,8 +44,7 @@ public function generateInner(): Collection ]); }, new Collection()), ]) - ->pipeThrough($this->markupTransformers) - ->toJson(); + ->pipeThrough($this->markupTransformers); } public function prependBreadcrumbs(array $breadcrumbs): static diff --git a/src/Schema/FaqPageSchema.php b/src/Schema/FaqPageSchema.php index df68f62..4fd76b1 100644 --- a/src/Schema/FaqPageSchema.php +++ b/src/Schema/FaqPageSchema.php @@ -41,7 +41,6 @@ public function generateInner(): Collection '@type' => $this->type, 'mainEntity' => $this->questions, ]) - ->pipeThrough($this->markupTransformers) - ->toJson(); + ->pipeThrough($this->markupTransformers); } } \ No newline at end of file diff --git a/tests/Feature/JSON-LD/FaqPageTest.php b/tests/Feature/JSON-LD/FaqPageTest.php new file mode 100644 index 0000000..856b59e --- /dev/null +++ b/tests/Feature/JSON-LD/FaqPageTest.php @@ -0,0 +1,59 @@ +assertDontSee('"application/ld+json"') + ->assertDontSee('"@type": "FAQPage"'); +}); + +it('can correctly render the JSON-LD Schema markup: FaqPageTest', function () { + config()->set('seo.title.suffix', ' | Laravel SEO'); + + $page = Page::create([]); + + $page::$overrides = [ + 'title' => 'Test FAQ', + 'enableTitleSuffix' => true, + 'url' => 'https://example.com/test/faq', + 'schema' => SchemaCollection::initialize()->addFaqPage(function (FaqPageSchema $faqPage): FaqPageSchema { + return $faqPage + ->addQuestion(name: 'Can this package add FaqPage to the schema?', acceptedAnswer: 'Yes!') + ->addQuestion(name: 'Does it support multiple questions?', acceptedAnswer: 'Of course.'); + }), + ]; + + get(route('seo.test-page', ['page' => $page])) + ->assertSee('"application/ld+json"', false) + ->assertSee( + '', + false + ); +}); \ No newline at end of file diff --git a/tests/Feature/JSON-LD/SchemaCollectionTest.php b/tests/Feature/JSON-LD/SchemaCollectionTest.php index 98860a7..1e7a773 100644 --- a/tests/Feature/JSON-LD/SchemaCollectionTest.php +++ b/tests/Feature/JSON-LD/SchemaCollectionTest.php @@ -1,6 +1,9 @@ 'Test title', 'image' => 'images/twitter-1743x1743.jpg', 'author' => 'Ralph J. Smit', - 'schema' => SchemaCollection::make()->withArticle(function (SEOData $SEOData, Collection $article) { - return $article->mergeRecursive([ - 'alternativeHeadline' => 'My alternative headline', - 'author' => [ - [ - '@type' => 'Person', - 'name' => 'Second author', - ], - ], - ]); - }), + 'schema' => SchemaCollection::make() + ->addArticle(function (ArticleSchema $article, SEOData $SEOData) { + return $article + ->addAuthor('Second author') + ->markup(function (Collection $markup) { + return $markup->mergeRecursive([ + 'alternativeHeadline' => 'My alternative headline', + ]); + }); + }), ]; get(route('seo.test-page', ['page' => $page])) ->assertSee('"application/ld+json"', false) ->assertSee( '', + ], + 'image' => secure_url('images/twitter-1743x1743.jpg'), + 'alternativeHeadline' => 'My alternative headline', + ]) . '', false ); }); @@ -143,69 +145,100 @@ 'title' => 'Test article', 'enableTitleSuffix' => true, 'url' => 'https://example.com/test/article', - 'schema' => SchemaCollection::make()->withBreadcrumbList(function (SEOData $SEOData, Collection $breadcrumb) { - $items = $breadcrumb->get('itemListElement', []); + 'schema' => SchemaCollection::make() + ->addBreadcrumbs(function (BreadcrumbListSchema $breadcrumbList, SEOData $SEOData) { + return $breadcrumbList + ->prependBreadcrumbs([ + 'Homepage' => 'https://example.com', + 'Category' => 'https://example.com/test', + ]) + ->appendBreadcrumbs([ + 'Subarticle' => 'https://example.com/test/article/2', + ]); + }), + ]; - $breadcrumb->put( - 'itemListElement', - [ + get(route('seo.test-page', ['page' => $page])) + ->assertSee('"application/ld+json"', false) + ->assertSee( + '', + false + ); +}); + +it('can correctly render the JSON-LD Schema markup: FaqPage', function () { + config()->set('seo.title.suffix', ' | Laravel SEO'); + + $page = Page::create([]); - return $breadcrumb; - }), + $page::$overrides = [ + 'title' => 'Test article', + 'enableTitleSuffix' => true, + 'url' => 'https://example.com/test/article', + 'schema' => SchemaCollection::make() + ->addFaqPage(function (FaqPageSchema $faqPage, SEOData $SEOData) { + return $faqPage + ->addQuestion(name: 'Can this package add FaqPage to the schema?', acceptedAnswer: 'Yes!') + ->addQuestion(name: 'Does it support multiple questions?', acceptedAnswer: 'Of course.'); + }), ]; get(route('seo.test-page', ['page' => $page])) ->assertSee('"application/ld+json"', false) ->assertSee( '', + ], + ]) . '', false ); }); From 71c93c89205e813661a021914dc99cab43627c60 Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 14:29:37 +0200 Subject: [PATCH 10/17] Style --- src/Schema/ArticleSchema.php | 7 +++---- src/Schema/BreadcrumbListSchema.php | 2 -- src/Schema/FaqPageSchema.php | 3 +-- src/Support/Tag.php | 6 +++--- tests/Feature/JSON-LD/FaqPageTest.php | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Schema/ArticleSchema.php b/src/Schema/ArticleSchema.php index 498b953..cc4a826 100644 --- a/src/Schema/ArticleSchema.php +++ b/src/Schema/ArticleSchema.php @@ -3,7 +3,6 @@ namespace RalphJSmit\Laravel\SEO\Schema; use Carbon\CarbonInterface; -use Closure; use Illuminate\Support\Collection; use RalphJSmit\Laravel\SEO\Support\SEOData; @@ -29,7 +28,7 @@ class ArticleSchema extends CustomPreDefinedSchema public function addAuthor(string $authorName): static { - if ( ! $this->authors) { + if (! $this->authors) { $this->authors = [ '@type' => 'Person', 'name' => $authorName, @@ -63,12 +62,12 @@ public function initializeMarkup(SEOData $SEOData): void ]; foreach ($properties as $markupProperty => $SEODataProperty) { - if ( $SEOData->{$SEODataProperty} ) { + if ($SEOData->{$SEODataProperty}) { $this->{$markupProperty} = $SEOData->{$SEODataProperty}; } } - if ( $SEOData->author ) { + if ($SEOData->author) { $this->authors = [ '@type' => 'Person', 'name' => $SEOData->author, diff --git a/src/Schema/BreadcrumbListSchema.php b/src/Schema/BreadcrumbListSchema.php index 79e709f..cec03b3 100644 --- a/src/Schema/BreadcrumbListSchema.php +++ b/src/Schema/BreadcrumbListSchema.php @@ -2,9 +2,7 @@ namespace RalphJSmit\Laravel\SEO\Schema; -use Closure; use Illuminate\Support\Collection; -use Illuminate\Support\HtmlString; use RalphJSmit\Laravel\SEO\Support\SEOData; class BreadcrumbListSchema extends CustomPreDefinedSchema diff --git a/src/Schema/FaqPageSchema.php b/src/Schema/FaqPageSchema.php index 4fd76b1..8cd2a06 100644 --- a/src/Schema/FaqPageSchema.php +++ b/src/Schema/FaqPageSchema.php @@ -3,7 +3,6 @@ namespace RalphJSmit\Laravel\SEO\Schema; use Illuminate\Support\Collection; -use Illuminate\Support\HtmlString; use RalphJSmit\Laravel\SEO\Support\SEOData; /** @@ -43,4 +42,4 @@ public function generateInner(): Collection ]) ->pipeThrough($this->markupTransformers); } -} \ No newline at end of file +} diff --git a/src/Support/Tag.php b/src/Support/Tag.php index 4ebafd1..25a6191 100644 --- a/src/Support/Tag.php +++ b/src/Support/Tag.php @@ -27,7 +27,7 @@ abstract class Tag implements Renderable /** * The content of the tag */ - public null|string|HtmlString $inner = null; + public null | string | HtmlString $inner = null; public array $attributesPipeline = []; @@ -48,7 +48,7 @@ public function collectAttributes(): Collection $indexA = array_search($a, static::ATTRIBUTES_ORDER); $indexB = array_search($b, static::ATTRIBUTES_ORDER); - return match ( true ) { + return match (true) { $indexB === $indexA => 0, // keep the order defined in $attributes if neither $a or $b are in ATTRIBUTES_ORDER $indexA === false => 1, $indexB === false => -1, @@ -58,7 +58,7 @@ public function collectAttributes(): Collection ->pipeThrough($this->attributesPipeline); } - public function getInner(): null|string|HtmlString + public function getInner(): null | string | HtmlString { return $this->inner; } diff --git a/tests/Feature/JSON-LD/FaqPageTest.php b/tests/Feature/JSON-LD/FaqPageTest.php index 856b59e..8798d3b 100644 --- a/tests/Feature/JSON-LD/FaqPageTest.php +++ b/tests/Feature/JSON-LD/FaqPageTest.php @@ -56,4 +56,4 @@ ]) . '', false ); -}); \ No newline at end of file +}); From a199718bab199413eeec39fe717e2c6bb8555a1e Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 14:31:11 +0200 Subject: [PATCH 11/17] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index de5a996..1ffa4da 100644 --- a/README.md +++ b/README.md @@ -333,8 +333,8 @@ This package can also **generate any structured data** for you (also called sche Structured data is a very vast subject, so we highly recommend you to check the [Google documentation dedicated to it](https://developers.google.com/search/docs/appearance/structured-data/search-gallery). Structured data can be added in two ways: -- Construct custom arrays of the structured data format, which is then rendered in JSON with the correct tags on the right place by the package. -- Use one of the 2 pre-defined templates (`Article` and `BreadcrumbList`). +- Construct custom arrays of the structured data format, which is then rendered by the package in JSON on the correct place. +- Use one of the 3 pre-defined templates to fluently build your structured data (`Article`, `BreadcrumbList`, `FaqPage`). ### Adding your first schema From d1a4bd7cf60a9665eccaeb0c8a1e3e172517886e Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 14:35:25 +0200 Subject: [PATCH 12/17] Update README.md --- README.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1ffa4da..766f7c9 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,7 @@ public function getDynamicSEOData(): SEOData schema: SchemaCollection::make() ->add(fn (SEOData $SEOData) => [ // You could use the `$SEOData` to dynamically - // use any data about the current page. + // fetch any data about the current page. '@context' => 'https://schema.org', '@type' => 'FAQPage', 'mainEntity' => [ @@ -371,7 +371,7 @@ public function getDynamicSEOData(): SEOData ### Pre-configured Schema: Article and BreadcrumbList -To help you get started with structured data, we added 3 preconfigured schema that you can modify using fluent methods. The following types are available: +To help you get started with structured data, we added 3 preconfigured schema that you can construct using fluent methods. The following types are available: 1. `Article` 2. `BreadcrumbList` @@ -379,7 +379,7 @@ To help you get started with structured data, we added 3 preconfigured schema th ### Article schema markup -To enable structured data, you need to use the schema property of the SEOData class. To automatically generate `Article` schema markup, use the `->addArticle()` method: +In order to automatically and fluently generate `Article` schema markup, use the `->addArticle()` method: ```php @@ -389,13 +389,12 @@ public function getDynamicSEOData(): SEOData { return new SEOData( // ... - schema: SchemaCollection::make() - ->addArticle(), + schema: SchemaCollection::make()->addArticle(), ); } ``` - -You can pass a closure to `->addArticle()` method to customize the individual schema markup. This closure will receive an instance of ArticleSchema as its argument. You can an additional author by using the `->addAuthor()` method: + +This will construct an article schema using all data provided by the `SEOData` object. You can pass a closure to `->addArticle()` method to customize the individual schema markup. This closure will receive an instance of ArticleSchema as its argument. You can an additional author by using the `->addAuthor()` method. ```php use RalphJSmit\Laravel\SEO\Schema\ArticleSchema; @@ -410,26 +409,28 @@ public function getDynamicSEOData(): SEOData title: "A boring title" schema: SchemaCollection::make() ->addArticle(function (ArticleSchema $article, SEOData $SEOData): ArticleSchema { - return $article - ->addAuthor($this->moderator) - ->markup(function (Collection $markup) use ($SEOData) : Collection { - return $markup - ->put('alternativeHeadline', "Not {$SEOData->title}") // Set/overwrite alternative headline property to `Will be "Not A boring title"` :) - ->mergeRecursive([ - //... - ]); - }); + return $article->addAuthor($this->moderator); }), ); } ``` +You can completely customize the schema markup by using the `->markup()` method on the `ArticleSchema` instance: + +```php +SchemaCollection::initialize()->addArticle(function (ArticleSchema $article, SEOData $SEOData): ArticleSchema { + return $article->markup(function (Collection $markup) use ($SEOData): Collection { + return $markup->put('alternativeHeadline', "Not {$SEOData->title}"); // Set/overwrite alternative headline property to `Will be "Not A boring title"` :) + }); +}); +``` + > [!TIP] > Check the Google documentation about [Article](https://developers.google.com/search/docs/appearance/structured-data/article) for more information. ### BreadcrumbList schema markup -You can also add `BreadcrumbList` schema markup by using the `->withBreadcrumbList()` function on the `SchemaCollection`. +You can also add `BreadcrumbList` schema markup by using the `->addBreadcrumbList()` function on the `SchemaCollection`. By default, the schema will only contain the current url from `$SEOData->url`. From 4639fc3078c97756d5de4d2555afc9e0736de849 Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 14:36:10 +0200 Subject: [PATCH 13/17] Update README.md --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 766f7c9..fb64e48 100644 --- a/README.md +++ b/README.md @@ -435,10 +435,6 @@ You can also add `BreadcrumbList` schema markup by using the `->addBreadcrumbLis By default, the schema will only contain the current url from `$SEOData->url`. ```php -use RalphJSmit\Laravel\SEO\SchemaCollection; -use RalphJSmit\Laravel\SEO\Support\SEOData; -use Illuminate\Support\Collection; - SchemaCollection::initialize() ->addBreadcrumbs(function (BreadcrumbListSchema $breadcrumbs, SEOData $SEOData): BreadcrumbListSchema { return $breadcrumbs @@ -470,9 +466,6 @@ This code will generate `BreadcrumbList` JSON-LD structured data with the follow You can also add FAQPage schema markup by using the ->addFaqPage() function on the SchemaCollection: ```php -use RalphJSmit\Laravel\SEO\Schema\FaqPageSchema; -use RalphJSmit\Laravel\SEO\SchemaCollection;use RalphJSmit\Laravel\SEO\Support\SEOData; - SchemaCollection::initialize() ->addFaqPage(function (FaqPageSchema $faqPage, SEOData $SEOData): FaqPageSchema { return $faqPage From 191061a16125f95aa6e167ad911571cff03b2c43 Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 14:38:27 +0200 Subject: [PATCH 14/17] Rename --- src/Schema/ArticleSchema.php | 2 +- src/Schema/BreadcrumbListSchema.php | 2 +- .../{CustomPreDefinedSchema.php => CustomSchemaFluent.php} | 2 +- src/Schema/FaqPageSchema.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/Schema/{CustomPreDefinedSchema.php => CustomSchemaFluent.php} (93%) diff --git a/src/Schema/ArticleSchema.php b/src/Schema/ArticleSchema.php index cc4a826..b7cea04 100644 --- a/src/Schema/ArticleSchema.php +++ b/src/Schema/ArticleSchema.php @@ -6,7 +6,7 @@ use Illuminate\Support\Collection; use RalphJSmit\Laravel\SEO\Support\SEOData; -class ArticleSchema extends CustomPreDefinedSchema +class ArticleSchema extends CustomSchemaFluent { public array $authors = []; diff --git a/src/Schema/BreadcrumbListSchema.php b/src/Schema/BreadcrumbListSchema.php index cec03b3..1bc522e 100644 --- a/src/Schema/BreadcrumbListSchema.php +++ b/src/Schema/BreadcrumbListSchema.php @@ -5,7 +5,7 @@ use Illuminate\Support\Collection; use RalphJSmit\Laravel\SEO\Support\SEOData; -class BreadcrumbListSchema extends CustomPreDefinedSchema +class BreadcrumbListSchema extends CustomSchemaFluent { public Collection $breadcrumbs; diff --git a/src/Schema/CustomPreDefinedSchema.php b/src/Schema/CustomSchemaFluent.php similarity index 93% rename from src/Schema/CustomPreDefinedSchema.php rename to src/Schema/CustomSchemaFluent.php index 4e898ca..86b8655 100644 --- a/src/Schema/CustomPreDefinedSchema.php +++ b/src/Schema/CustomSchemaFluent.php @@ -6,7 +6,7 @@ use Illuminate\Support\Collection; use RalphJSmit\Laravel\SEO\Support\SEOData; -abstract class CustomPreDefinedSchema extends CustomSchema +abstract class CustomSchemaFluent extends CustomSchema { public array $markupTransformers = []; diff --git a/src/Schema/FaqPageSchema.php b/src/Schema/FaqPageSchema.php index 8cd2a06..ad93c3c 100644 --- a/src/Schema/FaqPageSchema.php +++ b/src/Schema/FaqPageSchema.php @@ -8,7 +8,7 @@ /** * @see https://developers.google.com/search/docs/appearance/structured-data/faqpage */ -class FaqPageSchema extends CustomPreDefinedSchema +class FaqPageSchema extends CustomSchemaFluent { public Collection $questions; From 389e9ed90dcaa4b4d0be333214d1ae38581d4548 Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 14:40:01 +0200 Subject: [PATCH 15/17] Update CustomSchemaFluent.php --- src/Schema/CustomSchemaFluent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema/CustomSchemaFluent.php b/src/Schema/CustomSchemaFluent.php index 86b8655..a9a7d71 100644 --- a/src/Schema/CustomSchemaFluent.php +++ b/src/Schema/CustomSchemaFluent.php @@ -14,7 +14,7 @@ public function __construct(SEOData $SEOData, array $markupBuilders = []) { $this->initializeMarkup($SEOData); - // `$markupBuilders` are closures that modify this schema + // `$markupBuilders` are closures that modify this fluent schema // tag object and can call methods on it to change items... foreach ($markupBuilders as $markupBuilder) { $markupBuilder($this, $SEOData); From e1ace45b4575a622d6b9dc0edbd880aebeaa067c Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:07:33 +0200 Subject: [PATCH 16/17] WIP --- .phpunit.cache/test-results | 2 +- src/Support/AlternateTag.php | 1 + src/Support/SchemaTagCollection.php | 4 +- src/Support/Tag.php | 3 -- .../Feature/JSON-LD/SchemaCollectionTest.php | 42 ++++++++++++++++++- tests/TestCase.php | 21 ---------- tests/Unit/Schema/CustomSchemaTest.php | 42 ++++++++++++++++++- 7 files changed, 83 insertions(+), 32 deletions(-) diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results index a4fbe1c..bd7d8f3 100644 --- a/.phpunit.cache/test-results +++ b/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":"pest_2.34.2","defects":[],"times":{"P\\Tests\\Unit\\Support\\HasSEOTest::__pest_evaluable_it_automatically_associates_a_SEO_model_on_creation":0.017,"P\\Tests\\Feature\\JSONLD\\ArticleTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__Article":0.033,"P\\Tests\\Feature\\JSONLD\\ArticleTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__Article":0.002,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_canonical_URL_if_allowed":0.001,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_model_level_canonical_url_if_set_on_override":0.002,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_cannot_display_the_canonical_url_if_not_allowed":0.001,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_will_not_break_if_no_canonical__url_column_exists_in_seo_table":0.003,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_model_level_canonical_url_if_set_in_database":0.002,"P\\Tests\\Unit\\Facades\\SEOManagerTest::__pest_evaluable_the_SEOManager_facade_works_as_expected":0,"P\\Tests\\Unit\\Schema\\ArticleSchemaTest::__pest_evaluable_it_can_construct_Schema_Markup__Article":0,"P\\Tests\\Unit\\Schema\\ArticleSchemaTest::__pest_evaluable_it_can_add_multiple_authors_to_Schema_Markup__Article":0,"P\\Tests\\Unit\\Support\\SEODataTest::__pest_evaluable_it_can_determine_the_size_of_an_image":0.001,"P\\Tests\\Unit\\Support\\SEODataTest::__pest_evaluable_it_can_allow_to_prepareForUsage_without_a_model_in_the_database":0.001,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_display_the_image_url_if_it_came_from_a_model":0.002,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_not_render_the_default_image_if_that_was_disabled":0.001,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_display_the_image_url_from_a_model":0.002,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_render_the_default_image#('\/public\/test\/image.jpg')":0.001,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_render_the_default_image#('public\/test\/image.jpg')":0.001,"P\\Tests\\Feature\\Tags\\SitemapTagTest::__pest_evaluable_it_can_display_the_sitemap_if_path_is_set":0.002,"P\\Tests\\Unit\\Schema\\BreadcrumbListSchemaTest::__pest_evaluable_it_can_construct_Schema_Markup__BreadcrumbList":0,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_can_override_the_author":0.002,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_can_display_the_fallback_description_tag":0.001,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_will_not_display_the_author_tag_if_there_isn_t_a_author":0.001,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_will_display_the_author_if_the_associated_SEO_model_has_a_author":0.002,"P\\Tests\\Feature\\JSONLD\\BreadcrumbListTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__BreadcrumbList":0.001,"P\\Tests\\Feature\\JSONLD\\BreadcrumbListTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__BreadcrumbList":0.002,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_will_display_the_description_if_the_associated_SEO_model_has_a_description":0.002,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_can_display_the_fallback_description_tag":0.002,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_will_not_display_the_description_tag_if_there_isn_t_a_description":0.001,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_include_the_widths_and_heights_of_Twitter_images_if_the_image_was_overridden_using_a_URL#('summary', 'images\/twitter-1743x1743.jpg', '1743', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_include_the_widths_and_heights_of_Twitter_images_if_the_image_was_overridden_using_a_URL#('summary_large_image', 'images\/twitter-3597x1799.jpg', '3597', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_with_the_image_on_a_Page#('summary_large_image', 'images\/twitter-3597x1799.jpg', '3597', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_with_the_image_on_a_Page#('summary', 'images\/twitter-1743x1743.jpg', '1743', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_uses_openGraphTitle_over_title":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_render_the_Twitter_Card_summary__large__image_for_too_large_or_small_images#('images\/twitter-72x72.jpg')":0.002,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_render_the_Twitter_Card_summary__large__image_for_too_large_or_small_images#('images\/twitter-4721x4721.jpg')":0.002,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_escape_the_title":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_without_the_image":0.002,"P\\Tests\\Feature\\Tags\\FaviconTagTest::__pest_evaluable_it_will_render_the_favicon_if_the_favicon_is_set":0.001,"P\\Tests\\Feature\\Tags\\FaviconTagTest::__pest_evaluable_it_will_not_render_the_favicon_if_the_favicon_is_set_to_null":0.001,"P\\Tests\\Unit\\SEOManagerTest::__pest_evaluable_the_SEOManager_singleton_works_as_expected":0,"P\\Tests\\Feature\\Tags\\AlternateTagTest::__pest_evaluable_it_will_not_display_the_aternates_tags_if_there_isn_t_any_alternate":0.001,"P\\Tests\\Feature\\Tags\\AlternateTagTest::__pest_evaluable_it_will_display_the_alternates_links_if_the_associated_SEO_model_has_alternates_links":0.002,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager_with_a_SEOData_data_object":0.001,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager":0,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager_with_and_without_a_model":0.001,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_infer_the_title_from_the_url_if_that_is_allowed":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_escape_the_title":0.001,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_display_the_title_if_the_associated_SEO_model_has_a_title":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_infer_the_title_from_the_url_if_that_is_allowed_and_the_model_doesn_t_return_a_title":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_not_infer_the_title_from_the_url_if_that_isn_t_allowed":0.003,"P\\Tests\\Feature\\JSONLD\\FaqPageTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__FaqPageTest":0.002,"P\\Tests\\Feature\\JSONLD\\FaqPageTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__FaqPageTest":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_add_properties_to_a_SEO_model#('description', 'This is a description')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_add_properties_to_a_SEO_model#('title', 'My Cool Page Title')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_have_immutable_timestamps":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_override_certain_SEO_Data#('title', 'My Cool Page Title')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_override_certain_SEO_Data#('description', 'This is a description')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_morph_a_model_to_the_SEO_model":0,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_disable_the_suffix_in_the_page_model":0.002,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_give_the_title_of_a_page_a_suffix_it_was_specified":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_disable_the_suffix_in_the_page_model_dynamically_via_a_function":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_prepare_the_SEO_for_use_on_a_page":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_cannot_overwrite_the_robots_tag__default__value_with_the_robots_attribute_if__force__default__is_set__DB_Model_":0.007,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_overwrite_the_robots_tag__default__value_with_the_robots_attribute__DB_Model_":0.002,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_cannot_overwrite_the_robots_tag__default__value_with_the_robots_attribute_if__force__default__is_set__SEOData_":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_overwrite_the_robots_tag__default__value_with_the_robots_attribute__SEOData_":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_output_the_robots_tag__default__value":0.001,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_replace_the_title_if_we_re_on_the_homepage#('Custom homepage title', 'Custom homepage title')":0.001,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_replace_the_title_if_we_re_on_the_homepage#(null, '| My Website suffix')":0.001,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_pipe_the_generated_tags_through_the_transformers_just_before_render":0.001,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_pipe_the_SEOData_through_the_transformer_before_putting_it_into_the_collection":0.002,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_can_render_the_SEOData_from_an_object_that_s_directly_passed_in":0.001,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_uses_openGraphTitle_over_title":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags_for_a_post_or_page_with_a_few_additional_overrides":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_will_escape_the_title":0.002,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags":0.002,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags_for_a_post_or_page":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_locale_tags":0.001}} \ No newline at end of file +{"version":"pest_2.34.2","defects":[],"times":{"P\\Tests\\Unit\\Support\\HasSEOTest::__pest_evaluable_it_automatically_associates_a_SEO_model_on_creation":0.002,"P\\Tests\\Feature\\JSONLD\\ArticleTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__Article":0.005,"P\\Tests\\Feature\\JSONLD\\ArticleTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__Article":0.032,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_canonical_URL_if_allowed":0.001,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_model_level_canonical_url_if_set_on_override":0.002,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_cannot_display_the_canonical_url_if_not_allowed":0.001,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_will_not_break_if_no_canonical__url_column_exists_in_seo_table":0.004,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_model_level_canonical_url_if_set_in_database":0.003,"P\\Tests\\Unit\\Facades\\SEOManagerTest::__pest_evaluable_the_SEOManager_facade_works_as_expected":0,"P\\Tests\\Unit\\Schema\\ArticleSchemaTest::__pest_evaluable_it_can_construct_Schema_Markup__Article":0,"P\\Tests\\Unit\\Schema\\ArticleSchemaTest::__pest_evaluable_it_can_add_multiple_authors_to_Schema_Markup__Article":0,"P\\Tests\\Unit\\Support\\SEODataTest::__pest_evaluable_it_can_determine_the_size_of_an_image":0.002,"P\\Tests\\Unit\\Support\\SEODataTest::__pest_evaluable_it_can_allow_to_prepareForUsage_without_a_model_in_the_database":0.002,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_display_the_image_url_if_it_came_from_a_model":0.003,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_not_render_the_default_image_if_that_was_disabled":0.001,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_display_the_image_url_from_a_model":0.003,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_render_the_default_image#('\/public\/test\/image.jpg')":0.003,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_render_the_default_image#('public\/test\/image.jpg')":0.002,"P\\Tests\\Feature\\Tags\\SitemapTagTest::__pest_evaluable_it_can_display_the_sitemap_if_path_is_set":0.002,"P\\Tests\\Unit\\Schema\\BreadcrumbListSchemaTest::__pest_evaluable_it_can_construct_Schema_Markup__BreadcrumbList":0.001,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_can_override_the_author":0.004,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_can_display_the_fallback_description_tag":0.002,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_will_not_display_the_author_tag_if_there_isn_t_a_author":0.002,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_will_display_the_author_if_the_associated_SEO_model_has_a_author":0.003,"P\\Tests\\Feature\\JSONLD\\BreadcrumbListTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__BreadcrumbList":0.002,"P\\Tests\\Feature\\JSONLD\\BreadcrumbListTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__BreadcrumbList":0.002,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_will_display_the_description_if_the_associated_SEO_model_has_a_description":0.003,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_can_display_the_fallback_description_tag":0.001,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_will_not_display_the_description_tag_if_there_isn_t_a_description":0.002,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_include_the_widths_and_heights_of_Twitter_images_if_the_image_was_overridden_using_a_URL#('summary', 'images\/twitter-1743x1743.jpg', '1743', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_include_the_widths_and_heights_of_Twitter_images_if_the_image_was_overridden_using_a_URL#('summary_large_image', 'images\/twitter-3597x1799.jpg', '3597', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_with_the_image_on_a_Page#('summary_large_image', 'images\/twitter-3597x1799.jpg', '3597', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_with_the_image_on_a_Page#('summary', 'images\/twitter-1743x1743.jpg', '1743', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_uses_openGraphTitle_over_title":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_render_the_Twitter_Card_summary__large__image_for_too_large_or_small_images#('images\/twitter-72x72.jpg')":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_render_the_Twitter_Card_summary__large__image_for_too_large_or_small_images#('images\/twitter-4721x4721.jpg')":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_escape_the_title":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_without_the_image":0.002,"P\\Tests\\Feature\\Tags\\FaviconTagTest::__pest_evaluable_it_will_render_the_favicon_if_the_favicon_is_set":0.002,"P\\Tests\\Feature\\Tags\\FaviconTagTest::__pest_evaluable_it_will_not_render_the_favicon_if_the_favicon_is_set_to_null":0.002,"P\\Tests\\Unit\\SEOManagerTest::__pest_evaluable_the_SEOManager_singleton_works_as_expected":0,"P\\Tests\\Feature\\Tags\\AlternateTagTest::__pest_evaluable_it_will_not_display_the_aternates_tags_if_there_isn_t_any_alternate":0.001,"P\\Tests\\Feature\\Tags\\AlternateTagTest::__pest_evaluable_it_will_display_the_alternates_links_if_the_associated_SEO_model_has_alternates_links":0.003,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager_with_a_SEOData_data_object":0.006,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager":0,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager_with_and_without_a_model":0.011,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_infer_the_title_from_the_url_if_that_is_allowed":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_escape_the_title":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_display_the_title_if_the_associated_SEO_model_has_a_title":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_infer_the_title_from_the_url_if_that_is_allowed_and_the_model_doesn_t_return_a_title":0.003,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_not_infer_the_title_from_the_url_if_that_isn_t_allowed":0.001,"P\\Tests\\Feature\\JSONLD\\FaqPageTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__FaqPageTest":0.002,"P\\Tests\\Feature\\JSONLD\\FaqPageTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__FaqPageTest":0.002,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_add_properties_to_a_SEO_model#('description', 'This is a description')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_add_properties_to_a_SEO_model#('title', 'My Cool Page Title')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_have_immutable_timestamps":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_override_certain_SEO_Data#('title', 'My Cool Page Title')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_override_certain_SEO_Data#('description', 'This is a description')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_morph_a_model_to_the_SEO_model":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_disable_the_suffix_in_the_page_model":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_give_the_title_of_a_page_a_suffix_it_was_specified":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_disable_the_suffix_in_the_page_model_dynamically_via_a_function":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_prepare_the_SEO_for_use_on_a_page":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_cannot_overwrite_the_robots_tag__default__value_with_the_robots_attribute_if__force__default__is_set__DB_Model_":0.003,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_overwrite_the_robots_tag__default__value_with_the_robots_attribute__DB_Model_":0.003,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_cannot_overwrite_the_robots_tag__default__value_with_the_robots_attribute_if__force__default__is_set__SEOData_":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_overwrite_the_robots_tag__default__value_with_the_robots_attribute__SEOData_":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_output_the_robots_tag__default__value":0.002,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_replace_the_title_if_we_re_on_the_homepage#('Custom homepage title', 'Custom homepage title')":0.002,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_replace_the_title_if_we_re_on_the_homepage#(null, '| My Website suffix')":0.001,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_pipe_the_generated_tags_through_the_transformers_just_before_render":0.001,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_pipe_the_SEOData_through_the_transformer_before_putting_it_into_the_collection":0.002,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_can_render_the_SEOData_from_an_object_that_s_directly_passed_in":0.001,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_uses_openGraphTitle_over_title":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags_for_a_post_or_page_with_a_few_additional_overrides":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_will_escape_the_title":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags":0.002,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags_for_a_post_or_page":0.005,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_locale_tags":0.002,"P\\Tests\\Unit\\Support\\TagTest::__pest_evaluable_it_orders_tag_attributes":0.022,"P\\Tests\\Unit\\Schema\\CustomSchemaTest::__pest_evaluable_it_can_construct_a_custom_faq_schema":0.001,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_multiple_custom_JSON_LD_Schemas_markup":0.003,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__FaqPage":0.002,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_a_custom_JSON_LD_Schemas_markup":0.002,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_a_custom_JSON_LD_Schemas_markup_from_a_function":0.002,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__BreadcrumbList":0.003,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__Article":0.003}} \ No newline at end of file diff --git a/src/Support/AlternateTag.php b/src/Support/AlternateTag.php index 05bc7a9..259c9f5 100644 --- a/src/Support/AlternateTag.php +++ b/src/Support/AlternateTag.php @@ -9,6 +9,7 @@ public function __construct( string $href, ) { parent::__construct('alternate', $href); + $this->attributes['hreflang'] = $hreflang; } } diff --git a/src/Support/SchemaTagCollection.php b/src/Support/SchemaTagCollection.php index 1f90de8..9dff9ba 100644 --- a/src/Support/SchemaTagCollection.php +++ b/src/Support/SchemaTagCollection.php @@ -21,9 +21,7 @@ public static function initialize(?SEOData $SEOData = null): ?static $collection = new static(); foreach ($schemas as $schema) { - $collection->push( - new CustomSchema(value($schema, $SEOData)) - ); + $collection->push(new CustomSchema(value($schema, $SEOData))); } foreach ($schemas->markup as $markupClass => $markupBuilders) { diff --git a/src/Support/Tag.php b/src/Support/Tag.php index 25a6191..43f257d 100644 --- a/src/Support/Tag.php +++ b/src/Support/Tag.php @@ -7,9 +7,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; -/** - * A representation of a HTML tag - */ abstract class Tag implements Renderable { const ATTRIBUTES_ORDER = ['rel', 'hreflang', 'title', 'name', 'href', 'property', 'description', 'content']; diff --git a/tests/Feature/JSON-LD/SchemaCollectionTest.php b/tests/Feature/JSON-LD/SchemaCollectionTest.php index 1e7a773..e1d0aee 100644 --- a/tests/Feature/JSON-LD/SchemaCollectionTest.php +++ b/tests/Feature/JSON-LD/SchemaCollectionTest.php @@ -13,7 +13,26 @@ it('can correctly render a custom JSON-LD Schemas markup', function () { $page = Page::create([]); - $faqPageSchema = $this->faqTestSchema; + $faqPageSchema = [ + '@context' => 'https://schema.org', + '@type' => 'FAQPage', + 'mainEntity' => [ + '@type' => 'Question', + 'name' => 'Can this package add FaqPage to the schema?', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Yes!', + ], + ], + [ + '@type' => 'Question', + 'name' => 'Does it support multiple questions?', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Of course.', + ], + ], + ]; $page::$overrides = [ 'schema' => SchemaCollection::make()->add($faqPageSchema), @@ -246,7 +265,26 @@ it('can correctly render multiple custom JSON-LD Schemas markup', function () { $page = Page::create([]); - $faqPageSchema = $this->faqTestSchema; + $faqPageSchema = [ + '@context' => 'https://schema.org', + '@type' => 'FAQPage', + 'mainEntity' => [ + '@type' => 'Question', + 'name' => 'Can this package add FaqPage to the schema?', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Yes!', + ], + ], + [ + '@type' => 'Question', + 'name' => 'Does it support multiple questions?', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Of course.', + ], + ], + ]; $now = now(); $yesterday = now()->yesterday(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 36bca20..32cdbb4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,27 +9,6 @@ class TestCase extends Orchestra { - public array $faqTestSchema = [ - '@context' => 'https://schema.org', - '@type' => 'FAQPage', - 'mainEntity' => [ - '@type' => 'Question', - 'name' => 'Can this package add FaqPage to the schema?', - 'acceptedAnswer' => [ - '@type' => 'Answer', - 'text' => 'Yes!', - ], - ], - [ - '@type' => 'Question', - 'name' => 'Does it support multiple questions?', - 'acceptedAnswer' => [ - '@type' => 'Answer', - 'text' => 'Of course.', - ], - ], - ]; - protected function setUp(): void { parent::setUp(); diff --git a/tests/Unit/Schema/CustomSchemaTest.php b/tests/Unit/Schema/CustomSchemaTest.php index 89f7232..26626f1 100644 --- a/tests/Unit/Schema/CustomSchemaTest.php +++ b/tests/Unit/Schema/CustomSchemaTest.php @@ -3,10 +3,48 @@ use RalphJSmit\Laravel\SEO\Schema\CustomSchema; it('can construct a custom faq schema', function () { - $schema = new CustomSchema($this->faqTestSchema); + $schema = new CustomSchema([ + '@context' => 'https://schema.org', + '@type' => 'FAQPage', + 'mainEntity' => [ + '@type' => 'Question', + 'name' => 'Can this package add FaqPage to the schema?', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Yes!', + ], + ], + [ + '@type' => 'Question', + 'name' => 'Does it support multiple questions?', + 'acceptedAnswer' => [ + '@type' => 'Answer', + 'text' => 'Of course.', + ], + ], + ]); expect((string) $schema->render()) ->toBe( - '' + '' ); }); From 2cd329a196f4f89161ee546c955e194e6bedeb60 Mon Sep 17 00:00:00 2001 From: "Ralph J. Smit" <59207045+ralphjsmit@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:08:34 +0200 Subject: [PATCH 17/17] Delete test-results --- .phpunit.cache/test-results | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .phpunit.cache/test-results diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results deleted file mode 100644 index bd7d8f3..0000000 --- a/.phpunit.cache/test-results +++ /dev/null @@ -1 +0,0 @@ -{"version":"pest_2.34.2","defects":[],"times":{"P\\Tests\\Unit\\Support\\HasSEOTest::__pest_evaluable_it_automatically_associates_a_SEO_model_on_creation":0.002,"P\\Tests\\Feature\\JSONLD\\ArticleTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__Article":0.005,"P\\Tests\\Feature\\JSONLD\\ArticleTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__Article":0.032,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_canonical_URL_if_allowed":0.001,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_model_level_canonical_url_if_set_on_override":0.002,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_cannot_display_the_canonical_url_if_not_allowed":0.001,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_will_not_break_if_no_canonical__url_column_exists_in_seo_table":0.004,"P\\Tests\\Feature\\Tags\\CanonicalTagTest::__pest_evaluable_it_can_display_the_model_level_canonical_url_if_set_in_database":0.003,"P\\Tests\\Unit\\Facades\\SEOManagerTest::__pest_evaluable_the_SEOManager_facade_works_as_expected":0,"P\\Tests\\Unit\\Schema\\ArticleSchemaTest::__pest_evaluable_it_can_construct_Schema_Markup__Article":0,"P\\Tests\\Unit\\Schema\\ArticleSchemaTest::__pest_evaluable_it_can_add_multiple_authors_to_Schema_Markup__Article":0,"P\\Tests\\Unit\\Support\\SEODataTest::__pest_evaluable_it_can_determine_the_size_of_an_image":0.002,"P\\Tests\\Unit\\Support\\SEODataTest::__pest_evaluable_it_can_allow_to_prepareForUsage_without_a_model_in_the_database":0.002,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_display_the_image_url_if_it_came_from_a_model":0.003,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_not_render_the_default_image_if_that_was_disabled":0.001,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_display_the_image_url_from_a_model":0.003,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_render_the_default_image#('\/public\/test\/image.jpg')":0.003,"P\\Tests\\Feature\\Tags\\ImageTagTest::__pest_evaluable_it_will_render_the_default_image#('public\/test\/image.jpg')":0.002,"P\\Tests\\Feature\\Tags\\SitemapTagTest::__pest_evaluable_it_can_display_the_sitemap_if_path_is_set":0.002,"P\\Tests\\Unit\\Schema\\BreadcrumbListSchemaTest::__pest_evaluable_it_can_construct_Schema_Markup__BreadcrumbList":0.001,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_can_override_the_author":0.004,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_can_display_the_fallback_description_tag":0.002,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_will_not_display_the_author_tag_if_there_isn_t_a_author":0.002,"P\\Tests\\Feature\\Tags\\AuthorTagTest::__pest_evaluable_it_will_display_the_author_if_the_associated_SEO_model_has_a_author":0.003,"P\\Tests\\Feature\\JSONLD\\BreadcrumbListTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__BreadcrumbList":0.002,"P\\Tests\\Feature\\JSONLD\\BreadcrumbListTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__BreadcrumbList":0.002,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_will_display_the_description_if_the_associated_SEO_model_has_a_description":0.003,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_can_display_the_fallback_description_tag":0.001,"P\\Tests\\Feature\\Tags\\DescriptionTagTest::__pest_evaluable_it_will_not_display_the_description_tag_if_there_isn_t_a_description":0.002,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_include_the_widths_and_heights_of_Twitter_images_if_the_image_was_overridden_using_a_URL#('summary', 'images\/twitter-1743x1743.jpg', '1743', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_include_the_widths_and_heights_of_Twitter_images_if_the_image_was_overridden_using_a_URL#('summary_large_image', 'images\/twitter-3597x1799.jpg', '3597', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_with_the_image_on_a_Page#('summary_large_image', 'images\/twitter-3597x1799.jpg', '3597', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_with_the_image_on_a_Page#('summary', 'images\/twitter-1743x1743.jpg', '1743', \u2026)":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_uses_openGraphTitle_over_title":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_render_the_Twitter_Card_summary__large__image_for_too_large_or_small_images#('images\/twitter-72x72.jpg')":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_not_render_the_Twitter_Card_summary__large__image_for_too_large_or_small_images#('images\/twitter-4721x4721.jpg')":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_will_escape_the_title":0.003,"P\\Tests\\Feature\\Tags\\TwitterCardSummaryTagsTest::__pest_evaluable_it_can_correctly_render_the_Twitter_Card_summary_without_the_image":0.002,"P\\Tests\\Feature\\Tags\\FaviconTagTest::__pest_evaluable_it_will_render_the_favicon_if_the_favicon_is_set":0.002,"P\\Tests\\Feature\\Tags\\FaviconTagTest::__pest_evaluable_it_will_not_render_the_favicon_if_the_favicon_is_set_to_null":0.002,"P\\Tests\\Unit\\SEOManagerTest::__pest_evaluable_the_SEOManager_singleton_works_as_expected":0,"P\\Tests\\Feature\\Tags\\AlternateTagTest::__pest_evaluable_it_will_not_display_the_aternates_tags_if_there_isn_t_any_alternate":0.001,"P\\Tests\\Feature\\Tags\\AlternateTagTest::__pest_evaluable_it_will_display_the_alternates_links_if_the_associated_SEO_model_has_alternates_links":0.003,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager_with_a_SEOData_data_object":0.006,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager":0,"P\\Tests\\Unit\\HelpersTest::__pest_evaluable_it_can_get_the_TagManager_with_and_without_a_model":0.011,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_infer_the_title_from_the_url_if_that_is_allowed":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_escape_the_title":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_display_the_title_if_the_associated_SEO_model_has_a_title":0.002,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_infer_the_title_from_the_url_if_that_is_allowed_and_the_model_doesn_t_return_a_title":0.003,"P\\Tests\\Feature\\Tags\\TitleTagTest::__pest_evaluable_it_will_not_infer_the_title_from_the_url_if_that_isn_t_allowed":0.001,"P\\Tests\\Feature\\JSONLD\\FaqPageTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__FaqPageTest":0.002,"P\\Tests\\Feature\\JSONLD\\FaqPageTest::__pest_evaluable_it_does_not_render_by_default_the_JSON_LD_Schema_markup__FaqPageTest":0.002,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_add_properties_to_a_SEO_model#('description', 'This is a description')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_add_properties_to_a_SEO_model#('title', 'My Cool Page Title')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_have_immutable_timestamps":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_override_certain_SEO_Data#('title', 'My Cool Page Title')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_override_certain_SEO_Data#('description', 'This is a description')":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_morph_a_model_to_the_SEO_model":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_disable_the_suffix_in_the_page_model":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_give_the_title_of_a_page_a_suffix_it_was_specified":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_disable_the_suffix_in_the_page_model_dynamically_via_a_function":0.001,"P\\Tests\\Unit\\Models\\SEOTest::__pest_evaluable_it_can_prepare_the_SEO_for_use_on_a_page":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_cannot_overwrite_the_robots_tag__default__value_with_the_robots_attribute_if__force__default__is_set__DB_Model_":0.003,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_overwrite_the_robots_tag__default__value_with_the_robots_attribute__DB_Model_":0.003,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_cannot_overwrite_the_robots_tag__default__value_with_the_robots_attribute_if__force__default__is_set__SEOData_":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_overwrite_the_robots_tag__default__value_with_the_robots_attribute__SEOData_":0.001,"P\\Tests\\Feature\\Tags\\RobotsTagTest::__pest_evaluable_it_can_output_the_robots_tag__default__value":0.002,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_replace_the_title_if_we_re_on_the_homepage#('Custom homepage title', 'Custom homepage title')":0.002,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_replace_the_title_if_we_re_on_the_homepage#(null, '| My Website suffix')":0.001,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_pipe_the_generated_tags_through_the_transformers_just_before_render":0.001,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_it_can_pipe_the_SEOData_through_the_transformer_before_putting_it_into_the_collection":0.002,"P\\Tests\\Feature\\TagManagerTest::__pest_evaluable_can_render_the_SEOData_from_an_object_that_s_directly_passed_in":0.001,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_uses_openGraphTitle_over_title":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags_for_a_post_or_page_with_a_few_additional_overrides":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_will_escape_the_title":0.003,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags":0.002,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_OpenGraph_tags_for_a_post_or_page":0.005,"P\\Tests\\Feature\\Tags\\OpenGraphTagsTest::__pest_evaluable_it_can_correctly_render_locale_tags":0.002,"P\\Tests\\Unit\\Support\\TagTest::__pest_evaluable_it_orders_tag_attributes":0.022,"P\\Tests\\Unit\\Schema\\CustomSchemaTest::__pest_evaluable_it_can_construct_a_custom_faq_schema":0.001,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_multiple_custom_JSON_LD_Schemas_markup":0.003,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__FaqPage":0.002,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_a_custom_JSON_LD_Schemas_markup":0.002,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_a_custom_JSON_LD_Schemas_markup_from_a_function":0.002,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__BreadcrumbList":0.003,"P\\Tests\\Feature\\JSONLD\\SchemaCollectionTest::__pest_evaluable_it_can_correctly_render_the_JSON_LD_Schema_markup__Article":0.003}} \ No newline at end of file