Trong quá trình xây dựng và phát triển một website trên nền tảng Blogger, việc tối ưu hóa trải nghiệm người dùng (User Experience - UX) và hỗ trợ công cụ tìm kiếm là yếu tố then chốt. Một trong những thành phần quan trọng thường bị bỏ qua chính là trang Sitemap HTML. Đây không chỉ là một mục lục thông thường mà còn là công cụ điều hướng mạnh mẽ, thể hiện sự chuyên nghiệp của trang web.

Bài viết này sẽ cung cấp một giải pháp toàn diện: bộ code sitemap động cùng hướng dẫn chi tiết để bạn có thể tự mình tạo sitemap cho Blogger với giao diện hiện đại, đầy đủ tính năng và chuẩn SEO.

tạo sitemap cho Blogger
Hướng dẫn cách tạo sơ đồ trang web cho Blogspot
{getToc} $title={Table of Contents}

Sitemap HTML là gì và tại sao nó quan trọng?

Trước hết, cần phân biệt rõ giữa Sitemap XML và Sitemap HTML.

  • Sitemap XML: Là một tệp tin dành riêng cho các công cụ tìm kiếm (Googlebot, Bingbot...). Nó liệt kê tất cả các URL trên trang của bạn để giúp bot thu thập dữ liệu (crawl) và lập chỉ mục (index) một cách hiệu quả hơn. Tệp này thường không hiển thị cho người dùng thông thường.
  • Sitemap HTML: Là một trang hiển thị trực quan trên website của bạn, liệt kê cấu trúc và toàn bộ nội dung (bài viết, trang tĩnh) một cách có tổ chức. Trang này phục vụ trực tiếp cho người dùng.

Việc đầu tư vào một trang Sitemap HTML cho Blogger chuyên nghiệp mang lại nhiều lợi ích thiết thực:

  • Cải thiện trải nghiệm người dùng (UX): Giúp độc giả dễ dàng tìm thấy nội dung họ quan tâm, đặc biệt với các blog có số lượng bài viết lớn. Thay vì phải tìm kiếm thủ công, họ có một "bản đồ" tổng thể về website của bạn.
  • Tăng cường liên kết nội bộ (Internal Linking): Một trang sitemap tập trung liên kết đến tất cả các bài viết và trang quan trọng, giúp phân phối dòng chảy "link juice" và tăng cường sự liên kết chặt chẽ trong cấu trúc website, một yếu tố được Google đánh giá cao.
  • Hỗ trợ gián tiếp cho SEO: Khi người dùng dễ dàng tìm thấy thông tin, họ có xu hướng ở lại trang lâu hơn và giảm tỷ lệ thoát (bounce rate). Đây là những tín hiệu tích cực mà các công cụ tìm kiếm sử dụng để đánh giá chất lượng trang web.
  • Thể hiện sự chuyên nghiệp: Một trang sitemap được thiết kế tốt cho thấy sự đầu tư nghiêm túc vào website, tạo dựng niềm tin và uy tín với độc giả.

Giới thiệu Code Sitemap: Các tính năng nổi bật

Bộ code được chia sẻ trong bài viết này được xây dựng bằng Javascript thuần (Vanilla JS), đảm bảo hiệu suất tối ưu và tích hợp các tính năng cao cấp:

  • Giao diện Responsive: Tự động tối ưu hiển thị trên mọi kích thước màn hình, từ máy tính để bàn đến máy tính bảng và điện thoại di động.
  • Tải dữ liệu bất đồng bộ: Tải toàn bộ danh sách bài viết và trang mà không cần tải lại trang, mang đến trải nghiệm mượt mà.
  • Tìm kiếm & Lọc dữ liệu động: Cho phép người dùng tìm kiếm bài viết theo tiêu đề, lọc theo nhãn (danh mục) và sắp xếp theo ngày đăng hoặc ngày cập nhật.
  • Phân trang thông minh: Tự động chia nhỏ danh sách bài viết thành nhiều trang để cải thiện tốc độ tải và sự gọn gàng.
  • Cài đặt đơn giản: Không yêu cầu chỉnh sửa theme phức tạp, chỉ cần thực hiện qua giao diện tạo trang của Blogger.

