Tối ưu SEO website CSR (React, Angular) bằng kĩ thuật PreCrawl

Gần đây, mình có phải gặp một bài toán khá hay, với nhu cầu là thiết kế một blog với 3 tiêu chí

  • Quản Lý Nội Dung Động: Các bài viết, tag, category cần được động, có thể quản lý (thêm sửa xoá mới) hoàn toàn trên giao diện để cho những người không rành về công nghệ cũng có thể vận hành được
  • Miễn phí: Quan trọng nhất, không được mất phí cho bất kì phần nào của hệ thống, đây là yêu cầu quan trọng nhất, vì cái này sẽ phục vụ cho 1 số tổ chức có kinh phí eo hẹp, hoặc cho cá nhân viết blog theo sở thích, … và không mong muốn chi thêm 1 khoản phí cho website (tất nhiên là không tính tới domain)
  • SEO: Website phải tốt cho SEO

Disclaimer: Đây không phải là một bài toán của blog mình, hiện tại blog của mình đang sử dụng WordPress, và nó tốn chi phí hàng tháng, nhưng cũng đáp ứng đủ các nhu cầu của mình.

Nếu cần Solution tối ưu nhất, hãy đi đến hướng đi cuối

Hướng đi 1: Static Site Generator (SSG)

Hướng tiếp cận đầu tiên của mình là sử dụng 1 opensource sinh ra các trang tĩnh cho blog, sau đó deploy bản được build ra lên một số trang cho host trang tĩnh miễn phí như Firebase Hosting, Github Page, …

Giải pháp này thì đã có rất nhiều open source, bạn có thể tham khảo tại đây, dưới mục Blog: https://github.com/myles/awesome-static-generators#blogs

Nhìn chung luồng này sẽ dạng như sau

  • Người dùng sẽ viết bài ở dạng Markdown, hoặc có thể là viết ở Notion, …
  • Sẽ cần 1 cái gì đó trigger thực hiện build khi viết bài xong, có thể là ci/cd tự động khi push markdown ở github, hoặc ở notion có lắng nghe sự kiện
  • Lúc này hệ thống sẽ build ra bản trang tĩnh, và có thể được deploy lên các trang hosting cho phép deploy trang tĩnh
image - quochung.cyou PTIT
image 1 - quochung.cyou PTIT
  • Ví dụ: Hexo – Viết các file markdown, khi chạy generate sẽ sinh folder build
  • …

Pros

  • Miễn phí
  • SEO tốt
  • Có thể viết động, người dùng vận hành vào viết bài được nếu mà CI-CD đã làm hết -> Tuy nhiên vẫn chưa ở mức cao nhất là có trang quản lý riêng như mong muốn ban đầu lắm

Cons:

  • Đa số các giải pháp đều cần build lại ra trang tĩnh mỗi lần sửa đổi, và khi số lượng bài viết tăng lên thì càng tốn thời gian hơn.
  • Khi cần sửa đổi bài viết, thêm bài viết đều cần build lại, chưa thể vào xem thử tức thì
  • Chưa có giao diện quản lý trực quan

Hướng đi 2: NextJS

Một hướng đi thứ 2 để tối ưu SEO là sử dụng 1 framework server side rendering, có khá nhiều framework như vậy. Điển hình là NextJS.

Ta có thể sử dụng 1 database miễn phí, query bằng api để hỗ trợ trang web động.

Tuy nhiên NextJS nếu muốn build ra các dạng trang tĩnh, để có thể deploy free trên các trang host tĩnh như Github Page, Firebase Hosting, …. thì bắt buộc sẽ cần render toàn bộ trước.

Cụ thể sẽ cần sử dụng  getStaticPaths, và lúc thời điểm build ra bản output, thì sẽ cần query toàn bộ bài viết ở database, và build.

image 2 - quochung.cyou PTIT

Pros

  • Miễn phí
  • SEO tốt
  • Có thể viết động, chỉnh sửa động bằng giao diện do có thể code call api vào database động

Cons:

  • Thời gian build cực kì lâu, vì đa số các hosting free static site sẽ xoá toàn bộ file mỗi lần upload lên, và mỗi lần build thì cần build toàn bộ.

Hướng đi 3 – Solution: Pre-Crawl

Ở cách làm này, ta sẽ sử dụng CSR truyền thống để khi người dùng lướt web, sẽ đọc bài viết, dữ liệu được load động từ API. Và việc tạo bài viết, chuyên mục, … được admin vận hành tạo từ trang quản trị, cũng sẽ thực hiện qua API Database, ở đây sử dụng API Supabase

