ralphjsmit/laravel-seo

一个用于处理任何 Laravel 应用程序 SEO 的包,无论大小。

1.6.3 2024-08-30 11:58 UTC

README

laravel-seo

再次不必担心 Laravel 中的 SEO 问题!

目前 Laravel 的 SEO 包并不多,且现有的包设置复杂,与数据库脱节。它们只提供了生成标签的辅助工具,但您仍然需要手动使用这些辅助工具:没有自动生成,且几乎无法直接使用。

此包可以直接生成有效且有用的元标签,只需有限的初始配置,同时仍提供简单但强大的 API 进行操作。它可以生成

  1. 标题标签(带有站点后缀)
  2. 元标签(作者、描述、图像、机器人等)
  3. OpenGraph 标签(Facebook、LinkedIn 等)
  4. Twitter 标签
  5. 结构化数据(文章、面包屑、FAQ 页面或任何自定义模式)
  6. 网站图标
  7. 机器人标签
  8. 备用链接标签

如果您熟悉 Spatie 的媒体库包,此包几乎以相同的方式工作,但仅用于 SEO。我相信这将非常有帮助,因为网站最好从一开始就关注 SEO。

以下是一些您可以使用此包进行的示例

$post = Post::find(1);

$post->seo->update([
   'title' => 'My great post',
   'description' => 'This great post will enhance your live.',
]);

它将在您的页面上直接渲染 SEO 标签

<!DOCTYPE html>
<html>
<head>
    {!! seo()->for($post) !!}

    {{-- No need to separately render a <title> tag or any other meta tags! --}}
</head>

它甚至允许您从模型中 动态检索 SEO 数据,无需手动保存到 SEO 模型。以下代码不需要您或您的用户进行任何额外工作

class Post extends Model
{
    use HasSEO;

    public function getDynamicSEOData(): SEOData
    {
        $pathToFeaturedImageRelativeToPublicPath = // ..;

        // Override only the properties you want:
        return new SEOData(
            title: $this->title,
            description: $this->excerpt,
            image: $pathToFeaturedImageRelativeToPublicPath,
        );
    }
}

安装

运行以下命令安装此包

composer require ralphjsmit/laravel-seo

发布迁移和配置文件

php artisan vendor:publish --tag="seo-migrations"
php artisan vendor:publish --tag="seo-config"

接下来,前往新发布的配置文件 config/seo.php 并确保所有设置都正确。这些设置都是一些默认值

<?php