Hướng dẫn cài đặt chi tiết

Vui lòng thực hiện tuần tự theo các bước dưới đây để đảm bảo sitemap hoạt động chính xác.

Bước 1: Tích hợp thư viện Bootstrap Icons (Nếu cần)

Code sitemap sử dụng icon từ thư viện Bootstrap để giao diện thêm phần trực quan. Nếu theme của bạn chưa tích hợp, hãy thêm đoạn mã sau vào trước thẻ đóng </head>.

  1. Vào trang quản trị Blogger, chọn Chủ đề (Theme).
  2. Nhấn vào nút Tùy chỉnh (Customize) -> Chỉnh sửa HTML (Edit HTML).
  3. Tìm đến thẻ </head> (thường ở gần cuối) và dán đoạn code sau vào ngay phía trên nó.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"/>
Nếu theme của bạn đã có sẵn thư viện Bootstrap Icons hoặc Font Awesome, bạn có thể bỏ qua bước này.{alertInfo}

Bước 2: Tạo một Trang tĩnh mới

  1. Trong menu quản trị, điều hướng đến mục Trang (Pages).
  2. Nhấn vào nút Trang mới (New Page).
  3. Nhập tiêu đề cho trang, ví dụ: Sitemap, Sơ đồ trang web, hoặc Mục lục.

Bước 3: Thêm Code vào Trang

Đây là bước quan trọng nhất trong việc tạo sitemap cho Blogger.

  1. Trong giao diện soạn thảo trang, chuyển từ chế độ Soạn thư (Compose view) sang chế độ xem HTML (HTML view) bằng cách nhấp vào biểu tượng </>.
  2. Xóa toàn bộ nội dung mặc định (nếu có).
  3. Sao chép và dán toàn bộ đoạn code sitemap cho blogspot bạn đã chuẩn bị vào khung soạn thảo.
<style>
#sitemap{max-width:1100px;margin:2rem auto}
#sitemap .card{
  background:var(--widget-bg);border:1px solid var(--border-color);
  border-radius:var(--rounded);box-shadow:var(--shadow)
}
#sitemap .card-body{padding:1.5rem}

#sitemap .hstack{display:flex;flex-wrap:wrap;gap:.75rem;margin-bottom:.5rem}
#sitemap .hstack .field{flex:1 1 220px;min-width:220px}
#sitemap .label{display:block;font-weight:600;color:var(--title-color);margin-bottom:.35rem}
#sitemap .input,#sitemap .select{
  display:block;width:100%;height:2.75rem;border-radius:var(--rounded);
  background:var(--dynamic-bg);color:var(--title-color);
  border:1px solid var(--border-color);padding:0 .875rem;font:inherit
}
#sitemap .input::placeholder{color:var(--meta-color)}
#sitemap .input:focus,#sitemap .select:focus{box-shadow:0 0 0 2px var(--input-shadow)}

#sitemap .list{list-style:none;margin:0;padding:0;border-top:0}
#sitemap .item{
  --ih:0;
  display:flex;gap:1rem;align-items:flex-start;
  padding:1rem 0;border-bottom:1px solid var(--border-color)
}

#sitemap .idx{
  width:2.25rem;height:2.25rem;flex:0 0 auto;display:grid;place-items:center;
  font-weight:700;border-radius:999px;background:var(--widget-bg);
  color:var(--title-color);border:1px solid var(--border-color);
  background: hsl(var(--ih) 80% 95% / .85);
  border-color: hsl(var(--ih) 60% 70% / .70);
  color: hsl(var(--ih) 35% 28%);
}

#sitemap .content{min-width:0;flex:1 1 auto}
#sitemap .title{
  color:var(--title-color);font-weight:700;text-decoration:none;line-height:1.4;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
  letter-spacing:-.005em;
}
#sitemap .title:hover{color:var(--title-hover-color)}
#sitemap .meta{margin-top:.35rem;font-size:.875rem;color:var(--meta-color);display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}
#sitemap .dot{opacity:.6}

