Bỏ qua, tới nội dung
Tăng trưởng
6 phút đọc

SEO và GEO cho landing page của app: guide, JSON-LD và llms.txt

Những gì mình đặt lên bảy landing page tĩnh để Google và các công cụ trả lời bằng AI tìm thấy, trích dẫn được: guide, FAQPage JSON-LD, llms.txt, robots, sitemap.

Dao Van Thuong
Mobile & Fullstack Engineer
Read in English
Ảnh bìa: landing page của app gửi guide, dữ liệu có cấu trúc và llms.txt tới crawler tìm kiếm và AI
Trong bài này
  1. Mọi thứ đều tĩnh
  2. Guide trả lời câu hỏi thật
  3. Dữ liệu có cấu trúc sinh từ cùng một dictionary
  4. hreflang, x-default và một bài học từ Search Console
  5. Sitemap, robots và llms.txt
  6. Rút ra

Mỗi app iOS trong bảy app của mình có một landing page riêng trên tên miền .io.vn (vì sao chọn .io.vn): lockboxy.io.vn, stampzy.io.vn, talkzy.io.vn, baton.io.vn, minivid.io.vn, ringsy.io.vn và linkeeper.io.vn. Trước đây chúng chủ yếu chỉ có trang chủ cùng các trang privacy, terms và support. Người đã có link App Store thì vậy là đủ, còn người chưa biết app thì gần như không tìm ra.

Đầu tháng 10, mình làm cùng một lượt cho cả bảy site: thêm mục guide, trang FAQ đầy đủ, metadata riêng cho từng trang, dữ liệu có cấu trúc, sitemap kèm ảnh, llms.txt, và file robots gọi tên các crawler AI. Mục tiêu là hai kiểu được tìm thấy: tìm kiếm truyền thống (SEO), và được ChatGPT, Claude, Perplexity hay phần trả lời AI của Google trích dẫn (GEO, generative engine optimization).

Bài này kể lại các site đó có gì và vì sao, dựa trên code và commit trong các repo landing.

Mọi thứ đều tĩnh

Cả bảy landing đều là Next.js App Router với hai locale, /vi và /en (năm site mặc định tiếng Việt). Mọi route quan trọng được prerender lúc build. Trang guide dùng generateStaticParams với dynamicParams = false, và commit ghi rõ: "All new routes are prerendered (SSG); no request-time rendering."

Với crawler, điều này quan trọng hơn bất kỳ thẻ meta nào. Bot không chạy JavaScript vẫn nhận đủ bài viết, câu trả lời FAQ và JSON-LD ngay trong HTML đầu tiên. Không có render lúc request để chậm, cũng không có fetch phía client để lỗi.

Sơ đồ những gì SEO và GEO của một landing gồm: các trang, JSON-LD, sitemap và llms.txt, với robots.txt cho phép crawler tìm kiếm và AI
Mọi thứ crawler đọc được, ngay trong HTML đầu tiên

Guide trả lời câu hỏi thật

Thay đổi lớn nhất là nội dung. Mỗi landing giờ có 21 guide song ngữ, riêng Stampzy có 24. Chúng được thêm qua ba đợt: đợt đầu đi cùng phần SEO, thêm sáu bài cùng lúc với FAQ và llms.txt, rồi thêm mười hai bài nữa, bài nào cũng có ảnh, kèm các trang so sánh và trang theo nhu cầu sử dụng.

Chủ đề là những gì người ta thật sự tìm, không phải giới thiệu app:

  • Stampzy: cách chụp ảnh có ngày giờ, địa điểm trên iPhone, chấm công bằng hình ảnh, bảng chấm công theo giờ bằng Excel, địa chỉ mới sau sáp nhập tỉnh năm 2025.
  • Talkzy: tốc độ đọc (từ mỗi phút) và độ dài kịch bản, kịch bản livestream bán hàng, dùng iPad hoặc iPhone thứ hai làm máy nhắc chữ.
  • Baton: giấc ngủ của bé sơ sinh, hút và trữ sữa, chia ca đêm, biểu đồ tăng trưởng.

Mỗi guide theo cùng một bộ quy tắc, ghi ngay trong commit message:

  1. Trả lời trước. Mở bài đưa câu trả lời ngắn trước mọi chi tiết, vì đó là đoạn mà AI hay trích.
  2. Heading dạng câu hỏi. Tiêu đề mục dùng đúng cách người ta gõ.
  3. Làm được không cần app. Commit của Talkzy viết rằng mỗi guide "works without the app and ends with what Talkzy actually ships". Guide của Baton có các bước làm bằng giấy bút.
  4. Chỉ nói tính năng đã ship. Phần cuối chỉ nhắc tính năng đã có trên main, không nhắc roadmap.
  5. Chủ đề sức khoẻ có lưu ý. Guide của Baton giữ câu "không phải lời khuyên y tế" và nêu tên nguồn: AAP, AASM, WHO, CDC, Bộ Y tế.
  6. Link nội bộ. Mỗi guide link tới hai, ba guide liên quan và tới trang FAQ.
Sơ đồ cách viết một guide: trả lời trước, heading dạng câu hỏi, làm được không cần app, chỉ nói tính năng đã ship, link liên quan, Article JSON-LD
Sáu quy tắc chung cho mọi guide

Slug cũng được bản địa hoá. Registry guide của Stampzy nối một id cố định với slug tiếng Việt và slug tiếng Anh, ví dụ cham-cong-bang-hinh-anh và photo-attendance-check-in. Nhờ vậy nút đổi ngôn ngữ nhảy được giữa hai bản mà không phải tải nội dung bài vào trình duyệt.

Mỗi guide còn có một ảnh chia sẻ 1200×630 riêng. Ảnh của Stampzy ghép tiêu đề guide với một màn hình thật của app, và cũng là ảnh đầu tiên của guide trong sitemap và JSON-LD.

Ảnh chia sẻ của một guide Stampzy: tiêu đề bên trái, màn hình đóng dấu ảnh có sẵn của app bên phải
Một ảnh guide thật trên stampzy.io.vn

Dữ liệu có cấu trúc sinh từ cùng một dictionary

Trang nào cũng có JSON-LD trong <script type="application/ld+json">, sinh từ chính dictionary mà trang dùng để render, nên hai bên không thể lệch nhau.

  • Trang chủ: một WebSite, app dưới dạng MobileApplication hoặc SoftwareApplication, và FAQ ở trang chủ dưới dạng FAQPage.
  • /faq: một FAQPage chứa mọi câu hỏi trên trang, kèm BreadcrumbList. FAQ của Baton có 18 câu chia năm nhóm, của Talkzy có 16 câu chia bốn nhóm.
  • Guide: một Article (Minivid dùng BlogPosting) có ngày đăng, ngày sửa, tác giả, ảnh, cùng breadcrumb quay về trang danh sách guide.

Graph của guide và FAQ trỏ về app qua @id, nên crawler thấy một thực thể app duy nhất mà site, FAQ và mọi guide đều nói về nó:

ts
{
  "@type": "FAQPage",
  "@id": `${url}#faq`,
  inLanguage: locale,
  about: { "@id": `${SITE_URL}/#app` },
  mainEntity: items.map((i) => ({
    "@type": "Question",
    name: i.q,
    acceptedAnswer: { "@type": "Answer", text: i.a },
  })),
}

Trong file structured data của Stampzy có một comment làm luật: không bao giờ thêm aggregateRating hay review. App chưa có đánh giá thật, và đánh giá bịa chính là thứ site này không chịu hiển thị. Helper của Talkzy còn escape ký tự < khi serialise JSON, để một ký tự lạ trong nội dung không phá được thẻ script.

hreflang, x-default và một bài học từ Search Console

Mỗi trang khai báo URL canonical và các bản vi/en. Trang còn khai báo x-default, và chỗ này có chuyện để kể.

Ngày 22/9, Search Console báo Google đã bỏ canonical mà trang /vi của Lockboxy khai báo, và chọn https://lockboxy.io.vn/ thay vào. URL gốc đó chỉ redirect theo ngôn ngữ trình duyệt, nên trang chủ tiếng Việt hoàn toàn không được index. Cách sửa là khai báo URL không có locale làm x-default, tức điểm vào trung lập của cả bộ ngôn ngữ, thay vì để nó trông như một bản sao cạnh tranh:

ts
languages: {
  vi: `${siteUrl}/vi${path}`,
  en: `${siteUrl}/en${path}`,
  "x-default": `${siteUrl}${path || "/"}`,
},

Giờ cả bảy landing đều khai báo x-default, trang giới thiệu các app cũng vậy. Google có đổi lựa chọn hay không là việc của Google và phải vài tuần mới thấy, nhưng mâu thuẫn thì đã hết.

Về xác minh quyền sở hữu: landing của Lockboxy đọc token xác minh Google và Bing từ biến môi trường, nên thêm token là chỉnh setting trên hosting chứ không cần commit. Comment trong code giải thích vì sao Bing cũng quan trọng: ChatGPT search chạy trên index của Bing. Nói chung, nếu bạn chỉnh được DNS thì Domain property của Search Console, xác minh bằng bản ghi TXT, là lựa chọn gọn hơn. Một property bao cả http, https và mọi subdomain, hợp với site redirect / sang một locale.

Sitemap, robots và llms.txt

Sitemap. sitemap.ts liệt kê mọi trang được index ở cả hai locale, mỗi trang kèm các bản hreflang. Mỗi guide còn khai báo ảnh bìa và ảnh trong bài dưới dạng image entry, kèm ngày sửa thật. Ở Stampzy, hai route trung chuyển có nội dung nằm trong URL fragment được cố tình bỏ ra vì chúng noindex.

Robots. Cho phép tất cả. Ngoài ra còn gọi tên các crawler AI: GPTBot, OAI-SearchBot, ChatGPT-User, PerplexityBot, ClaudeBot, Claude-SearchBot, Google-Extended, Applebot-Extended, CCBot và vài bot khác. Rule * vốn đã cho phép chúng. Gọi tên ra là để điều đó vẫn đúng nếu sau này có ai thêm một mặc định chặt hơn.

ts
rules: [
  { userAgent: "*", allow: "/" },
  { userAgent: AI_CRAWLERS, allow: "/" },
],

llms.txt. Landing nào cũng có /llms.txt, một bản tóm tắt Markdown ngắn cho mô hình ngôn ngữ. Bản của Stampzy gồm app là gì, các thông tin chính, trạng thái phát hành, giá, quyền riêng tư, app không phải là gì, và link tới mọi trang, mọi guide ở cả hai ngôn ngữ. File dài hơn, /llms-full.txt, chứa toàn bộ FAQ và mọi guide dưới dạng Markdown thuần. File này không bao giờ viết tay: một script (hoặc một route force-static ở Minivid và Ringsy) sinh ra nó từ cùng dictionary và dữ liệu guide với các trang. Ở Stampzy, npm run test:llms sẽ fail nếu file đã commit bị cũ.

Phần "không phải là gì" là phần mình để tâm nhất. Của Stampzy ghi rõ mã ảnh là mã kiểm tra tính nhất quán, không phải chứng nhận chống chỉnh sửa, và "Verified time" không phải bằng chứng pháp lý. Nếu AI sẽ mô tả app của mình, mình muốn tự đưa cho nó các giới hạn đó.

IndexNow. Lockboxy còn ping IndexNow sau khi bản deploy đã live, để Bing tải lại trang thay vì chờ tới lượt crawl. Danh sách URL lấy từ sitemap đã deploy. Lệnh này cố tình tách riêng, không chạy trong bước build: ping lúc build sẽ mời Bing đọc lại bản cũ vẫn đang online.

Rút ra

  • Prerender mọi thứ. Cách rẻ nhất để làm SEO và GEO là HTML có sẵn câu trả lời.
  • Viết guide về vấn đề của người dùng, trả lời trước, và làm sao để không có app vẫn dùng được.
  • Sinh JSON-LD và llms.txt từ cùng dữ liệu với trang, và đừng bao giờ đưa vào structured data một rating mình không có.
  • Khai báo x-default khi URL gốc redirect theo ngôn ngữ.
  • Cho phép crawler AI bằng tên, và nói rõ cho chúng biết app của bạn không làm gì.

Nửa còn lại là được tìm thấy trong App Store, mình viết ở bài ASO cho 10 ngôn ngữ không cần tool trả phí.

  • #SEO
  • #GEO
  • #Next.js
  • #Landing Pages
Chia sẻXLinkedInFacebook
Dao Van Thuong

Kỹ sư mobile và fullstack ở TP. Hồ Chí Minh. Mình tự xây và phát hành các app iOS indie — Lockboxy, Linkeeper, Minivid, Ringsy, Talkzy, Baton và Stampzy.