基于 Laravel Sitemap 扩展包编写定时任务生成 Laravel 学院站点地图

为什么需要站点地图

开始构建站点地图之前,需要搞清楚什么是站点地图,以及为什么要构建站点地图,Google Support 里面有一个介绍站点地图的页面说的比较清楚,考虑到国内由于某种原因不能访问该页面,学院君将其截图如下:

什么是站点地图

如何构建站点地图

了解了是什么,为什么,接下来就要了解怎么做了,之前学院君在博客系列教程「基于 Laravel 开发博客应用系列 —— 添加评论、RSS 订阅和站点地图功能实现」中,已经介绍过如何构建站点地图,不过那里讲的方法比较简单,不适用于大的站点,为了考虑到扩展性,新版学院基于扩展包 roumen/sitemap + 定时任务实现站点地图的定时构建。

安装配置扩展包

首先通过 Composer 安装扩展包:

composer require roumen/sitemap

对于 Laravel 5.5+ 版本该扩展包会自动发现,无需手动注册,而 Laravel 5.4 及以下版本需要在 config/app.php 中注册以下服务提供者:

Roumen\Sitemap\SitemapServiceProvider::class

安装完成后,可以将配置文件及相关前端资源和视图文件发布到应用根目录下:

php artisan vendor:publish --provider="Roumen\Sitemap\SitemapServiceProvider"

编写 Artisan 命令类

该扩展包支持通过 Web 访问动态生成站点地图,这样做能保证实时性,但是高峰期对系统性能是个不小的考验,我更倾向于通过 Artisan 命令通过定时任务调度执行,为此需要先创建一个 Artisan 命令类:

php artisan make:command GenerateSitemapCommand

该命令会在 app/Console/Commands 目录下生成一个命令类 GenerateSitemapCommand.php,接下来编写这个命令类,开始之前,要先规划好如果构建站点地图,对于大型站点来说,把所有东西都塞到一个站点地图里显然是不合适的,会导致这个文件无限膨胀,进而影响读取速度,更严重的是导致内存溢出,系统不可用,所以我的规划是我会在站点地图入口文件为首页、类目、标签、文章、页面、问答构建索引,由于文章和问答随着时间增长会越来越多,所以我会进一步将它们以月份为维度进行划分,分散存储到不同的子站点地图文件,然后从入口通过索引去访问,以下是站点地图入口文件截图(感兴趣同学可以自己去看:http://laravelacademy.org/sitemap.xml):

laravel学院站点地图

以其中某个索引为例访问其明细:

laravel学院站点地图

需要注意的是你提供的站点地图链接必须都是可以公开访问的,否则搜索引擎抓取不到,有可能会影响站点的索引量。

关于站点地图的构建模式考虑清楚了,接下来就可以编写代码了,我的命令类代码如下,仅供参考:

<?php

namespace App\Console\Commands;

use App\Services\SitemapService;
use Illuminate\Console\Command;

class GenerateSitemapCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'generate:sitemap';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Generate Sitemap Daily';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->info('[' . date('Y-m-d H:i:s', time()) . ']开始执行sitemap生成脚本');
        try {
            $sitemapService = new SitemapService();
            $sitemapService->buildIndex();
        } catch (\Exception $exception) {
            $this->error('生成sitemap失败:' . $exception->getMessage());
            return;
        }
        $this->info('[' . date('Y-m-d H:i:s', time()) . ']生成sitemap成功!');
    }
}

具体的业务逻辑位于 App\Services\SitemapService 里,这里的代码要借鉴的话需要根据自己的表结构做字段和模型调整,不要照搬:

<?php

namespace App\Services;

use App;
use function GuzzleHttp\Psr7\str;
use Log;
use App\Models\Tag;
use App\Models\Category;
use App\Models\Page;
use App\Models\Article;
use Roumen\Sitemap\Sitemap;
use App\Models\Discussion;

