สร้าง sitemap.xml ใน Laravel ด้วย spatie/laravel-sitemap

ใช้ spatie/laravel-sitemap สร้าง sitemap.xml จาก route และ model หรือให้ crawl ทั้งเว็บ ตั้ง scheduler ให้สร้างใหม่ทุกวัน แล้วบอก Google ผ่าน robots.txt กับ Search Console

เผยแพร่
อัปเดต
— ปรับให้ตรงกับ Laravel 13 และ laravel-sitemap v8 เพิ่มวิธีใส่ URL จาก model
เวลาอ่าน
4 นาที
ในโพสต์นี้ 6 หัวข้อ
  1. sitemap.xml บอก Google ว่าเว็บเรามีหน้าไหนบ้าง
  2. ติดตั้ง spatie/laravel-sitemap
  3. ใส่ URL เองจาก route และ model
  4. ให้ package ไล่ crawl เว็บแทน
  5. สร้างใหม่ทุกวันด้วย scheduler
  6. บอก Google ว่า sitemap อยู่ที่ไหน

Sitemap ช่วยให้ crawler ของ search engine รู้ว่าเว็บเรามีหน้าไหนให้เก็บบ้าง บทความนี้จะทำ sitemap.xml ให้เว็บที่เขียนด้วย Laravel โดยใช้ package spatie/laravel-sitemap ตั้งแต่ใส่ URL เอง ให้ model สร้าง URL ของตัวเอง ไปจนถึงตั้งให้สร้างใหม่ทุกวันและส่งให้ Google

sitemap.xml บอก Google ว่าเว็บเรามีหน้าไหนบ้าง

Sitemap คือไฟล์ XML ที่รวม URL ของหน้าในเว็บเราไว้ จะใส่วันที่แก้ไขล่าสุดของแต่ละหน้าด้วยก็ได้ Google อ่านไฟล์นี้แล้วใช้เป็นแนวทางว่าจะเข้ามา crawl หน้าไหน

ถ้าหน้าในเว็บลิงก์ถึงกันดีอยู่แล้ว Google ก็ไล่ตามลิงก์จนเจอเองได้ Google Search Central บอกว่าเว็บที่มีราว 500 หน้าหรือน้อยกว่า ลิงก์ภายในครบ และไม่ได้มีรูปหรือวิดีโอเยอะ อาจไม่ต้องมี sitemap เลย แต่ sitemap จะช่วยมากเมื่อ

  • เว็บใหญ่ หน้าใหม่ที่เพิ่งเพิ่มเข้ามาอาจหลุดสายตา Googlebot ไป
  • บางหน้าไม่มีลิงก์จากหน้าอื่นในเว็บชี้มาถึง Google ก็ไม่มีทางตามไปเจอ
  • เว็บเพิ่งเปิดใหม่ และยังมีเว็บอื่นลิงก์มาหาน้อย crawler ตามลิงก์ไปทีละหน้า ถ้ายังไม่มีใครลิงก์มาเลย Google ก็อาจหาเว็บเราไม่เจอ
  • มีรูปหรือวิดีโอเยอะ และอยากให้ขึ้นในผลค้นหา

ถึงมี sitemap แล้ว Google ก็ยังเลือกเองว่าจะ crawl และ index หน้าไหนเมื่อไร บาง URL ในนั้นอาจไม่ได้ขึ้นผลค้นหาเลย แต่ทำไว้ก็ไม่เสียอะไร และ Laravel ทำได้ในไม่กี่บรรทัด

ติดตั้ง spatie/laravel-sitemap

composer require spatie/laravel-sitemap

ตอนนี้ package อยู่ที่ เวอร์ชัน 8.2 ต้องใช้ PHP 8.4 กับ Laravel 12 ขึ้นไป (composer.json) ถ้าโปรเจกต์ยังอยู่บน Laravel 11 หรือ PHP ต่ำกว่า 8.4 composer จะลงเวอร์ชัน 7 ให้แทน โค้ดในบทความนี้ใช้ได้กับทั้งสองเวอร์ชัน ยกเว้น callback ของ crawler ที่ v8 เปลี่ยนชนิดของพารามิเตอร์ (ดู UPGRADING.md)

