Sosial Media
0
News
    Home Blogspot Widget

    Widget bài này năm xưa giống Facebook

    5 min read

    Widget hiển thị các bài đăng có cùng ngày/tháng với hôm nay trong những năm trước, theo kiểu bảng tin Facebook. Nếu không có bài “kỷ niệm”, widget vẫn hiển thị một thẻ mặc định

    Tính năng chính

    • Giao diện stories trượt ngang như tin tức của Facebook
    • Lọc bài theo ngày/tháng khớp hôm nay từ bài viết trong quá khư (có thể ± ngày theo cài đặt để phù hợp với múi giờ).

    Cách cài đặt nhanh

    1. Tuỳ chỉnh trong code:
      • LIMIT: Số bài muốn hiển thị.
      • FLEX_DAYS: Tăng giảm số ngày khớp (ví dụ 1 = hôm qua/hôm nay/ngày mai).

    Code

    <section id="anniversary-posts" class="story-wrap">
      <div class="story-head">
        <h3 class="story-title">Kỷ niệm</h3>
        <div class="story-ctrl">
          <button class="story-btn prev" aria-label="Prev" type="button" disabled>
            <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
              <path d="M15.5 19l-7-7 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
            </svg>
          </button>
          <button class="story-btn next" aria-label="Next" type="button" disabled>
            <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
              <path d="M8.5 5l7 7-7 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
            </svg>
          </button>
        </div>
      </div>
    
      <div class="story-rail">
        <ul class="story-track" aria-live="polite">
          <li class="story-skeleton"></li>
          <li class="story-skeleton"></li>
          <li class="story-skeleton"></li>
          <li class="story-skeleton"></li>
        </ul>
      </div>
    </section>
    
    <style>
    .story-wrap{margin:14px 0;font:inherit}
    .story-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
    .story-title{margin:0;font-weight:700}
    .story-ctrl{display:flex;gap:8px}
    .story-btn{
      width:34px;height:34px;border:1px solid #e5e7eb;border-radius:999px;background:#fff;
      display:inline-flex;align-items:center;justify-content:center;cursor:pointer;opacity:.95
    }
    .story-btn[disabled]{opacity:.4;cursor:not-allowed}
    .story-btn:active{transform:scale(.98)}
    .story-rail{position:relative}
    .story-track{
      display:flex;gap:10px;overflow-x:auto;overflow-y:hidden;padding:4px 2px 12px;scrollbar-width:none;
      -ms-overflow-style:none;scroll-snap-type:x mandatory;scroll-behavior:smooth;-webkit-overflow-scrolling:touch
    }
    .story-track::-webkit-scrollbar{display:none}
    .story-card{
      position:relative;flex:0 0 auto;width:140px;height:248px;border-radius:14px;overflow:hidden;
      background:#e5e7eb;scroll-snap-align:start;user-select:none
    }
    .story-bg{position:absolute;inset:0;background:#ddd center/cover no-repeat}
    .story-grad{
      position:absolute;inset:0;
      background:linear-gradient(180deg,rgba(0,0,0,.0) 10%, rgba(0,0,0,.35) 65%, rgba(0,0,0,.6) 100%);
    }
    .story-text{
      position:absolute;left:8px;right:8px;bottom:8px;color:#fff;line-height:1.2;
      font-weight:700;text-shadow:0 1px 2px rgba(0,0,0,.4)
    }
    .story-text .name{
      display:block;max-height:2.6em;overflow:hidden;-webkit-line-clamp:2;display:-webkit-box;-webkit-box-orient:vertical
    }
    .story-text .date{font-weight:600;font-size:12px;opacity:.9;margin-top:4px}
    .story-chevron{
      position:relative;flex:0 0 auto;width:56px;height:248px;border-radius:14px;background:#fff;
      display:grid;place-items:center;border:1px solid #e5e7eb;scroll-snap-align:start
    }
    .story-chevron .btn{
      width:36px;height:36px;border-radius:999px;background:#fff;border:1px solid #e5e7eb;display:grid;place-items:center
    }
    .story-skeleton{
      flex:0 0 auto;width:140px;height:248px;border-radius:14px;
      background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb 37%,#f3f4f6 63%);background-size:400% 100%;
      animation:shine 1.1s infinite
    }
    @keyframes shine{0%{background-position:100% 0}100%{background-position:0 0}}
    @media (max-width:480px){
      .story-card{width:34vw;max-width:160px;height:56vw;max-height:260px}
      .story-chevron{height:56vw;max-height:260px}
    }
    @media (prefers-reduced-motion: reduce){
      .story-track{scroll-behavior:auto}
      .story-skeleton{animation:none}
    }
    </style>
    
    <script>
    (function(){
      "use strict";
    
      const LIMIT = 20;
      const HARD_CAP = 2000;
      const FEED0 = '/feeds/posts/summary?alt=json&max-results=150';
      const FLEX_DAYS = 1;
    
      const now  = new Date();
      const TODAY= { d: now.getDate(), m1: now.getMonth()+1, y: now.getFullYear() };
    
      const root  = document.getElementById('anniversary-posts');
      if(!root) return;
      const track = root.querySelector('.story-track');
      const bPrev = root.querySelector('.story-btn.prev');
      const bNext = root.querySelector('.story-btn.next');
    
      const picked = [];
    
      const pad2 = n => String(n).padStart(2,'0');
      const escapeHTML = s => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
                                             .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
      const NO_STORY_BG = (() => {
        const svg =
    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 560" width="320" height="560">
      <defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="#dbeafe"/><stop offset="100%" stop-color="#93c5fd"/></linearGradient></defs>
      <rect x="0" y="0" width="320" height="560" rx="24" fill="url(#g)"/>
      <g fill="none" stroke="#1f2937" stroke-width="10" opacity="0.25">
        <rect x="70" y="120" width="180" height="140" rx="14"/>
        <circle cx="160" cy="330" r="42"/><path d="M60 420h200M60 460h160"/></g></svg>`;
        return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
      })();
    
      function getYMD(entry){
        const s = (entry?.published?.$t) || (entry?.updated?.$t) || '';
        const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
        return m ? { y:+m[1], m1:+m[2], d:+m[3] } : null;
      }
      function buildFlexSet(){
        const set = new Set();
        const base = new Date(TODAY.y, TODAY.m1-1, TODAY.d, 12);
        set.add(\`\${TODAY.d}-\${TODAY.m1}\`);
        for(let i=1;i<=FLEX_DAYS;i++){
          const d1=new Date(base); d1.setDate(base.getDate()-i);
          const d2=new Date(base); d2.setDate(base.getDate()+i);
          set.add(\`\${d1.getDate()}-\${d1.getMonth()+1}\`);
          set.add(\`\${d2.getDate()}-\${d2.getMonth()+1}\`);
        }
        return set;
      }
      const FLEXSET = buildFlexSet();
    
      function sameDayPast(parts){
        if(!parts || parts.y >= TODAY.y) return false;
        return FLEXSET.has(\`\${parts.d}-\${parts.m1}\`);
      }
      function getLink(entry){
        const alt = (entry.link || []).find(l => l.rel === 'alternate');
        return alt?.href || '#';
      }
      function getThumb(entry){
        if (entry.media$thumbnail?.url)
          return entry.media$thumbnail.url.replace(/\\/s\\d{2,4}(-c)?\\//, '/s720-c/');
        const html = (entry.content?.$t) || (entry.summary?.$t) || '';
        const m = html.match(/<img[^>]+src="([^"]+)"/i);
        return m ? m[1] : NO_STORY_BG;
      }
      function shuffle(a){ for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]];} return a; }
    
      function render(list){
        let html = '';
        if (!list.length){
          html = \`
            <li class="story-card">
              <div class="story-bg" style="background-image:url('\${NO_STORY_BG}')"></div>
              <div class="story-grad"></div>
              <div class="story-text"><span class="name">No Story</span></div>
            </li>\`;
          track.innerHTML = html;
          bPrev.disabled = bNext.disabled = true;
          return;
        }
        html = list.map(it => \`
          <li class="story-card">
            <a href="\${it.link}" class="story-link" aria-label="\${escapeHTML(it.title)}">
              <div class="story-bg" style="background-image:url('\${it.thumb}')"></div>
              <div class="story-grad"></div>
              <div class="story-text">
                <span class="name">\${escapeHTML(it.title)}</span>
                <span class="date">\${pad2(it.d)}/\${pad2(it.m1)}/\${it.y}</span>
              </div>
            </a>
          </li>\`).join('');
        html += \`
          <li class="story-chevron">
            <button type="button" class="btn go-next" aria-label="Next">
              <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
                <path d="M8.5 5l7 7-7 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
              </svg>
            </button>
          </li>\`;
        track.innerHTML = html;
        track.querySelector('.go-next')?.addEventListener('click', () => scrollByStep(1));
        updateButtons();
      }
    
      async function crawl(url, scanned=0){
        if (!url || scanned >= HARD_CAP) return;
        try{
          const res = await fetch(url, { credentials:'same-origin' });
          if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
          const data = await res.json();
          const entries = data?.feed?.entry || [];
          for (const e of entries){
            const parts = getYMD(e);
            if (sameDayPast(parts)){
              picked.push({
                title: e?.title?.$t || '(Không tiêu đề)',
                link:  getLink(e),
                thumb: getThumb(e),
                y: parts.y, m1: parts.m1, d: parts.d
              });
            }
          }
          const next = (data?.feed?.link || []).find(l => l.rel === 'next');
          if (next?.href && scanned + entries.length < HARD_CAP){
            const u = next.href.startsWith('http') ? new URL(next.href) : null;
            const href = u ? (u.pathname + u.search) : next.href;
            return crawl(href, scanned + entries.length);
          }
        }catch(err){ console.error('Anniversary feed error:', err); }
      }
    
      function scrollByStep(dir=1){
        const step = Math.max(track.clientWidth*0.95, 320);
        track.scrollBy({ left: step*dir, behavior:'smooth' });
      }
      function updateButtons(){
        const max = track.scrollWidth - track.clientWidth - 1;
        bPrev.disabled = track.scrollLeft <= 0;
        bNext.disabled = track.scrollLeft >= max;
      }
      track.addEventListener('wheel', e => {
        if (Math.abs(e.deltaY) > Math.abs(e.deltaX)){
          track.scrollLeft += e.deltaY; e.preventDefault(); updateButtons();
        }
      }, { passive:false });
      track.addEventListener('scroll', () => updateButtons(), { passive:true });
      bPrev.addEventListener('click', () => scrollByStep(-1));
      bNext.addEventListener('click', () => scrollByStep(1));
    
      (async function init(){
        track.innerHTML = '<li class="story-skeleton"></li><li class="story-skeleton"></li><li class="story-skeleton"></li><li class="story-skeleton"></li>';
        await crawl(FEED0);
        shuffle(picked);
        render(picked.slice(0, LIMIT));
      })();
    })();
    </script>
      
    55 nhận xét
    e nghĩ a chia sẽ giao diện đó nữa chứ :D :D
    Template này có gì đặc biệt đâu em 🤔
    Được quá nhở 😍
    Tại đang làm cái blog cá nhân theo dạng Facebook nên nghĩ ra 😁
    hi vì e thích đơn giản a :D a share mẫu giao diện đó đi :P
    Nó chưa hoàn chỉnh, khi nào hoàn chỉnh a share
    giờ ông TruongDevs nghỉ hoạt động cái blog TruongDevs và thành lập cái khác đi là hết giả mạo ngay ấy mà.
    ơ cái comment bị lỗi rôig
    e chạy thử ko thấy load ra đc j

    đã chỉnh FLEX_DAYS thành 10, max-results=1500
    Bác cứ để đấy đừng xóa đi nhé. Tối về e check lại xem sao
    Share theme đi anh
    Template này có gì đâu mà share, nhìn đơn giản không hợp với xu hướng bây giờ
    kb có ae nào chạy đc ko, e chạy thử lại r vẫn ko lên
    lâu rồi chưa ra bài viết mới a nhỉ :P
    Không có ý tưởng gì e 😁
    viết bài chia sẽ mã nguồn giao diện blogger, tự động chuyển hướng liên kết khi trong bài viết là đường link và ngược lại nếu là nội dung text thì không chuyển hướng đó a :D, như hồi xưa bên linkthuthuat á a :D
    Vẫn không hiểu lắm chức năng như em nói
    kiểu như title vẫn bình thường a nhé, còn ở phần body, a tùy biến sao, mà khi mình viết bài chỉ cần bỏ liên kết vào bài viết thì tự động chuyển hướng liên kết theo link, và ngược lại, nếu người dùng viết bài như bình thường thì không chuyển hướng đó a :D
    Thử đoạn code này xem đúng ý e không?
    Chỉ áp dụng trong phạm vi nội dung bài viết (.post-body)
    [pre] <script>
    document.addEventListener("DOMContentLoaded", function() {
    const postBody = document.querySelector('.post-body');
    if (!postBody) return;

    // Nhận dạng các URL
    const urlRegex = /((https?:\/\/|www\.)[^\s<]+)/g;

    const walker = document.createTreeWalker(postBody, NodeFilter.SHOW_TEXT, null, false);
    const textNodes = [];

    while (walker.nextNode()) {
    const node = walker.currentNode;
    // Bỏ qua các text đã nằm trong thẻ <a>
    if (!node.parentNode.closest('a')) textNodes.push(node);
    }

    textNodes.forEach(node => {
    const text = node.nodeValue;
    if (urlRegex.test(text)) {
    const replacedHTML = text.replace(urlRegex, function(url) {
    let href = url.startsWith('http') ? url : 'https://' + url;
    return `<a href="${href}" target="_blank" rel="nofollow noopener">${url}</a>`;
    });

    const span = document.createElement('span');
    span.innerHTML = replacedHTML;
    node.parentNode.replaceChild(span, node);
    }
    });
    });
    </script>[/pre]
    Sao domain với nội dung website chẳng liên quan gì đến nhau vậy? 🤔
    em gắn tạm chờ tenten duyệt doamin chủ thể đăng ký dưới 18
    Vậy để khi có domain ổn định đi đỡ phải thay đổi lại nhé. Và gửi yêu cầu liên kết tại đây nhé 😁
    Nhận xét này đã bị tác giả xóa.
    Bác có vẻ yêu âm nhạc nhỉ? 😁
    gửi e mã nguồn youtube + hình ảnh ở phần comment đc k a :D
    Done nha bạn! 👌
    cho thuê subdomain ko bạn
    Nhận xét này đã bị tác giả xóa.
    Chưa thấy đặt link của mình, hơn nữa không cần thiết comment phải gắn link vào toàn bộ nội dung đâu
    Nhận xét này đã bị tác giả xóa.
    Lượng truy cập có tí ti nên cần gì nhỉ 😂
    Nhận xét này đã bị tác giả xóa.
    kéo tóp comment :v
    trà đá hà nội nhiều tên phết nhể
    Blogger bây giờ hình như chỉ lấy đc max-results=150 thì phải, bài cũ hơn ko lấy đc
    Vẫn bình thường mà bác, như cái trang nhận xét nó lấy full đến bài cuối cùng luôn
    Chúc a zai cuối tuần vui vẻ và hạnh phúc, hóng a ra bài mới :P, để e cóp dán nà kk
    Cảm ơn em! Nhưng cạn ý tưởng rồi 😂
    Chuyển sang làm thơ đi anh
    Hello anh, em sống lại rồi nè =))
    Vụt mõm nó chưa e?
    Mấy hôm trước sao em vào blog anh bị gì không truy cập được
    À có gì anh đổi feed em sang wp nha
    Domain Hết hạn vào cuối tuần nên quên gia hạn thôi
    Em đăng bài nó hỏng đẩy em lên vậy ah ơi =))
    nhìn xịn phết :))
    Chắc giờ được rồi đấy 😁
    Nói thế không biết lỗi gì
    Nhận xét này đã bị tác giả xóa.
    Chẳng có cách nào ngoài cách duy nhất tải file ảnh và file nội dung .xml đấy về, xong mở notepad lên ctrl+h rồi nhập đường dẫn ảnh hiện tải và đổi đường dẫn hàng loạt. Còn file ảnh up lên kho lưu trữ.
    Additional JS