JSON-LD คืออะไร ใส่ในเว็บ Laravel ยังไงให้ Google อ่านหน้าเว็บเราออก

JSON-LD คือก้อน JSON ในแท็ก script ที่บอก Google ว่าหน้านี้เป็นสินค้า บทความ หรือร้านค้า ราคาเท่าไหร่ ใครเขียน Google อาจเอาไปโชว์ราคาหรือดาวรีวิวในผลค้นหา แต่ไม่รับปากว่าจะโชว์ และข้อมูลต้องตรงกับที่หน้าเว็บแสดง

เผยแพร่
เวลาอ่าน
5 นาที
แท็ก
#Laravel#SEO

HTML บอกคนอ่าน JSON-LD บอก Google

เปิดหน้าสินค้าหน้าหนึ่ง เราเห็นทันทีว่านี่คือกาแฟคั่วกลาง ราคา 320 บาท ยังมีของ แต่โปรแกรมที่อ่าน HTML เห็นเลข 320 เป็นแค่ข้อความใน <span> ไม่รู้ว่าเป็นราคา น้ำหนัก หรือยอดกดถูกใจ Google ต้องเดาเอาจากหน้าตาของหน้า

JSON-LD (JSON for Linking Data) เป็นทางบอกตรง ๆ เราเขียนข้อมูลของหน้าเป็น JSON ใส่ไว้ในแท็ก script ชนิด application/ld+json เบราว์เซอร์ไม่รันและไม่แสดง คนเปิดเว็บจึงไม่เห็น แต่ Google กับ search engine อื่นอ่านได้

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "กาแฟคั่วกลาง 250 กรัม",
  "image": "https://shop.example.com/img/medium-roast.jpg",
  "description": "เมล็ดอาราบิกาจากดอยช้าง คั่วกลาง หอมช็อกโกแลต",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.8",
    "reviewCount": "126"
  },
  "offers": {
    "@type": "Offer",
    "price": "320",
    "priceCurrency": "THB",
    "availability": "https://schema.org/InStock"
  }
}
</script>

key ที่ขึ้นต้นด้วย @ เป็นคำของ JSON-LD เอง ตามสเปก JSON-LD 1.1 ของ W3C:

  • @context บอกว่าชื่อ key ในก้อนนี้มาจากคลังคำไหน แทบทุกเว็บใช้ https://schema.org
  • @type บอกว่าสิ่งนี้คืออะไร เช่น Product, BlogPosting, LocalBusiness
  • @id ตั้งชื่อให้สิ่งนั้น ก้อนอื่นจะได้อ้างถึงได้ (ตัวอย่างนี้ยังไม่ใช้ เดี๋ยวจะเห็นข้างล่าง)

ส่วน name, offers, price เป็นคำของ schema.org แต่ละ type มี property ของตัวเอง เปิดดูได้จากหน้าของ type นั้น เช่น schema.org/Product ค่าที่มีตัวเลือกตายตัวอย่าง availability ก็เขียนเป็น URL ของ schema.org (https://schema.org/InStock)

Google แนะนำ JSON-LD มากกว่า Microdata

ข้อมูลแบบนี้เรียกรวม ๆ ว่า structured data คือข้อมูลที่จัดเป็นโครงสร้างให้เครื่องอ่าน เขียนได้สามแบบ Google รองรับทั้ง JSON-LD, Microdata และ RDFa แต่แนะนำ JSON-LD ถ้าระบบของเราทำได้ ใครเคยแกะธีม WordPress หรือเทมเพลตร้านค้าเก่า ๆ อาจเคยเห็น itemprop แทรกอยู่ใน HTML นั่นคือ Microdata

<!-- Microdata: ใส่ attribute ปนลงไปใน HTML ที่แสดงผล -->
<div itemscope itemtype="https://schema.org/Product">
  <h1 itemprop="name">กาแฟคั่วกลาง 250 กรัม</h1>
  <div itemprop="offers" itemscope itemtype="https://schema.org/Offer">
    <span itemprop="price" content="320">฿320</span>
    <meta itemprop="priceCurrency" content="THB">
  </div>
</div>

<!-- JSON-LD: HTML เดิมไม่ต้องแตะ ข้อมูลแยกไปอยู่ก้อนเดียว -->
<h1>กาแฟคั่วกลาง 250 กรัม</h1>
<span>฿320</span>
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "Product", "name": "กาแฟคั่วกลาง 250 กรัม",
  "offers": { "@type": "Offer", "price": "320", "priceCurrency": "THB" } }
</script>

Microdata ผูกอยู่กับโครงสร้าง HTML พอมีคนย้าย div ข้อมูลก็พังตาม JSON-LD อยู่แยกเป็นก้อน สร้างจากข้อมูลใน database ได้ตรง ๆ ไม่ต้องยุ่งกับ view และ Google ยังอ่าน JSON-LD ที่ JavaScript ใส่เข้ามาทีหลังได้ด้วย

Google อาจโชว์ราคาและดาวรีวิวในผลค้นหา