package ทำ sitemap ได้สองทาง คือใส่ URL เองจากข้อมูลที่แอปมีอยู่แล้ว กับให้ package ไล่ crawl เว็บเราแล้วเก็บทุกลิงก์ที่เจอ ถ้าหน้าส่วนใหญ่มาจากฐานข้อมูลอยู่แล้ว ทางแรกดีกว่า เพราะเร็วกว่า และรู้วันที่แก้ไขของแต่ละหน้าจริง ๆ

ใส่ URL เองจาก route และ model

Sitemap::create() สร้าง sitemap เปล่าขึ้นมา แล้วเรา add() URL เข้าไปทีละอัน จะใส่เป็น path เฉย ๆ หรือสร้างด้วย Url::create() เพื่อใส่วันที่แก้ไขล่าสุดด้วยก็ได้ สุดท้าย writeToFile() เขียนออกเป็นไฟล์

use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\Tags\Url;

Sitemap::create()
    ->add('/')
    ->add('/about')
    ->add(Url::create('/contact')->setLastModificationDate(now()->subMonth()))
    ->writeToFile(public_path('sitemap.xml'));

path อย่าง /about จะกลายเป็น URL เต็มด้วย url() ของ Laravel ตอนรันจาก command Laravel เอาโดเมนมาจาก APP_URL ดังนั้น ตั้ง APP_URL ใน .env บน production ให้ถูก ไม่อย่างนั้นทุก URL ใน sitemap จะขึ้นต้นด้วย http://localhost

เราเขียนไฟล์ไว้ใน public/ เพื่อให้เปิดได้ที่ https://example.com/sitemap.xml ตรง ๆ

ให้ model สร้าง URL ของตัวเองด้วย Sitemapable

หน้าที่มาจากฐานข้อมูล เช่นบทความหรือสินค้า ให้ model implement interface Sitemapable แล้วบอกใน toSitemapTag() ว่าแต่ละแถวมี URL อะไร

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\Sitemap\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url;

class Post extends Model implements Sitemapable
{
    public function toSitemapTag(): Url|string|array
    {
        return Url::create(route('posts.show', $this))
            ->setLastModificationDate($this->updated_at);
    }
}

ตัวอย่างนี้สมมติว่ามี route ชื่อ posts.show อยู่แล้ว เช่น Route::get('/blog/{post:slug}', ...)->name('posts.show') ทีนี้ส่ง model เข้า add() ได้เลย จะทีละตัวหรือทั้ง collection ก็ได้

use App\Models\Post;
use Spatie\Sitemap\Sitemap;

Sitemap::create()
    ->add('/')
    ->add(Post::query()->whereNotNull('published_at')->get())
    ->writeToFile(public_path('sitemap.xml'));

อย่าลืมกรองเฉพาะหน้าที่เปิดให้คนทั่วไปเห็นแล้ว ถ้าใส่โพสต์ร่างลงไป Google จะเข้าไปเจอหน้า 404 หรือหน้าที่ยังไม่พร้อม

ไฟล์ที่ได้หน้าตาประมาณนี้

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
    <url>
        <loc>https://example.com</loc>
    </url>
    <url>
        <loc>https://example.com/blog/first-post</loc>
        <lastmod>2026-10-01T09:30:00+07:00</lastmod>
    </url>
    <!-- URL ที่เหลือ -->
</urlset>

ให้ package ไล่ crawl เว็บแทน

ถ้าเว็บเป็นหน้าคงที่เยอะ ไม่ได้มาจาก model ให้ SitemapGenerator เปิดหน้าแรกแล้วไล่ตามลิงก์ไปทั่วเว็บแทน

use Spatie\Sitemap\SitemapGenerator;

SitemapGenerator::create(config('app.url'))
    ->writeToFile(public_path('sitemap.xml'));

create() รับ URL หน้าแรกที่จะเริ่ม crawl package จะตามเฉพาะลิงก์ในโดเมนเดียวกัน วิธีนี้มีข้อเสียตรงที่เว็บต้องเปิดได้จริงตอนสั่ง และ package ไม่รู้ว่าแต่ละหน้าแก้ล่าสุดเมื่อไร ไฟล์ที่ได้จึงมีแค่ <loc> ไม่มี <lastmod>

จะ crawl แล้วเติมหน้าที่ crawler หาไม่เจอเข้าไปด้วยก็ได้ โดยเรียก getSitemap() ก่อนแล้วค่อย add()

use App\Models\Post;
use Spatie\Sitemap\SitemapGenerator;

