iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 11

Day 11:排程 sitemap 自動產生——為什麼「產生」跟「回應請求」要刻意拆開

  • 分享至 

  • xImage
  •  

前言:sitemap.xml 應該是即時算出來的,還是預先算好的?

「使用者或搜尋引擎爬蟲請求 sitemap.xml 的時候,即時查資料庫組出 XML 回傳,不是很直覺嗎?」

即時組出來確實直覺,但如果網站的頁面跟新聞數量多,每次請求都要重新查一次全站的頁面、新聞、快速連結,這個回應會變慢,而且每一次爬蟲來訪,都要重新承擔一次這個查詢成本。今天要看的是這個系統怎麼用排程機制,把「產生 sitemap 內容」跟「回應 sitemap 請求」拆成兩件事。

今日目標

  • 認識 Laravel 的 Task Scheduling(Schedule facade)怎麼設定排程任務
  • 看一個真實案例:一支專門「產生」sitemap 的 Command,透過排程每天執行
  • 理解「產生」跟「回應請求」拆開的設計好處
  • 認識 Schedule::job()Schedule::command() 兩種排程方式的差異

本文主體

排程任務怎麼定義

Laravel 的排程設定集中在 routes/console.php

Schedule::job(FetchNewsJob::class)->hourly()->withoutOverlapping();
Schedule::job(SyncNewsJob::class)->hourly()->withoutOverlapping();

Schedule::command('telescope:prune')->daily();
Schedule::command('sitemap:generate')->daily();

Schedule::job() 排程的是一個 Queue Job(明天後天會細講),Schedule::command() 排程的是一支 Artisan Command。sitemap:generate 這支指令被設定成每天執行一次——不是每次有人請求 sitemap.xml 才即時產生,而是每天固定時間先算好,存起來備用

產生 sitemap 的 Command

class GenerateSitemap extends Command
{
    protected $signature = 'sitemap:generate';

    public function handle(): void
    {
        UrlFacade::forceScheme('https');

        $sitemap = Sitemap::create();

        $this->addHomePage($sitemap);
        $this->addPages($sitemap);
        $this->addNews($sitemap);
        $this->addQuickLinkCategories($sitemap);
        $this->addStaticPages($sitemap);

        Storage::put('sitemap.xml', $sitemap->render());

        $this->info('Sitemap generated successfully.');
    }
}

這支 Command 用 spatie/laravel-sitemap 套件組出整個網站地圖——把首頁、所有已發佈的頁面、所有已發佈的新聞、快速連結分類頁、還有幾個固定的靜態頁面,各自組成一個 URL 條目,設定對應的更新頻率跟優先度,最後把整份 XML 內容存進 Storage,而不是直接回傳給誰

為什麼要強制 https

注意 handle() 一開始有一行 UrlFacade::forceScheme('https')。排程指令是在背景執行的,不是透過一次真實的 HTTP 請求觸發,這代表 Laravel 沒有一個「當下這次請求是不是 https」的線索可以參考,route() helper 組網址時預設可能會用 http。手動強制 scheme 為 https,確保產生出來的 sitemap 裡的網址都是正確的協定,不會因為排程執行環境的差異而產生錯誤的連結。

各分類怎麼組進 sitemap

以新聞跟頁面為例,各自用對應的已發佈 scope(前幾天看過的 invokable scope class)過濾,只把真正該公開的內容加進地圖:

private function addNews(Sitemap $sitemap): void
{
    News::query()
        ->tap(new NewsPublished)
        ->get()
        ->each(function (News $news) use ($sitemap) {
            $sitemap->add(
                Url::create($news->url)
                    ->setLastModificationDate($news->updated_at)
                    ->setPriority(0.6)
                    ->setChangeFrequency(Url::CHANGE_FREQUENCY_MONTHLY)
            );
        });
}

頁面用 whereNotIn('template', [...]) 額外排除掉「別名」「外部連結」「登入」這幾種特殊型別的頁面(還記得 Day 4 講的 PageType 嗎?這裡就是它被消費的另一個地方)——這幾種頁面本身不該出現在搜尋引擎的網站地圖裡,因為它們不是真正的內容頁。

為什麼「產生」跟「回應」要拆開

把這支 Command 跟 Day 12 要看的「回應 sitemap.xml 請求」的邏輯放在一起看,會發現這個系統刻意把兩件事分開負責:

  • 產生(今天看的 sitemap:generate):負責查資料庫、組內容、存檔案,跑在排程裡,跟任何一次真實的使用者請求無關
  • 回應(明天要看的 Controller):負責讀已經存好的檔案、用正確的 HTTP header 回傳,不需要再碰資料庫

這樣拆開的好處很直接:不管一天之內有多少次 sitemap.xml 的請求(搜尋引擎爬蟲通常會頻繁造訪),回應邏輯都只是讀一個已經存在的靜態檔案,完全不會對資料庫造成額外負擔。 真正查資料庫組內容這件「重」的事,一天只發生一次,而且是在排程的時間點、不是在使用者等待回應的當下。

❌ 每次請求都即時查詢組出 sitemap

public function xml()
{
    $sitemap = Sitemap::create();
    // 每次都重新查詢所有已發佈的新聞、頁面...
    return response($sitemap->render(), 200, ['Content-Type' => 'application/xml']);
}

爬蟲越常來訪,資料庫查詢負擔越重,而且內容其實不會秒級變化,即時查詢換來的「即時性」意義不大。

✅ 排程定期產生,請求只負責讀取已存好的內容

// 排程:定期產生
Schedule::command('sitemap:generate')->daily();

// Controller:只讀取,不查資料庫

今日思考題

你的專案裡有沒有一個「內容其實不需要每次請求都即時算」的功能,目前卻是即時查詢組出來的?排程 + 快取檔案這種模式,適合套用進去嗎?

今日重點回顧

  • Laravel 的 Schedule::command()Schedule::job() 可以設定週期性自動執行的任務
  • 真實案例:sitemap:generate 每天排程執行一次,查詢所有已發佈內容,組出完整 sitemap 存進檔案
  • 排程指令沒有真實 HTTP 請求的上下文,forceScheme('https') 這類手動設定是背景執行常見的細節
  • 把「產生」(重、可以排程慢慢做)跟「回應」(輕、要即時反應)拆開,是常見且有效的效能設計

明日預告

明天要看這個系統一個真實發生過的 bug——sitemap.xml 明明產生出來了,Google Search Console 卻讀不到,根因藏在一個意想不到的地方。


上一篇
Day 10:一支 Command,修一次資料遷移後留下的欄位缺失
下一篇
Day 12:一個 Content-Type 的 bug——sitemap.xml 為什麼 Google 解析不了
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言