#sitemap .badge{
  display:inline-flex;align-items:center;gap:.35rem;
  padding:.25rem .55rem;border-radius:999px;font-size:.75rem;font-weight:600;
  border:1px solid var(--border-color);background:var(--gray-bg);color:var(--title-color)
}
#sitemap .badge[style*="--h"]{
  background:hsl(var(--h) 80% 95% / .85); border-color:hsl(var(--h) 60% 70% / .7);
  color:hsl(var(--h) 35% 28%)
}
#sitemap .badge i{font-size:.9em}

#sitemap .empty{
  margin:1rem 0;padding:1rem;border:1px dashed var(--border-color);
  border-radius:var(--rounded);text-align:center;color:var(--meta-color)
}

#sitemap .footer{display:flex;align-items:center;justify-content:space-between;margin-top:1rem}
#sitemap .btn{
  display:inline-flex;align-items:center;gap:.5rem;height:2.5rem;padding:0 1rem;
  border-radius:var(--rounded);border:1px solid var(--btn-bg);
  background:var(--btn-bg);color:var(--btn-color);font-weight:600
}
#sitemap .btn.secondary{background:transparent;border-color:var(--border-color);color:var(--title-color)}
#sitemap .btn[disabled]{opacity:.5;cursor:not-allowed}
#sitemap .btn:focus{box-shadow:0 0 0 2px var(--btn-shadow)}

@media (hover:hover){
  #sitemap .item:hover{background:color-mix(in srgb, var(--accent-color), transparent 96%)}
  #sitemap .item:hover .title{color:var(--title-hover-color)}
  #sitemap .item:hover .idx{filter:saturate(1.1);transform:translateY(-1px);transition:transform .15s ease}
}

#sitemap ol#sm-list{list-style:none;padding-left:0}
#sitemap ol#sm-list>li::before{content:none!important}
#sitemap ol#sm-list>li::marker{content:""}

#sitemap .item.loading{justify-content:center;padding:1.25rem 0;border-bottom:0}
#sitemap .item.loading .content{margin-left:0;text-align:center;color:var(--meta-color)}

@media (min-width:1024px){
  #sitemap .hstack{display:grid;grid-template-columns:1fr 1fr 2fr;align-items:end}
}

@media (max-width:900px){
  #sitemap .hstack{display:grid;grid-template-columns:1fr 1fr;align-items:end}
  #sitemap .hstack .field:last-child{grid-column:1 / -1}
}

@media (max-width:680px){
  #sitemap .card-body{padding:1rem}
  #sitemap .hstack{display:grid;grid-template-columns:1fr;gap:.6rem;margin-bottom:.25rem}
  #sitemap .input,#sitemap .select{height:2.5rem}
  #sitemap .item{gap:.75rem;padding:.85rem 0}
  #sitemap .idx{width:2rem;height:2rem;font-size:.95rem}
  #sitemap .meta{font-size:.82rem}
  #sitemap .badge{font-size:.72rem;padding:.2rem .5rem}
  #sitemap .footer{gap:.75rem}
  #sitemap .btn{height:2.4rem;padding:0 .9rem}
}

@media (max-width:480px){
  #sitemap .footer{display:grid;grid-template-columns:1fr 1fr;align-items:center}
  #sitemap #sm-prev{justify-self:start;width:max-content}
  #sitemap #sm-next{justify-self:end;width:max-content}
}
  
@media (max-width:360px){
  #sitemap .title{-webkit-line-clamp:3}
  #sitemap .badge{font-size:.7rem;padding:.18rem .45rem}
}
</style>