SitemapGenerator::create(config('app.url'))
    ->getSitemap()
    ->add(Post::query()->whereNotNull('published_at')->get())
    ->writeToFile(public_path('sitemap.xml'));

ถ้าลิงก์ในเว็บสร้างด้วย JavaScript crawler ปกติจะมองไม่เห็น package เปิด headless Chrome รัน JavaScript ให้ได้ แต่ต้องลง spatie/browsershot เพิ่มและตั้ง execute_javascript ใน config ดูวิธีได้ใน เอกสารของ package

สร้างใหม่ทุกวันด้วย scheduler

เอาโค้ดข้างบนไปใส่ใน Artisan command แล้วให้ scheduler สั่งรันทุกวัน ตั้งแต่ Laravel 11 ไม่มี app/Console/Kernel.php แล้ว ทั้ง command แบบ closure และ schedule อยู่ใน routes/console.php ไฟล์เดียว

use App\Models\Post;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
use Spatie\Sitemap\Sitemap;

Artisan::command('sitemap:generate', function (): void {
    Sitemap::create()
        ->add('/')
        ->add('/about')
        ->add(Post::query()->whereNotNull('published_at')->get())
        ->writeToFile(public_path('sitemap.xml'));

    $this->info('สร้าง sitemap.xml แล้ว');
})->purpose('Generate sitemap.xml');

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

ลองรันเองก่อนหนึ่งครั้ง แล้วเปิด public/sitemap.xml ดู

php artisan sitemap:generate

ถ้า command ยาวขึ้นเรื่อย ๆ จะ แยกเป็นคลาสด้วย php artisan make:command ก็ได้ ส่วน Schedule::command(...) ยังอยู่ใน routes/console.php เหมือนเดิม

daily() รันตอนเที่ยงคืนทุกวัน แต่ scheduler จะทำงานก็ต่อเมื่อ server เรียก schedule:run ทุกนาที ถ้ายังไม่เคยตั้ง ให้เพิ่ม cron นี้บน server (เอกสาร Laravel) บน shared hosting ที่ใช้ cPanel หรือ DirectAdmin ก็เพิ่มได้ที่เมนู Cron Jobs

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

อยากให้ sitemap อัปเดตทันทีที่เผยแพร่โพสต์ใหม่ ไม่ต้องรอถึงเที่ยงคืน ก็ดัก model event saved ของ Post แล้วสั่ง Artisan::queue('sitemap:generate') ได้

แล้วถ้าตั้ง cron ไม่ได้ล่ะ

Sitemap ส่งกลับจาก route ได้ตรง ๆ package จะตอบเป็น XML พร้อม content type ให้เอง เราจึงสร้าง sitemap ตอนมีคนเปิด /sitemap.xml แทนการเขียนไฟล์ทิ้งไว้ได้

use App\Models\Post;
use Illuminate\Support\Facades\Route;
use Spatie\Sitemap\Sitemap;

Route::get('/sitemap.xml', function () {
    return Sitemap::create()
        ->add('/')
        ->add(Post::query()->whereNotNull('published_at')->get());
});

ใช้วิธีนี้ต้องลบ public/sitemap.xml ทิ้งก่อน เพราะ web server จะส่งไฟล์จริงใน public/ ให้เลยโดยไม่ผ่าน route และถ้ามีหน้าเป็นหมื่นหน้า ทุกครั้งที่ Googlebot เข้ามาจะ query ฐานข้อมูลทั้งชุด ควร cache ผลไว้หรือกลับไปใช้วิธีเขียนไฟล์

บอก Google ว่า sitemap อยู่ที่ไหน

เพิ่มบรรทัด Sitemap: ใน public/robots.txt ใส่เป็น URL เต็ม crawler ที่อ่าน robots.txt จะรู้ว่า sitemap อยู่ตรงไหน

User-agent: *
Disallow:

Sitemap: https://example.com/sitemap.xml

ดูเรื่อง robots.txt เพิ่มได้ใน คู่มือของ Google

robots.txt ช่วยให้ Google หาเจอ แต่ไม่บอกผลอะไรเรา ถ้าอยากรู้ว่า Google อ่าน sitemap ได้ไหม เจอกี่ URL และ index ไปแล้วเท่าไร ให้ยืนยันความเป็นเจ้าของโดเมนใน Google Search Console แล้วส่ง URL ของ sitemap ที่เมนู Sitemaps ขั้นตอนอยู่ใน Build and submit a sitemap

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →