Widget bài này năm xưa giống Facebook
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
- 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,'&').replace(/</g,'<').replace(/>/g,'>')
.replace(/"/g,'"').replace(/'/g,''');
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>
e nghĩ a chia sẽ giao diện đó nữa chứ :D :D
Trả lờiXóaTemplate này có gì đặc biệt đâu em 🤔
Trả lờiXóaĐược quá nhở 😍
Trả lờiXóaTại đang làm cái blog cá nhân theo dạng Facebook nên nghĩ ra 😁
Trả lờiXóahi vì e thích đơn giản a :D a share mẫu giao diện đó đi :P
Trả lờiXóaNó chưa hoàn chỉnh, khi nào hoàn chỉnh a share
Trả lờiXóagiờ ô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à.
Trả lờiXóaơ cái comment bị lỗi rôig
Trả lờiXóaQuá hay
Trả lờiXóaThank bác!
Trả lờiXóae chạy thử ko thấy load ra đc j
Trả lờiXóađã 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
Trả lờiXóaShare theme đi anh
Trả lờiXóaTemplate này có gì đâu mà share, nhìn đơn giản không hợp với xu hướng bây giờ
Trả lờiXóakb có ae nào chạy đc ko, e chạy thử lại r vẫn ko lên
Trả lờiXóalâu rồi chưa ra bài viết mới a nhỉ :P
Trả lờiXóaKhông có ý tưởng gì e 😁
Trả lờiXóaviế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
Trả lờiXóaVẫn không hiểu lắm chức năng như em nói
Trả lờiXóakiể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
Trả lờiXóaSao domain với nội dung website chẳng liên quan gì đến nhau vậy? 🤔
Trả lờiXóaem gắn tạm chờ tenten duyệt doamin chủ thể đăng ký dưới 18
Trả lờiXóaVậ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é 😁
Trả lờiXóaNhận xét này đã bị tác giả xóa.
Trả lờiXóa🎈 ⓈⒾⓃⒼⓁⒺⓈ ⒹⒶⓎ 👬 👭
Trả lờiXó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ỉ? 😁
Trả lờiXóagửi e mã nguồn youtube + hình ảnh ở phần comment đc k a :D
Trả lờiXóaEm 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
Trả lờiXóa[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:
Trả lờiXóaTê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! 👌
Trả lờiXóacho thuê subdomain ko bạn
Trả lờiXóaKhông bạn
Trả lờiXóaNhận xét này đã bị tác giả xóa.
Trả lờiXóaChư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
Trả lờiXóaNhận xét này đã bị tác giả xóa.
Trả lờiXóaNhì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:
Trả lờiXóahttps://i.imgur.com/ZAMqmml.png
Lượng truy cập có tí ti nên cần gì nhỉ 😂
Trả lờiXóaDone nha!
Trả lờiXóaNhận xét này đã bị tác giả xóa.
Trả lờiXóakéo tóp comment :v
Trả lờiXóatrà đá hà nội nhiều tên phết nhể
Trả lờiXóaBlogger 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
Trả lờiXóaVẫ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
Trả lờiXóaChú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
Trả lờiXóaCảm ơn em! Nhưng cạn ý tưởng rồi 😂
Trả lờiXóaChuyển sang làm thơ đi anh
Trả lờiXóaHello anh, em sống lại rồi nè =))
Trả lờiXóaVụt mõm nó chưa e?
Trả lờiXóaĐang ở Đức mà web sập rồi: https://i.imgur.com/cwrl7u8.jpeg
Trả lờiXóamì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.
Trả lờiXóaLiê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
Trả lờiXóaÀ có gì anh đổi feed em sang wp nha
Trả lờiXóaDomain Hết hạn vào cuối tuần nên quên gia hạn thôi
Trả lờiXóaDone rồi em
Trả lờiXóaEm đăng bài nó hỏng đẩy em lên vậy ah ơi =))
Trả lờiXóanhìn xịn phết :))
Trả lờiXóaChắc giờ được rồi đấy 😁
Trả lờiXóaKhô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
Trả lờiXóaNói thế không biết lỗi gì
Trả lờiXóaNhận xét này đã bị tác giả xóa.
Trả lờiXóaChẳ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ữ.
Trả lờiXóa