class SitemapService
{
    public function buildArticles()
    {
        $sitemap = App::make("sitemap");

        $sitemapName = '';
        $articlesData = [];

        Article::public()->select(['id', 'created_at', 'updated_at'])->chunk(100, function ($articles) use (&$articlesData, &$sitemapName) {
            foreach ($articles as $article) {
                $sitemapName = date('Y-m', strtotime($article->created_at));
                $articlesData[$sitemapName][] = [
                    'url' => route('article.show', ['id' => $article->id]),
                    'lastmod' => strtotime($article->updated_at)
                ];
            }
        });

        $lastModTimes = [];
        foreach ($articlesData as $name => $data) {
            $lastModTime = 0;
            foreach ($data as $_data) {
                if ($_data['lastmod'] > $lastModTime) {
                    $lastModTime = $_data['lastmod'];
                }
                $sitemap->add($_data['url'], date(DATE_RFC3339, $_data['lastmod']), '0.8', 'daily');
            }
            $info = $sitemap->store('xml','articles-' . $name, storage_path('app/public/sitemap'));
            $lastModTimes[$name] = $lastModTime;
            Log::info($info);
            $sitemap->model->resetItems();
        }
        return $lastModTimes;
    }

    public function buildDiscussions()
    {
        $sitemap = App::make("sitemap");

        $sitemapName = '';
        $discussionsData = [];

        Discussion::public()->select(['id', 'created_at', 'updated_at'])->chunk(100, function ($discussions) use (&$discussionsData, &$sitemapName) {
            foreach ($discussions as $discussion) {
                $sitemapName = date('Y-m', strtotime($discussion->created_at));
                $discussionsData[$sitemapName][] = [
                    'url' => route('discussion.show', ['id' => $discussion->id]),
                    'lastmod' => strtotime($discussion->updated_at)
                ];
            }
        });

        $lastModTimes = [];
        foreach ($discussionsData as $name => $data) {
            $lastModTime = 0;
            foreach ($data as $_data) {
                if ($_data['lastmod'] > $lastModTime) {
                    $lastModTime = $_data['lastmod'];
                }
                $sitemap->add($_data['url'], date(DATE_RFC3339, $_data['lastmod']), '0.8', 'daily');
            }
            $info = $sitemap->store('xml','discussions-' . $name, storage_path('app/public/sitemap'));
            $lastModTimes[$name] = $lastModTime;
            Log::info($info);
            $sitemap->model->resetItems();
        }
        return $lastModTimes;
    }

    public function buildPages()
    {
        $sitemap = App::make("sitemap");
        $lastModTime = 0;

        $pages = Page::public()->get(['id','slug','updated_at']);
        foreach ($pages as $page) {
            $pageLastModTime = strtotime($page->updated_at);
            if ($pageLastModTime > $lastModTime) {
                $lastModTime = $pageLastModTime;
            }
            $url = route('page.show', ['slug' => $page->slug]);
            $sitemap->add($url, date(DATE_RFC3339, strtotime($page->updated_at)), '0.6', 'weekly');
        }

        $info = $sitemap->store('xml','pages', storage_path('app/public/sitemap'));
        Log::info($info);
        return $lastModTime;
    }

    public function buildCategories()
    {
        $sitemap = App::make("sitemap");
        $lastModTime = 0;

        Category::with('parent')->public()->chunk(100, function ($categories) use ($sitemap, &$lastModTime) {
            foreach ($categories as $category) {
                $catLastModTime = strtotime($category->updated_at);
                if ($catLastModTime > $lastModTime) {
                    $lastModTime = $catLastModTime;
                }
                if ($category->parent_id == 0) {
                    $url = route('article.root.category', ['name' => $category->slug]);
                } else {
                    $url = route('article.category', ['name' => $category->parent->slug, 'subName' => $category->slug]);
                }
                $sitemap->add($url, date(DATE_RFC3339, strtotime($category->updated_at)), '0.5', 'weekly');
            }
        });

        $info = $sitemap->store('xml','categories', storage_path('app/public/sitemap'));
        Log::info($info);
        return $lastModTime;
    }

    public function buildTags()
    {
        $sitemap = App::make("sitemap");
        $lastModTime = 0;

        Tag::chunk(100, function ($tags) use ($sitemap, &$lastModTime) {
            foreach ($tags as $tag) {
                $tagLastModTime = strtotime($tag->updated_at);
                if ($tagLastModTime > $lastModTime) {
                    $lastModTime = $tagLastModTime;
                }
                $url = route('article.tags', ['name' => $tag->slug]);
                $sitemap->add($url, date(DATE_RFC3339, strtotime($tag->updated_at)), '0.5', 'weekly');
            }
        });

        $info = $sitemap->store('xml','tags', storage_path('app/public/sitemap'));
        Log::info($info);
        return $lastModTime;
    }

    public function buildHome()
    {
        $sitemap = App::make("sitemap");
        $sitemap->add(config('app.url'), date(DATE_RFC3339, time()), '1.0', 'daily');
        $info = $sitemap->store('xml', 'home', storage_path('app/public/sitemap'));
        Log::info($info);
        return true;
    }

    public function buildIndex()
    {
        // create sitemap index
        $sitemap = App::make ("sitemap");

        // add sitemaps (loc, lastmod (optional))
        if ($this->buildHome()) {
            $sitemap->addSitemap(config('app.url') . '/storage/sitemap/home.xml', date(DATE_RFC3339, time()));
        }
        if ($lastModTime = $this->buildTags()) {
            $sitemap->addSitemap(config('app.url') . '/storage/sitemap/tags.xml', date(DATE_RFC3339, $lastModTime));
        }
        if ($lastModTime = $this->buildCategories()) {
            $sitemap->addSitemap(config('app.url') . '/storage/sitemap/categories.xml', date(DATE_RFC3339, $lastModTime));
        }
        if ($lastModTime = $this->buildPages()) {
            $sitemap->addSitemap(config('app.url') . '/storage/sitemap/pages.xml', date(DATE_RFC3339, $lastModTime));
        }
        if ($lastModTimes = $this->buildArticles()) {
            foreach ($lastModTimes as $name => $time) {
                $sitemap->addSitemap(config('app.url') . '/storage/sitemap/articles-' . $name . '.xml', date(DATE_RFC3339, $time));
            }
        }
        if ($lastModTimes = $this->buildDiscussions()) {
            foreach ($lastModTimes as $name => $time) {
                $sitemap->addSitemap(config('app.url') . '/storage/sitemap/discussions-' . $name . '.xml', date(DATE_RFC3339, $time));
            }
        }

        // create file sitemap.xml in your public folder (format, filename)
        $sitemap->store('sitemapindex', 'sitemap');
    }

}

代码编写好了之后,不要忘了在 app/Console/Kernel.php 中注册命令类:

protected $commands = [
    ...
    GenerateSitemapCommand::class,
];

这样,就可以在项目根目录下通过运行如下 Artisan 命令来对代码进行测试:

php artisan generate:sitemap

定时任务调度

我们不可能每天到线上去运行站点地图生成命令,这个需要通过任务调度来实现定时生成站点地图。Laravel 中实现任务调度也很简单,参考下文档,几分钟就能搞定。

首先在 App\Console\Kernel 类中编辑 schedule 方法如下:

protected function schedule(Schedule $schedule)
{
    $schedule->command(GenerateSitemapCommand::class)->twiceDaily(1, 13);
}

我配置了站点地图生成命令每天凌晨1点和下午1点执行(更多调度方法参考文档),这两个时间点都不是业务高峰期,避免对峰值性能的影响。

然后在线上通过 sudo crontab -e 命令新增一条任务调度(如果之前位项目配置过可以忽略):

* * * * * php /你的项目根目录/artisan schedule:run >> /dev/null 2>&1

至此,站点地图生成功能就全部完成了,接下来需要将生成的站点地图提交到各大搜索引擎,通常是百度、Google、必应、搜狗、360等。

将站点地图提交到搜索引擎

我们以百度为例,访问链接提交页面:https://ziyuan.baidu.com/linksubmit/index,如果注册,需要注册站长账号并登录。登录之后在当前页面找到 自动提交 -> sitemap

百度站点地图提交

将自己的站点地图提交过去即可,接下来就是静静等待百度抓取了,你还可以在搜索框通过以下方式获取站点索引量(将对应域名换成你自己的):

site:laravelacademy.org

其他搜索引擎也是类似,你只要在对应的搜索引擎框里通过 site:你的域名 的方式搜索,就能找到对应搜索引擎提交链接/站点地图的方式,比如 Google 的话从下面红框点进去就能找到:

提交站点地图到Google

提交站点地图到Google

好了,关于站点地图的介绍到此就结束了,有什么问题,欢迎在下面的评论中与我交流。

学院君 has written 977 articles

Laravel学院院长,终身学习者

积分:112022 等级:P12 职业:手艺人 城市:杭州

2 条回复

  1. xsj xsj says:

    大牛你好,我是个刚接触laravel的小白,我想问下App\Services\SitemapService;这部分是要自己新建么?

登录后才能进行评论,立即登录?