<div id="sitemap" data-blog="" data-post-max="4" data-page-max="2" data-page-size="10">
  <div class="card">
    <div class="card-body">
      <!-- Filters -->
      <div class="hstack">
        <div class="field">
          <label class="label"><i class="bi bi-folder2-open"></i> Danh mục</label>
          <select id="sm-cat" class="select"></select>
        </div>
        <div class="field">
          <label class="label"><i class="bi bi-arrow-down-up"></i> Sắp xếp</label>
          <select id="sm-sort" class="select">
            <option value="published">Ngày đăng</option>
            <option value="updated">Ngày cập nhật</option>
          </select>
        </div>
        <div class="field" style="flex:2 1 260px;min-width:260px">
          <label class="label"><i class="bi bi-search"></i> Tìm kiếm</label>
          <input id="sm-search" type="search" class="input" placeholder="Tìm tiêu đề…">
        </div>
      </div>

      <!-- List -->
      <div id="sm-empty" class="empty" style="display:none">
        <i class="bi bi-emoji-frown"></i> Không có nội dung phù hợp
      </div>
      <ol id="sm-list" class="list"></ol>

      <!-- Footer -->
      <div class="footer">
        <button id="sm-prev" class="btn secondary"><i class="bi bi-chevron-left"></i>Trước</button>
        <!-- ĐÃ BỎ sm-info -->
        <button id="sm-next" class="btn">Sau<i class="bi bi-chevron-right"></i></button>
      </div>
    </div>
  </div>
</div>