Mấu chốt là ví dụ như nếu sử dụng React, thì SEO sẽ rất kém, vì React đi theo dạng sử dụng đa số là JS và để JS build ra HTML. Nên các bot crawl của Google, Bing, … khi vào trang web nếu không chạy được js sẽ không load ra dữ liệu gì cả

Ví dụ hình ảnh một trang web sử dụng CSR sẽ trông như nào với Bot qua http://botsimulator.com/:

  • Kết quả nhận được, bot không lấy được dữ liệu gì cả, chỉ có tên trang web, keyword, desc, … đều rỗng
image 3 - quochung.cyou PTIT
image 5 - quochung.cyou PTIT

Phương án đề ra là ta sẽ sử dụng một luồng nghiệp vụ khác, riêng biệt, chuyên thực hiện việc đi vào các trang web như người dùng mở browser vào web vậy, sau đó cache lại các dữ liệu html vào 1 trang lưu tĩnh, lúc này khi Bot tới, ta sẽ có 1 tầng proxy kiểm tra xem có phải là Bot không, và redirect Bot sang trang tĩnh html đã được xử lý xong, còn người dùng thì đi luồng thông thường.

Có thể nhìn biểu đồ sau với các công nghệ sử dụng:

  • Proxy bên trên xử lý redirect: Cloudflare Worker Route + Cloudflare Worker
  • Lưu trữ HTML: Supabase Storage
  • Trang tĩnh: lưu trữ tại Cloudflare Page, dev bằng React
  • Thực hiện crawl trang web: Github Action
  • Database + API động bài viết: Supabase Database
image 7 - quochung.cyou PTIT

Kết quả nhận được:

  • Bot sẽ crawl được các dữ liệu trong bài viết (Trong ảnh ở phần Indexable Page Text)
image 4 - quochung.cyou PTIT
image 6 - quochung.cyou PTIT

Pros

  • Miễn phí
  • SEO tốt
  • Có thể viết động, chỉnh sửa động bằng giao diện do có thể code call api vào database động
  • Build nhanh, do trang build ra là trang csr chỉ chứa code call api cho user <- Xử lý được hướng đi 1,2

Cons:

  • Sẽ cần flow crawl dữ liệu, cái này thì có thể chạy bằng manual cũng được, mình sử dụng 1 workflow trên Github Action để crawl, Github Action sẽ free 2200 phút / tháng (khoảng chạy được 1 tiếng /ngày, vì vậy sẽ cần triển khai logic dạng kiểm tra có nên crawl lại 1 trang không bằng thời gian cập nhật, … tuỳ vào nghiệp vụ)

Code triển khai + chi tiết setup với techstack bên trên

https://github.com/quochung-cyou/csr-seo-page-crawl

[Tìm hiểu React – Phần 1] Tại sao React ra đời

image 32 - quochung.cyou PTIT

Tại sao lại cần React? Viết bằng Javascript thuần không ổn sao?

Mọi thứ xuất hiện đều có lí do của nó. Thời kỳ đầu tiên của lập trình web, đa số chúng ta chỉ có các web tĩnh, có thể là các trang đọc báo, trang tài liệu wiki, …. Các dữ liệu trên trang thường không thay đổi. Điều này kéo dài một thời gian, tuy nhiên chúng ta ngày càng mong muốn nhiều hơn, chúng ta muốn những website “động”, những website mạng xã hội mà bài viết được cập nhật mỗi lần tải trang, những trang web mà có thể nhận/gửi tin nhắn mới, những trang web có các video mới như Youtube, …

Trước khi React sinh ra

  • Hãy tưởng tượng một bài toán thực tế. Ta có một cái nút “đăng nhập”, khi người dùng nhấn nút, yêu cầu của bài toán cần có 4 trạng thái như sau:
    • Nút trước khi bị ấn hiển thị màu xanh dương, ghi là “Nhấn để đăng nhập”
    • Nút khi bị ấn hiển thị màu xanh dương nhưng đen đi một xíu do đang bị ấn, hiển thị chữ “đang đăng nhập”
    • Sau khi đăng nhập hoàn thành, nút chuyển sang màu xanh lá, hiển thị “đăng nhập thành công”
    • Sau khi đăng nhập thất bại, nút chuyển sang màu đỏ, hiển thị “đăng nhập thất bại”
image 33 - quochung.cyou PTIT
  • Chúng ta tưởng tượng, code xử lí đăng nhập sẽ trong file javascript, còn cái nút thì ở file html. Như vậy, để cập nhật trạng thái màu, chữ của nút, ta thường phải làm các bước sau:
    • Lấy DOM của nút, tức là trong file xử lí code cần cho ngôn ngữ lập trình biết là ta muốn thay đổi cái nút nào. thường ta sẽ sử dụng các cú pháp như document.getElementById (Lấy một dom bằng định danh ID), hoặc document.querySelector (Lấy bằng query), ….
    • Thêm một event listener là khi nhấn nút sẽ chạy hàm nào đó, chạy logic gì đó cho nút
    • Viết code xử lí hàm thay đổi nút
    • Xoá listener, reset mọi thứ về ban đầu khi tải lại trang -> nút về trạng thái chưa ấn

Oke, hãy thử làm bài toán này, đầu tiên hãy tạo nút chúng ta cần ở HTML

<button type="button" id="dangnhapbutton">Đăng nhập</button>

Tiếp theo trong Javascript, ta tìm nút trên để thêm hàm xử lí khi nhấn cho nó

const dangnhapbutton = document.getElementById("dangnhapbutton"); //lấy nút theo id
dangnhapbutton .addEventListener("click", () => { //thêm event khi ấn 
  // xử lí khi ấn ở đây
});

Oke, giờ giả sử, khi ấn nút, thì nó cần hiện là “Đang đăng nhập”, ta sửa code dạng như sau

const dangnhapbutton = document.getElementById("dangnhapbutton"); //lấy nút theo id
dangnhapbutton .addEventListener("click", () => { //thêm event khi ấn 
  dangnhapbutton.textContent = "Đang đăng nhập"; //hiển thị đang đăng nhập
});
  • Giờ thì khi ấn nút, nút từ “đăng nhập” sẽ được chuyển thành “đang đăng nhập”. Tuy nhiên chúng ta cần nó hiển thị đăng nhập thành công, hoặc đăng nhập thất bại, hoặc sau 1 lúc thì nó lại chuyển về “đăng nhập”. Giờ làm sao nhỉ? Ta cần một cách nào đó để biết là nút đang ở trạng thái nào phía hiển thị, sau đó tuỳ nó đang ở trạng thái gì thì sẽ chuyển sang trạng thái mới.
  • Ví dụ ta thử làm Khi ấn 1 lần, chuyển đăng nhập -> đang đăng nhập. ấn tiếp thì chuyển lại về đăng nhập
<button type="button" id="dangnhapbutton" data-dangnhap="false">Đăng nhập</button> //ta cần một attribute data-dangnhap để lưu trạng thái phía hiển thị
const dangnhapbutton = document.getElementById("dangnhapbutton"); //lấy nút theo id
dangnhapbutton .addEventListener("click", () => { //thêm event khi ấn 
  const trangthai = dangnhapbutton .getAttribute("data-dangnhap") === "true"; //lấy trạng thái phía frontend xem là đăngn hập chưa
  dangnhapbutton .setAttribute("data-dangnhap", !trangthai ); //đảo ngược trạng thái
  dangnhapbutton .textContent = trangthai  ? "Đang đăng nhập" : "Đăng nhập"; //tuỳ trạng thái là 0 hay 1 thì cập nhật trạng thái mới 
});
  • Vấn đề cơ bản như 1 nút chỉ tắt bật như vậy. Nhưng ta phải lưu cùng lúc 1 attribute thông số gì đó ở phía html, rồi lại lưu 1 biến ở phía code để xử lí. Giả sử ta có 4 trạng thái, 10 trạng thái gì đó, ta sẽ phải lưu 10 cái ở phía html, rồi lấy ra 10 cái ở javascript rồi if else logic xử lí…
  • Điều này rõ ràng rất khó scale mở rộng hệ thống hơn. Sẽ ra sao nếu web của ta có rất nhiều nút? Ta sẽ có 1 đống code thừa trùng lặp, ta phải code rất nhiều cho một tác vụ tưởng chừng đơn giản, …
  • Liệu có cách nào để chúng liên kết với nhau, để ta có thể xử lí qua lại dễ dàng hơn không nhỉ?

Thời điểm này, việc tạo ra một cách thuận tiện, dễ mở rộng cho frontend là một thử thách với các công ty. Đây là tiền đề cho sự ra đời cho một số cái tên nổi tiếng, mà chắc bạn đã từng nghe qua như: Backbone, KnockoutJS, AngularJS, and jQuery.

Hãy cùng xem những giải pháp trên có gì khác với React, các cách tiếp cận khác nhau của từng giải pháp

Đọc tiếp phần 2: