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>
      
    63 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.
    🎈 ⓈⒾⓃⒼⓁⒺⓈ ⒹⒶⓎ 👬 👭
    💐 ①① ❍ ①① ❍ ②⑤ 👫 💕

    Kẻ Cô Đơn Chờ Đón Lễ Độc Thân 💃 💘 🕺 😘
    Thân Toàn Độc Mong Gặp Người Tình Xưa 👫 💞

    https://www.youtube.com/watch?v=WDL--ga8pDA

    👨 Nếu biết rằng tôi vẫn phòng không
    💖 Trời ơi người ấy có ngóng trông
    👩 Có nghĩ đến ngày xưa mặn nồng
    💋 Hay là đang vui vẻ trong lòng 🤔

    https://www.youtube.com/watch?v=Hi9TsnHDuVM
    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
    Em dùng đoạn code sau thử, lưu ý chỉ áp dụng trong khu vực comment (#comment-holder / #comments) nên sửa id cho đúng với phần comment của em
    [pre]<style>
    .yt-embed{position:relative;width:100%;max-width:640px;aspect-ratio:16/9;margin:8px 0;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.08)}
    .yt-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
    a.cmt-image{display:inline-block;max-width:100%;margin:6px 0}
    a.cmt-image img{max-width:100%;height:auto;border-radius:8px}
    </style>

    <script>
    (()=>{const R=['#comment-holder','#comments','.comment-thread'],S='.comment-content,.comment-body,.cmt-content,.comment',I=/\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i,U=/https?:\/\/[^\s<>"']+/g,
    Y=u=>{try{let x=new URL(u),h=x.hostname.replace(/^www\./,'');if(h==='youtu.be')return x.pathname.slice(1).split('/')[0];
    if(h==='youtube.com'||h==='m.youtube.com'){if(x.pathname==='/'||x.pathname==='/watch')return x.searchParams.get('v');
    if(x.pathname.startsWith('/shorts/'))return x.pathname.split('/')[2]||x.pathname.split('/')[1];
    if(x.pathname.startsWith('/embed/'))return x.pathname.split('/')[2]}return null}catch{return null}},
    E=id=>{let d=document.createElement('div');d.className='yt-embed';let f=document.createElement('iframe');f.loading='lazy';f.referrerPolicy='origin-when-cross-origin';f.allow='accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';f.allowFullscreen=!0;f.title='YouTube video';f.src='https://www.youtube.com/embed/'+encodeURIComponent(id);d.appendChild(f);return d},
    A=u=>{let a=document.createElement('a');a.href=u;a.target='_blank';a.rel='nofollow noopener';a.className='cmt-image';let i=document.createElement('img');i.loading='lazy';i.decoding='async';i.src=u;i.alt='Image from comment';a.appendChild(i);return a},
    T=n=>{if(n.nodeType!==3)return;let t=n.nodeValue,m,last=0,c=0,f=document.createDocumentFragment();while((m=U.exec(t))){let u=m[0];f.appendChild(document.createTextNode(t.slice(last,m.index)));let id=Y(u);if(id)f.appendChild(E(id)),c=1;else if(I.test(u))f.appendChild(A(u)),c=1;else{let a=document.createElement('a');a.href=u;a.textContent=u;a.rel='nofollow noopener';a.target='_blank';f.appendChild(a)}last=U.lastIndex}
    if(!c)return;f.appendChild(document.createTextNode(t.slice(last)));n.parentNode.replaceChild(f,n)},
    P=r=>{if(r.dataset.l2m)return;r.dataset.l2m=1;let w=document.createTreeWalker(r,NodeFilter.SHOW_TEXT,{acceptNode(n){if(!/\bhttps?:\/\//.test(n.nodeValue||''))return NodeFilter.FILTER_REJECT;for(let p=n.parentNode;p;p=p.parentNode){if(p.nodeType!==1)continue;let tg=p.tagName;if(tg==='A'||tg==='CODE'||tg==='PRE'||p.classList?.contains('yt-embed'))return NodeFilter.FILTER_REJECT}return NodeFilter.FILTER_ACCEPT}});
    let a=[];while(w.nextNode())a.push(w.currentNode);a.forEach(T)},
    init=()=>{let roots=R.map(s=>document.querySelector(s)).filter(Boolean);if(!roots.length)return;roots.forEach(r=>{r.querySelectorAll(S).forEach(P);new MutationObserver(ms=>{for(const m of ms)m.addedNodes.forEach(n=>{if(n.nodeType!==1)return;if(n.matches?.(S))P(n);n.querySelectorAll?.(S).forEach(P)})}).observe(r,{childList:1,subtree:1})})};
    document.readyState!=='loading'?init():document.addEventListener('DOMContentLoaded',init);
    })();
    </script>
    [/pre]
    cho mình xin liên kết nha:
    Tên: Kho Nhạc Tổng Hợp
    URL: https://www.khonhactonghop.site/
    Mô tả: Kho Nhạc Tổng Hợp là trang âm nhạc
    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.
    Nhìn vào dòng bên trên cùng thấy hàng loạt trang web dùng proxy qua cloudflare đều bị lỗi:
    https://i.imgur.com/ZAMqmml.png
    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
    Xin phép ad để ảnh tạm đây: https://i.imgur.com/HHJndpN.jpeg
    Hello anh, em sống lại rồi nè =))
    Vụt mõm nó chưa e?
    Đang ở Đức mà web sập rồi: https://i.imgur.com/cwrl7u8.jpeg
    mình đã đặt mục tiêu xây dựng blog và viết thêm về mảng blog khác. Chủ đề blog mình viết thì khá là quen thuộc với mọi người rồi. Hơn nữa sắp tới dịp tết đến xuân về nên mình quyết định viết về chủ đề ngày tết. Cũng như cho các bạn tham khảo thêm nét văn hóa truyền thống tốt đẹp ngày tết nhằm quảng bá hình ảnh.
    Liên kết với mình nhé:
    URL:https://www.chieucuoinam.net/
    Tên:Chiều Cuối Năm
    Mô tả: Chiều cuối năm - là dịp cả nhà quây quần bên nhau bữa cơm chiều tối ngày cuối năm
    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 😁
    Không biết anh đăng bài hay update vào ngày hôm nay có lỗi giao diện giống em không? https://i.imgur.com/1jvk3gT.png

    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