<script>
(function(){
  const wrap   = document.getElementById('sitemap');
  const BLOG   = wrap.getAttribute('data-blog') || location.origin;
  const POST_MAX_REQ = +(wrap.getAttribute('data-post-max')||4);
  const PAGE_MAX_REQ = +(wrap.getAttribute('data-page-max')||2);
  const PAGE_SIZE_DEF= +(wrap.getAttribute('data-page-size')||10);

  const VN = {all:'Tất cả bài viết', unlabeled:'Không nhãn', pages:'Trang tĩnh',
              loading:'Đang tải Sitemap…', failed:'Không tải được Sitemap.'};

  const state = { groups:{}, key:'all', sort:'published', q:'', page:1, size:PAGE_SIZE_DEF };

  const catSel  = document.getElementById('sm-cat');
  const sortSel = document.getElementById('sm-sort');
  const searchI = document.getElementById('sm-search');
  const listEl  = document.getElementById('sm-list');
  const emptyEl = document.getElementById('sm-empty');
  const prevBtn = document.getElementById('sm-prev');
  const nextBtn = document.getElementById('sm-next');

  const esc = s=>String(s).replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
  const pad2=n=>String(n).padStart(2,'0');
  const debounce=(fn,t=180)=>{let id;return(...a)=>{clearTimeout(id);id=setTimeout(()=>fn(...a),t)}};

  function hashHue(str){let h=0;for(let i=0;i<str.length;i++)h=(h*31+str.charCodeAt(i))%360;return h}
  function labelIcon(lb){
    const k=lb.toLowerCase();
    if (/(javascript|js)/.test(k)) return 'bi-filetype-js';
    if (/html/.test(k) && /css/.test(k)) return 'bi-filetype-html';
    if (/css/.test(k)) return 'bi-filetype-css';
    if (/java\b/.test(k)) return 'bi-filetype-java';
    if (/python/.test(k)) return 'bi-filetype-py';
    if (/blogger|template|thủ thuật/.test(k)) return 'bi-journal-code';
    if (/lab|bài tập|exercise/.test(k)) return 'bi-clipboard-check';
    if (/tool|extractor|parser|generator/.test(k)) return 'bi-wrench-adjustable';
    if (/download|tài liệu|file/.test(k)) return 'bi-cloud-arrow-down';
    return 'bi-tag';
  }

  async function fetchList(kind='posts', start=1, max=150){
    const url = `${BLOG}/feeds/${kind}/summary?alt=json&max-results=${max}&start-index=${start}`;
    const r = await fetch(url, {credentials:'omit'}); if(!r.ok) throw new Error('feed:'+r.status);
    const j = await r.json(), feed=j.feed||{};
    const arr = (feed.entry||[]).map(e=>({
      title:(e.title?.$t||'').trim(),
      url: (e.link?.find(x=>x.rel==='alternate')?.href)||'#',
      published:new Date(e.published?.$t || e.updated?.$t),
      updated:new Date(e.updated?.$t || e.published?.$t),
      labels:(e.category||[]).map(c=>c.term).filter(Boolean)
    }));
    const total=+(feed.openSearch$totalResults?.$t||arr.length);
    return {entries:arr, hasMore:start+max<=total, nextStart:start+max};
  }
  async function getTotal(kind='posts'){
  const url = `${BLOG}/feeds/${kind}/summary?alt=json&max-results=1&start-index=1`;
  const r = await fetch(url,{credentials:'omit'});
  if(!r.ok) throw new Error('feed:'+r.status);
  const j = await r.json();
  return +(j?.feed?.openSearch$totalResults?.$t || 0);
}

async function fetchAll(kind){
  const total = await getTotal(kind);
  if (!total) return [];
  const chunk = 150;
  // trần an toàn để tránh loop vô hạn (ví dụ 50*150 = 7500)
  const maxLoops = Math.min(Math.ceil(total / chunk), 50);

  const all = [];
  let start = 1;
  for (let i = 0; i < maxLoops; i++){
    const {entries, hasMore, nextStart} = await fetchList(kind, start, chunk);
    all.push(...entries);
    if (!hasMore) break;
    start = nextStart;
  }
  return all;
}
  function buildGroups(posts,pages){
    const groups={all:[VN.all,posts]};
    const map=Object.create(null), unlabeled=[];
    posts.forEach(p=>{
      if(p.labels.length){ p.labels.forEach(lb=> (map[lb]??=[]).push(p) ); }
      else unlabeled.push(p);
    });
    Object.keys(map).sort((a,b)=>a.localeCompare(b)).forEach(lb=> groups['label_'+lb]=[lb,map[lb]]);
    if(unlabeled.length) groups.unlabeled=[VN.unlabeled,unlabeled];
    groups.pages=[VN.pages,pages];
    return groups;
  }
  function fillCats(){
    catSel.innerHTML = Object.entries(state.groups).map(([k,[title]])=>
      `<option value="${k}">${esc(title)}</option>`).join('');
  }

  function render(){
    let [,itemsRaw]=state.groups[state.key]||[VN.all,[]];
    let items = itemsRaw.slice();

    const q=state.q.trim().toLowerCase();
    if(q) items = items.filter(it=>it.title.toLowerCase().includes(q));

    const fld=(state.sort==='updated'?'updated':'published');
    items.sort((a,b)=>b[fld]-a[fld]);

    const total=items.length;
    const pages=Math.max(1,Math.ceil(total/state.size));
    if(state.page>pages) state.page=pages;
    const from=(state.page-1)*state.size;
    const slice=items.slice(from,from+state.size);

    if(!slice.length){
      listEl.innerHTML=''; emptyEl.style.display='';
    }else{
      emptyEl.style.display='none';
      listEl.innerHTML=slice.map((it,i)=>{
        const d=it.published, date=`${pad2(d.getDate())}/${pad2(d.getMonth()+1)}/${d.getFullYear()}`;
        const idx=from+i+1;
        const hue=(idx*29)%360;

        const tags = it.labels.map(lb=>{
          const h=hashHue(lb), ic=labelIcon(lb);
          return `<span class="badge" style="--h:${h}"><i class="bi ${ic}"></i>${esc(lb)}</span>`;
        }).join('');

        return `<li class="item" style="--ih:${hue}">
          <div class="idx">${idx}</div>
          <div class="content">
            <a class="title" href="${it.url}" target="_blank" rel="noopener">${esc(it.title)}</a>
            <div class="meta">
              <span><i class="bi bi-calendar-event"></i> ${date}</span>
              ${tags?`<span class="dot">•</span> ${tags}`:''}
            </div>
          </div>
        </li>`;
      }).join('');
    }

    prevBtn.disabled = state.page<=1;
    nextBtn.disabled = state.page>=pages;
  }

  (async function init(){
    listEl.innerHTML = `<li class="item loading"><div class="content" style="margin-left:0">
      <i class="bi bi-arrow-repeat"></i> ${VN.loading}</div></li>`;
    try{
      const [posts,pages]=await Promise.all([fetchAll('posts',POST_MAX_REQ), fetchAll('pages',PAGE_MAX_REQ)]);
      state.groups = buildGroups(posts,pages);
      fillCats(); render();
    }catch(e){
      console.warn(e);
      listEl.innerHTML = `<li class="item"><div class="content" style="margin-left:0;color:#dc2626">
        <i class="bi bi-exclamation-triangle"></i> ${VN.failed}</div></li>`;
    }
  })();

  catSel.addEventListener('change', ()=>{ state.key=catSel.value; state.page=1; render(); });
  sortSel.addEventListener('change', ()=>{ state.sort=sortSel.value; state.page=1; render(); });
  searchI.addEventListener('input', debounce(()=>{ state.q=searchI.value||''; state.page=1; render(); },200));
  prevBtn.addEventListener('click', ()=>{ if(state.page>1){ state.page--; render(); }});
  nextBtn.addEventListener('click', ()=>{ state.page++; render(); });

})();
</script>
  1. Nhấn nút Xuất bản (Publish) để hoàn tất. Bây giờ bạn có thể truy cập vào đường dẫn của trang vừa tạo để xem kết quả.