ผลที่เห็นได้ชัดที่สุดของ JSON-LD คือ rich result ผลค้นหาที่มีข้อมูลเพิ่มมาจากชื่อกับคำอธิบาย

ผลค้นหาแบบธรรมดา กับแบบที่มี Product JSON-LD

หน้าเดียวกัน ถ้า Google อ่านออกว่าเป็นสินค้า อาจแสดงดาว ราคา และสถานะสต็อกใต้ผลค้นหา

แต่ใส่ครบแล้ว Google ก็ไม่รับปากว่าจะโชว์ แนวทาง structured data ของ Google เขียนไว้ชัดว่า ผ่าน Rich Results Test แล้วก็ยังไม่แน่ว่าจะได้แสดง อัลกอริทึมเลือกเองว่าจะโชว์ผลแบบไหน และ Google ไม่เคยบอกว่า structured data ช่วยให้อันดับขึ้น ที่ได้แน่ ๆ คือ Google เข้าใจหน้าเราถูก และหน้าเราเข้าเกณฑ์ที่จะได้แสดงแบบพิเศษ

type ที่ใช้บ่อยในเว็บทั่วไป

Google รวบรวม type ที่รองรับไว้ในแกลเลอรี structured data ตัวที่เว็บบริษัท ร้านค้า และบล็อกได้ใช้บ่อย มีประมาณนี้

type ใส่ในหน้า Google อ่านอะไรไป
Article, BlogPosting บทความ ข่าว บล็อก ชื่อเรื่อง รูป วันที่เผยแพร่ วันที่แก้ไข
Product หน้าสินค้า ราคา สต็อก ดาวรีวิว
LocalBusiness หน้าร้าน คลินิก สาขา ที่อยู่ เบอร์โทร เวลาเปิดปิด
BreadcrumbList ทุกหน้าที่อยู่ลึกกว่าหน้าแรก เส้นทางหน้าที่โชว์แทน URL
Organization หน้าแรก หรือหน้าเกี่ยวกับเรา ชื่อ โลโก้ และบัญชีโซเชียลของบริษัท
Event, JobPosting, Recipe อีเวนต์ ประกาศรับสมัครงาน สูตรอาหาร วันเวลา สถานที่ เงินเดือน วัตถุดิบ

รายการนี้ Google ตัดออกเรื่อย ๆ ก่อนลงแรงกับ type ไหน เปิดแกลเลอรีดูก่อนว่ายังอยู่ไหม

ถ้าเว็บเราใช้ WordPress ปลั๊กอิน SEO อย่าง Yoast SEO หรือ Rank Math ใส่ JSON-LD พื้นฐานให้อยู่แล้ว ลองเปิด view-source ดูก่อนเขียนเพิ่มเอง จะได้ไม่มีสองก้อนที่บอกข้อมูลขัดกัน

ใส่ JSON-LD ในหน้า Blade

สร้างข้อมูลเป็น array ใน controller จาก model ตัวเดียวกับที่หน้าเว็บใช้แสดง ราคาในหน้ากับราคาใน JSON-LD จะได้มาจากช่องเดียวกันเสมอ

public function show(Product $product): View
{
    $jsonLd = [
        '@context' => 'https://schema.org',
        '@type' => 'Product',
        'name' => $product->name,
        'image' => $product->image_url,
        'description' => $product->summary,
        'offers' => [
            '@type' => 'Offer',
            'price' => (string) $product->price,
            'priceCurrency' => 'THB',
            'availability' => $product->stock > 0
                ? 'https://schema.org/InStock'
                : 'https://schema.org/OutOfStock',
            'url' => route('products.show', $product),
        ],
    ];

    return view('products.show', compact('product', 'jsonLd'));
}

พิมพ์ด้วย @json ดีกว่าเขียน {!! json_encode($jsonLd) !!} เอง เพราะ @json ใส่ JSON_HEX_TAG กับ flag อื่นให้ (CompilesJson.php) ตัว < กับ > กลายเป็น < และ > ถ้าคำอธิบายสินค้ามีคนพิมพ์ </script> มา ก็ปิดแท็กแล้วแทรกโค้ดของตัวเองไม่ได้ ถ้าจะเขียน json_encode เองก็ใส่ JSON_HEX_TAG ไว้เสมอ

@json มีสองเรื่องที่ควรรู้

  • ส่งเป็นตัวแปร อย่าเขียน array ลงไปตรง ๆ Blade แยกอาร์กิวเมนต์ของ @json ด้วยการตัดที่จุลภาค @json(['@type' => 'Product', 'name' => $name]) จึงพัง เตรียม array ไว้ใน controller หรือ @php ก่อน
  • ภาษาไทยออกมาเป็น ก… อ่านใน view-source ยาก แต่ Google อ่านได้ปกติ ถ้าอยากให้อ่านง่ายก็ส่ง flag เองได้ ขอให้มี JSON_HEX_TAG อยู่ด้วย
<script type="application/ld+json">@json($jsonLd, JSON_HEX_TAG | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)</script>

อีกตัวที่ชื่อคล้ายกันคือ Js::from() ตัวนี้ให้ผลเป็นคำสั่ง JSON.parse(...) ของ JavaScript ใช้ในแท็ก script ธรรมดาได้ แต่ใน application/ld+json ต้องเป็น JSON ล้วน จึงใช้ไม่ได้

เว็บ Inertia กับ React ทำแบบเดียวกัน

เว็บนี้เองก็ใส่ JSON-LD ทุกหน้า สร้าง array ฝั่ง PHP ส่งเป็น prop ให้ React แล้วพิมพ์ใน <Head> ของ Inertia JSON.stringify ไม่แปลง < ให้เหมือน @json เราจึงแปลงเอง

const jsonLdScript = (node) => JSON.stringify(node).replace(/</g, '\\u003c');

{jsonLd.map((node, index) => (
    <script
        key={index}
        head-key={`ld:${index}`}
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: jsonLdScript(node) }}
    />
))}

เว็บนี้เปิด SSR ไว้ (วิธีการทำงานของ Inertia SSR) JSON-LD จึงอยู่ใน HTML ตั้งแต่ครั้งแรกที่ server ส่งมา ถ้าเป็น SPA ที่ render ฝั่ง browser อย่างเดียว Google ก็ยังอ่านได้ตามที่บอกไว้ข้างบน แต่ search engine หรือบอทตัวอื่นอาจไม่รัน JavaScript

ใช้ @id ผูกข้อมูลข้ามก้อน

หน้าแรกของเว็บนี้มีก้อน Organization ที่ตั้ง @id ไว้ ส่วนโพสต์ทุกหน้าอ้าง @id เดียวกันเป็นผู้เขียนและผู้เผยแพร่ Google จึงรู้ว่าบทความทุกชิ้นเป็นของบริษัทเดียวกัน โดยไม่ต้องใส่โลโก้กับลิงก์โซเชียลซ้ำทุกหน้า

[
  {
    "@context": "https://schema.org",
    "@type": "Organization",
    "@id": "https://phattarachai.dev#organization",
    "name": "Phattarachai Artisan",
    "url": "https://phattarachai.dev",
    "logo": "https://phattarachai.dev/apple-touch-icon.png",
    "sameAs": ["https://github.com/phattarachai"]
  },
  {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": "JSON-LD คืออะไร ใส่ในเว็บ Laravel ยังไงให้ Google อ่านหน้าเว็บเราออก",
    "datePublished": "…",
    "dateModified": "…",
    "author": { "@type": "Organization", "@id": "https://phattarachai.dev#organization", "name": "Phattarachai Artisan" }
  }
]

ตัดมาบางส่วน ก้อนแรกอยู่หน้าแรก ก้อนที่สองอยู่หน้าโพสต์นี้ ลองเปิด view-source หน้านี้ดูของจริงได้

ตรวจก่อนขึ้นจริง

  • Rich Results Test ของ Google วาง URL หรือวางโค้ดก็ได้ บอกว่าหน้านี้เข้าเกณฑ์ rich result แบบไหน และขาด property ที่จำเป็นตัวไหน ถ้ายังเป็นเว็บในเครื่อง (localhost) ให้ copy ก้อน JSON-LD ไปวาง
  • Schema Markup Validator ตรวจกับคำศัพท์ทั้งหมดของ schema.org ไม่ใช่แค่ type ที่ Google ใช้ เหมาะกับเช็กว่าสะกด property ถูกไหม
  • Search Console หลังขึ้น production มีรายงาน rich result แยกตาม type บอกว่ามีกี่หน้าที่ข้อมูลมีปัญหา และเครื่องมือ URL Inspection เช็กได้ว่า Google เจอ JSON-LD ในหน้าไหนแล้วบ้าง

ข้อมูลใน JSON-LD ต้องตรงกับที่หน้าเว็บโชว์

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

  • ใส่ aggregateRating ได้เมื่อหน้านั้นแสดงรีวิวจริง ไม่ใช่ใส่ 4.9 ดาวไว้หวังให้ผลค้นหาดูดี
  • ราคาและสต็อกต้องตรงกับหน้า ถ้าเปลี่ยนราคาในหลังบ้านแล้ว JSON-LD ยังเป็นราคาเก่า ก็ถือว่าไม่ตรง การสร้างจาก model ตัวเดียวกับ view อย่างข้างบนช่วยกันเรื่องนี้ได้
  • อย่าทำหน้าเปล่า ๆ ขึ้นมาเพื่อใส่ JSON-LD อย่างเดียว

ถ้า Google เจอว่าทำผิดกฎ อาจโดน manual action ผลคือหน้านั้นหมดสิทธิ์แสดง rich result ส่วนอันดับในผลค้นหาปกติไม่กระทบ เช็กได้ในรายงาน Manual Actions ของ Search Console

ใส่ JSON-LD แล้ว อีกอย่างที่ช่วยให้ Google เจอหน้าเราครบคือ sitemap ถ้ายังไม่ได้ทำ ลองอ่านสร้าง sitemap.xml ใน Laravel ด้วย spatie/laravel-sitemap ต่อได้ครับ

อ่านต่อ

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

ล่าสุด

ดูทั้งหมด →