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:

[Web] CyouTodo

CyouTodo

Demo: https://todo.quochung.cyou

Source: https://github.com/quochung-cyou/CyouTodo

Author

  • quochungcyou

Project Structure

├── index.html - Trang hiển thị task
├── README.md
├── login.html - Trang đăng nhập đawng kí
├── dashboard.html - Trang dashboard
├── LICENSE - Giấy phép sử dụng
├── asset
│   ├── css
│   │   ├── index.css - CSS chung cho index/dashboard
│   │   ├── login.css - CSS cho login
│   │   ├── lightdarkmode.css - CSS cho phần bật sáng tối
│   │   ├── preload.css - CSS cho phần loader
|   ├── js
│   │   ├── index.js - JS chung cho index
│   │   ├── login.js - JS cho login
│   │   ├── lightdarkmode.js - JS cho phần bật sáng tối
│   │   ├── dashboard.js - JS cho phần dashboard
|   ├── img Lưu ảnh của project

Sequence Diagram

diagram - quochung.cyou PTIT

Description

  • Web bài tập lớn Team Web ProPTIT D21
  • Đề tài: CyouTodo – Ứng dụng quản lý công việc cá nhân
  • Phần chính là FrontEnd

Demo image

cyoutodo1 - quochung.cyou PTIT
cyoutodo2 - quochung.cyou PTIT
cyoutodo3 - quochung.cyou PTIT

Feature

  • [x] Đăng nhập/Đăng ký
  • [x] Thêm/Xoá/Sửa công việc
  • [x] Light/Dark mode
  • [x] Đếm ngược thời gian công việc
  • [x] Hiển thị thời gian còn lại, mức độ quan trọng
  • [x] Thống kê công việc, lịch
  • [ ] Cài đặt tài khoản

Prerequisites and Setup

  • JavaScript
  • JQuery
  • HTML/CSS (Pure CSS)
  • Firebase for hosting/realtime database
  • ApexChart (JS) – Just for demo

License Information

  • This software is licensed under MIT License.
  • Read More on the license.

Giúp đỡ tôi bằng cách chia sẻ hoặc tặng sao

stars - quochung.cyou PTIT

Thông tin liên hệ

Nếu có bất kỳ nội dung nào liên quan còn thiếu,hãy tạo issues cho nó, tôi sẽ theo dõi và cải thiện。

MIT License

Copyright (c) 2023 QuocHung

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

QLDT PTIT Enhancer – Tiện ích xếp TKB, bổ sung chức năng cho QLDT PTIT 2023

QLDT Enhancer

Bạn cũng có thể thử bản beta với nhiều chức năng hơn và nhiều lỗi hơn :v : https://github.com/quochung-cyou/QLDT_Enhancer/tree/beta

Author

Description

  • Script hỗ trợ việc xếp lịch TKB, đồng thời chỉnh lại giao diện QLDT một chút theo ý mình. Script được hoàn thành gấp rút trong nửa ngày vì yêu cầu của bạn bè mình, mình cũng chỉ mới học JavaScript nên nếu có lỗi hay vấn đề hi vọng các bạn có thể liên hệ mình qua [email protected]
  • Sử dụng script theo nhu cầu của bạn, mình không chịu trách nhiệm trước các vấn đề liên quan đến việc sử dụng script này.

Feature

  •  Tool xem xếp lịch TKB theo QLDT
  •  Chọn môn bằng dropdown nhanh chóng, đỡ phải tìm kiếm hay lướt như QLDT cũ
  •  Tự động cập nhật lịch thay đổi
  •  Chỉnh sửa giao diện thêm một chút
  •  Tự động giữ kết nối, tránh bị out session
  •  Danh sách môn đã đăng kí
  •  Tránh đăng kí trùng 1 môn

Prerequisites and Setup

Video hướng dẫn: https://www.youtube.com/watch?v=UYY8gMDWGD8

Technologies Used

  • JavaScript
  • JQuery

Demo image

demo1 - quochung.cyou PTIT
demo2 - quochung.cyou PTIT
demo3 - quochung.cyou PTIT
demo4 - quochung.cyou PTIT

License Information

  • This software is licensed under MIT License.
  • Read More on the license.

Giúp đỡ tôi bằng cách chia sẻ hoặc tặng sao

stars - quochung.cyou PTIT

Thông tin liên hệ

Nếu có bất kỳ nội dung nào liên quan còn thiếu,hãy tạo issues cho nó, tôi sẽ theo dõi và cải thiện。

MIT License

Copyright (c) 2023 QuocHung

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Trigger một form input để giả lập như người dùng gõ bằng JavaScript thuần

Khởi tạo các biến const cho Event

const EVENT_OPTIONS = {bubbles: true, cancelable: false, composed: true};
const EVENTS = {
    BLUR: new Event("blur", EVENT_OPTIONS),
    CHANGE: new Event("change", EVENT_OPTIONS),
    INPUT: new Event("input", EVENT_OPTIONS),
};

Thay đổi giá trị của form input mình cần sửa đổi và cập nhật value cho nó

    findInputBox = document.querySelector(".table-sm > thead > tr:nth-child(2) > td:nth-child(2) > input");
    findInputBox.focus();
    findInputBox.value = name; //name là giá trị cần set
    const tracker = findInputBox._valueTracker;
    tracker && tracker.setValue(name);
    findInputBox.dispatchEvent(EVENTS.INPUT);
    findInputBox.dispatchEvent(EVENTS.BLUR);