Tùy chỉnh và Cấu hình

Bạn có thể dễ dàng điều chỉnh cả chức năng lẫn giao diện của sitemap để phù hợp nhất với blog của mình.

Cấu hình chức năng

Để điều chỉnh cách sitemap hoạt động, bạn chỉ cần sửa các thuộc tính data-* trong đoạn mã HTML. Tìm đến dòng code sau:

<div id="sitemap" data-blog="" data-post-max="4" data-page-max="2" data-page-size="10">
  • data-post-max="4": Số lượng yêu cầu (request) tối đa để tải danh sách bài viết. Mỗi yêu cầu sẽ tải 150 bài. Ví dụ, 4 tương đương với việc tải tối đa 4 * 150 = 600 bài viết. Hãy tăng giá trị này nếu blog của bạn có số lượng bài viết lớn hơn.
  • data-page-max="2": Tương tự như trên, nhưng áp dụng cho các trang tĩnh (pages).
  • data-page-size="10": Số lượng bài viết được hiển thị trên mỗi trang của sitemap. Bạn có thể thay đổi thành 15, 20, hoặc bất kỳ giá trị nào phù hợp.

Tùy chỉnh Giao diện (CSS)

Phần CSS của sitemap được viết bằng cách sử dụng Biến CSS (CSS Variables) để bạn có thể dễ dàng thay đổi màu sắc cho phù hợp với theme hiện tại của blog mà không cần phải chỉnh sửa nhiều dòng code phức tạp.

Để tùy chỉnh, bạn chỉ cần tìm đến các biến có dạng var(--ten-bien) trong đoạn code <style>. Người dùng có thể thay đổi các giá trị này trực tiếp hoặc định nghĩa lại chúng trong CSS của theme.

Một vài biến màu sắc quan trọng bạn có thể quan tâm:

  • --widget-bg: Màu nền của khung sitemap.
  • --title-color: Màu chữ của tiêu đề bài viết.
  • --title-hover-color: Màu chữ của tiêu đề khi di chuột qua.
  • --border-color: Màu của các đường viền.
  • --shadow: Hiệu ứng đổ bóng của khung.

Bằng cách này, bạn có thể nhanh chóng "khoác" cho sitemap một chiếc áo mới hoàn toàn tiệp với màu sắc chủ đạo trên blog của mình.

Lời kết

Một trang sitemap không chỉ là một tiện ích, mà còn là một khoản đầu tư chiến lược cho sự phát triển lâu dài của blog. Đây là một thủ thuật SEO Blogger quan trọng giúp cải thiện điều hướng website. Bằng việc cung cấp một công cụ điều hướng mạnh mẽ và chuyên nghiệp, bạn không chỉ giữ chân độc giả hiệu quả hơn mà còn tạo ra những tín hiệu tích cực cho các thuật toán tìm kiếm.

Hy vọng rằng với hướng dẫn tạo sơ đồ trang web và bộ code này, bạn có thể dễ dàng nâng cấp trang web của mình. Nếu có bất kỳ câu hỏi hay vướng mắc nào trong quá trình thực hiện, vui lòng để lại bình luận bên dưới.