return [
    /**
     * Use this setting to specify the site name that will be used in OpenGraph tags.
     */
    'site_name' => null,

    /**
     * Use this setting to specify the path to the sitemap of your website. This exact path will outputted, so
     * you can use both a hardcoded url and a relative path. We recommend the latter.
     *
     * Example: '/storage/sitemap.xml'
     * Do not forget the slash at the start. This will tell the search engine that the path is relative
     * to the root domain and not relative to the current URL. The `spatie/laravel-sitemap` package
     * is a great package to generate sitemaps for your application.
     */
    'sitemap' => null,

    /**
     * Use this setting to specify whether you want self-referencing `<link rel="canonical" href="$url">` tags to
     * 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/.
     */
    'canonical_link' => true,

    'robots' => [
        /**
         * Use this setting to specify the default value of the robots meta tag. `<meta name="robots" content="noindex">`
         * Overwrite it with the robots attribute of the SEOData object. `SEOData->robots = 'noindex, nofollow'`
         * "max-snippet:-1" Use n chars (-1: Search engine chooses) as a search result snippet.
         * "max-image-preview:large" Max size of a preview in search results.
         * "max-video-preview:-1" Use max seconds (-1: There is no limit) as a video snippet in search results.
         * See https://developers.google.com/search/docs/advanced/robots/robots_meta_tag
         * Default: 'max-snippet:-1, max-image-preview:large, max-video-preview:-1'
         */
        'default' => 'max-snippet:-1,max-image-preview:large,max-video-preview:-1',

        /**
         * Force set the robots `default` value and make it impossible to overwrite it. (e.g. via SEOData->robots)
         * Use case: You need to set `noindex, nofollow` for the entire website without exception.
         * Default: false
         */
        'force_default' => false,
    ],

    /**
     * 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 accessible from the `public` folder.
     *
     * You can use the following filetypes: ico, png, gif, jpeg, svg.
     */
    'favicon' => null,

    'title' => [
        /**
         * Use this setting to let the package automatically infer a title from the url, if no other title
         * 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 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,

        /**
         * Use this setting to provide a suffix that will be added after the title on each page.
         * If you don't want a suffix, you should specify an empty string.
         */
        'suffix' => '',

        /**
         * Use this setting to provide a custom title for the homepage. We will not use the suffix on the homepage,
         * so you'll need to add the suffix manually if you want that. If set to null, we'll determine the title
         * just like the other pages.
         */
        'homepage_title' => null,
    ],

    'description' => [
        /**
         * Use this setting to specify a fallback description, which will be used on places
         * where we don't have a description set via an associated ->seo model or via
         * the ->getDynamicSEOData() method.
         */
        'fallback' => null,
    ],

    'image' => [
        /**
         * Use this setting to specify a fallback image, which will be used on places where you
         * don't have an image set via an associated ->seo model or via the ->getDynamicSEOData() method.
         * This should be a path to an image. The url to the path is generated using the `secure_url()` function (`secure_url($yourProvidedPath)`).
         */
        'fallback' => null,
    ],

    'author' => [
        /**
         * Use this setting to specify a fallback author, which will be used on places where you
         * don't have an author set via an associated ->seo model or via the ->getDynamicSEOData() method.
         */
        'fallback' => null,
    ],

    'twitter' => [
        /**
         * Use this setting to enter your username and include that with the Twitter Card tags.
         * Enter the username like 'yourUserName', so without the '@'.
         */
        '@username' => null,
    ],
];

现在,在您希望出现 SEO 标签的每个页面上添加以下 Blade 代码

{!! seo() !!}

这将默认渲染许多合理的标签,从而大大提高您的 SEO。它还将渲染像 <title> 标签这样的内容,因此您不必手动渲染。

要真正利用此包,您可以 将 Eloquent 模型与 SEO 模型关联。这将允许您从模型中 动态获取 SEO 数据,并且此包将根据该数据生成尽可能多的标签。

要将 Eloquent 模型与 SEO 模型关联,请向您的模型添加 HasSEO 特性

use RalphJSmit\Laravel\SEO\Support\HasSEO;

class Post extends Model
{
    use HasSEO;

    // ...

这将自动为您创建并关联一个 SEO 模型,当创建 Post 时。您还可以手动为 Post 创建 SEO 模型,使用 ->addSEO() 方法($post->addSEO())。

您可以通过 Eloquent 的 seo 关系获取 SEO 模型

$post = Post::find(1);

$seo = $post->seo;

在 SEO 模型中,您可以 更新以下属性

  1. title:这将用于 <title> 标签以及所有相关标签(OpenGraph、Twitter 等)
  2. description:这将用于 <meta> 描述标签以及所有相关标签(OpenGraph、Twitter 等)
  3. 作者:此处应填写作者姓名,它将被用于 <meta> 作者标签以及所有相关标签(OpenGraph、Twitter等)。
  4. 图像:此处应填写用于 <meta> 图像标签以及所有相关标签(OpenGraph、Twitter等)的图像路径。图像的URL由 secure_url() 函数生成,因此请确保图像是公开可用的,并且您提供了正确的路径。
  5. 机器人
    • 覆盖默认的机器人值,该值在配置中设置。(见 'seo.robots.default')。
    • 类似 noindex,nofollow 的字符串 (规范),该字符串被添加到 <meta name="robots">
$post = Post::find(1);

$post->seo->update([
   'title' => 'My title for the SEO tag',
   'image' => 'images/posts/1.jpg', // Will point to `public_path('images/posts/1.jpg')`
]);

但是,每次您进行更改时手动更新 SEO 模型可能有些繁琐。这就是为什么我提供了 getDynamicSEOData() 方法,您可以使用它从您的模型动态获取正确的数据并将其传递给 SEO 模型。

public function getDynamicSEOData(): SEOData
{
    return new SEOData(
        title: $this->title,
        description: $this->excerpt,
        author: $this->author->fullName,
        alternates: [
            new AlternateTag(
                hreflang: 'en',
                href: "https://example.com/en",
            ),
            new AlternateTag(
                hreflang: 'fr',
                href: "https://example.com/fr",
            ),
        ],
    );
}

您可以仅覆盖您想要的属性,并省略其他属性(或向它们传递 null)。您可以使用以下属性

  1. 标题
  2. 描述
  3. 作者(应为作者的姓名)
  4. 图像(应为图像路径,并且与 $url = public_path($path) 兼容)
  5. url(默认为 url()->current()
  6. enableTitleSuffix(应为 truefalse,这允许您在 config/seo.php 文件中设置后缀,该后缀将被追加到每个标题)
  7. 站点名称
  8. 发布时间(应为具有发布时间的 Carbon 实例。默认情况下,这将是您模型中的 created_at 属性)
  9. 修改时间(应为具有发布时间的 Carbon 实例。默认情况下,这将是您模型中的 updated_at 属性)
  10. 部分(应为您内容的部分名称。它用于 OpenGraph 文章标签,可能是文章的分类等)
  11. 标签(应为包含标签的数组。它用于 OpenGraph 文章标签)
  12. schema(这应该是一个 SchemaCollection 实例,其中您可以配置 JSON-LD 结构化数据标签)
  13. locale(这应该是页面的区域设置。默认情况下,这是从 app()->getLocale() 派生的,看起来像 ennl。)
  14. robots(应为包含机器人元标签内容值的字符串,如 nofollow,noindex)。您也可以使用 $SEOData->markAsNoIndex() 来防止页面被索引。
  15. alternates(应为 AlternateTag 的数组)。将渲染 <link rel="alternate" ... /> 标签。

最后,您应该更新您的 Blade 文件,以便它在生成标签时接收您的模型。

{!! seo()->for($page) !!}
{{-- Or pass it directly to the `seo()` method: --}}
{!! seo($page ?? null) !!}

生成标签时使用以下顺序(较高的覆盖较低的)

  1. 来自 SEOManager::SEODataTransformer($closure) 的任何覆盖(见下文)
  2. getDynamicSEOData() 方法的数据
  3. 关联 SEO 模型($post->seo)的数据
  4. config/seo.php 文件中的默认数据

直接从控制器传递 SEOData

另一种选择是将 SEOData 对象直接从控制器传递到布局文件,传递到 seo() 函数。

use Illuminate\Contracts\View\View;
use RalphJSmit\Laravel\SEO\Support\SEOData;

class Homepage extends Controller
{
    public function index(): View
    {
        return view('project.frontend.page.homepage.index', [
            'SEOData' => new SEOData(
                title: 'Awesome News - My Project',
                description: 'Lorem Ipsum',
            ),
        ]);
    }
}
{!! seo($SEOData) !!}

生成 JSON-LD 结构化数据

此包还可以为您生成任何结构化数据(也称为 schema markup)。结构化数据是一个非常广泛的主题,所以我们强烈建议您查看 针对该主题的 Google 文档

结构化数据可以通过两种方式添加

  • 构造结构化数据格式的自定义数组,然后该数组由包在正确的位置渲染为 JSON。
  • 使用3个预定义模板之一,流畅地构建您的结构化数据(文章面包屑列表FAQ页面)。

添加您的第一个架构

以FAQ页面架构标记为例,让我们将其添加到我们的网站中

use RalphJSmit\Laravel\SEO\SchemaCollection;

public function getDynamicSEOData(): SEOData
{
    return new SEOData(
        // ...
        schema: SchemaCollection::make()
            ->add(fn (SEOData $SEOData) => [
                // You could use the `$SEOData` to dynamically
                // fetch 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',
                    ],
                ],
            ]),
    );
}

技巧

在添加新架构时,您可以在此文档中查看要添加的键。

预配置架构:文章和面包屑列表

为了帮助您开始使用结构化数据,我们添加了3个预配置架构,您可以使用流畅的方法来构建。以下类型可用

  1. 文章
  2. 面包屑列表
  3. FAQ页面

文章架构标记

为了自动和流畅地生成文章架构标记,请使用->addArticle()方法

use RalphJSmit\Laravel\SEO\SchemaCollection;

public function getDynamicSEOData(): SEOData
{
    return new SEOData(
        // ...
        schema: SchemaCollection::make()->addArticle(),
    );
}

这将使用由SEOData对象提供的所有数据构建文章架构。您可以将闭包传递给->addArticle()方法以自定义单个架构标记。此闭包将接收ArticleSchema实例作为其参数。您可以使用->addAuthor()方法添加额外的作者。

use RalphJSmit\Laravel\SEO\Schema\ArticleSchema;
use RalphJSmit\Laravel\SEO\SchemaCollection;
use RalphJSmit\Laravel\SEO\Support\SEOData;
use Illuminate\Support\Collection;

public function getDynamicSEOData(): SEOData
{
    return new SEOData(
        // ...
        title: "A boring title"
        schema: SchemaCollection::make()
            ->addArticle(function (ArticleSchema $article, SEOData $SEOData): ArticleSchema {
                return $article->addAuthor($this->moderator);
            }),
    );
}

您可以通过在ArticleSchema实例上使用->markup()方法来完全自定义架构标记

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"` :)
    });
});

技巧

有关更多信息,请参阅Google关于文章的文档。

面包屑列表架构标记

您还可以通过在SchemaCollection上使用->addBreadcrumbList()函数来添加BreadcrumbList架构标记。

默认情况下,架构将仅包含来自$SEOData->url的当前url。

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 {
               // ...
            });
    });

此代码将生成具有以下四个页面的BreadcrumbList JSON-LD结构化数据

  1. 主页
  2. 分类
  3. [当前页面]
  4. 子文章

技巧

有关更多信息,请参阅Google关于面包屑列表的文档。

FAQ页面架构标记

您还可以通过在SchemaCollection上使用->addFaqPage()函数来添加FAQPage架构标记

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.");
   });

技巧

有关更多信息,请参阅Google关于FAQ页面的文档。

技巧

在生成结构化数据后,始终建议您使用Google的丰富结果验证器测试您的网站

高级使用

有时您可能有一些高级需求,需要您在生成标签之前应用自己的逻辑到SEOData类。

为此,您可以使用SEOManager外观的SEODataTransformer()函数注册一个或多个闭包,这些闭包将能够在最后时刻修改SEOData实例

// In the `boot()` method of a service provider somewhere
use RalphJSmit\Laravel\SEO\Facades\SEOManager;

SEOManager::SEODataTransformer(function (SEOData $SEOData): SEOData {
    // This will change the title on *EVERY* page. Do any logic you want here, e.g. based on the current request.
    $SEOData->title = 'Transformed Title';

    return $SEOData;
});

请确保在每个闭包中返回$SEOData对象。

在渲染之前修改标签

您还可以在生成标签之前立即注册可以修改最终生成的标签集合的闭包。如果您想添加自定义标签或修改标签的输出,这很有用。

SEOManager::tagTransformer(function (TagCollection $tags): TagCollection {
    $tags = $tags->reject(fn(Tag $tag) => $tag instanceof OpenGraphTag);

    $tags->push(new MetaTag(name: 'custom-tag', content: 'My custom content'));
    // Will render: <meta name="custom-tag" content="My custom content">

    return $tags;
});

路线图

我希望这个包对您有帮助!如果您有任何关于如何使其更有用的想法或建议,请告诉我(rjs@ralphjsmit.com)或通过问题。

欢迎提交PR,所以请随意Fork并提交拉取请求。我将很高兴审查您的更改,思考并添加到包中。

常规

🐞 如果您发现错误,请提交详细的错误报告,我将尽快修复。

🔐 如果您发现了一个漏洞,请查阅我们的安全策略

🙌 如果您想贡献,请提交一个pull请求。所有pull请求都将得到全额认可。如果您不确定我会不会接受您的想法,请随时联系我!

🙋‍♂️ Ralph J. Smit