lonceng notifikasi
Bagus — berikut fitur lonceng notifikasi yang canggih, responsif (mobile + desktop), aksesibel (ARIA), dan dibuat dengan perhatian pada SEO (fallback HTML & JSON-LD). Kode ini siap-pasang: cukup taruh satu blok di <head> (CSS + script) dan satu baris kecil di body jika ingin tempat khusus. Saya juga sertakan contoh pemakaian (API lokal/simple push simulation) — bisa langsung dipakai atau dihubungkan ke backend (push server / Firebase / OneSignal) nanti.
Cara pakai singkat
-
Salin seluruh blok ke dalam
<head>warkasa1919.com (atau ke template Blogger → bagian<head>). -
Jika ingin menempelkan bell di header khusus, tambahkan
<div id="w1919-notif-mount"></div>di tempat header Anda; kalau tidak ada, script akan otomatis menempelkannya di kanan atas layar. -
Gunakan fungsi
W1919Notifications.push({title, body, url, image})dari kode untuk menambahkan notifikasi baru (contoh ada di akhir).
Kode (paste ke <head>) — siap pakai
<!-- START: Warkasa1919 Notification Bell (place this in <head>) -->
<style>
/* Styles responsif & minimal */
#w1919-notif-root { position: fixed; z-index: 9999; top: 12px; right: 12px; display:flex; align-items:center; gap:8px; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
@media (max-width:640px) { #w1919-notif-root { top: 8px; right: 8px; } }
.w1919-bell-btn {
display:inline-grid; place-items:center;
width:48px; height:48px; border-radius:12px; background:linear-gradient(180deg,#ffffffcc,#f5f5f5cc);
box-shadow: 0 6px 18px rgba(17,17,17,0.08); cursor:pointer; position:relative; border:1px solid rgba(0,0,0,0.04);
}
.w1919-bell-btn:focus { outline:3px solid rgba(59,130,246,0.22); outline-offset:2px; }
.w1919-bell-icon { font-size:22px; line-height:1; }
.w1919-badge {
position:absolute; top:6px; right:6px; min-width:18px; height:18px;
padding:0 6px; border-radius:18px; font-size:12px; font-weight:700;
display:inline-flex; align-items:center; justify-content:center;
background:#ef4444; color:#fff; box-shadow:0 2px 6px rgba(0,0,0,0.15);
}
/* Panel */
.w1919-panel {
width:320px; max-width:calc(100vw - 24px); background:#fff; border-radius:12px;
box-shadow: 0 20px 50px rgba(11,20,50,0.12); overflow:hidden; border:1px solid rgba(0,0,0,0.04);
transform-origin: top right; animation:pop .12s ease;
}
@keyframes pop { from { transform: translateY(-6px) scale(.98); opacity:0 } to { transform:none; opacity:1 } }
.w1919-panel .head { display:flex; align-items:center; justify-content:space-between; padding:12px 12px; border-bottom:1px solid #f1f5f9; }
.w1919-panel .head h4 { margin:0; font-size:14px; font-weight:700; }
.w1919-panel .clear-btn { font-size:13px; color:#0ea5a3; background:none; border:0; cursor:pointer; }
.w1919-list { max-height:360px; overflow:auto; display:flex; flex-direction:column; gap:0; }
.w1919-item { display:flex; gap:10px; padding:12px; align-items:flex-start; border-bottom:1px solid #f8fafc; text-decoration:none; color:inherit; }
.w1919-item:hover { background:#f8fafc; }
.w1919-item .meta { display:flex; flex-direction:column; gap:4px; }
.w1919-item .title { font-weight:700; font-size:13px; }
.w1919-item .time { font-size:12px; color:#64748b; }
.w1919-item img { width:56px; height:40px; object-fit:cover; border-radius:6px; }
.w1919-empty { padding:24px; text-align:center; color:#64748b; font-size:13px; }
/* small-screen tweaks */
@media (max-width:420px){
.w1919-panel{ width:92vw; right:6px; left:6px; border-radius:10px; }
.w1919-item img{ display:none; }
}
/* simple accessibility helpers */
.sr-only { position:absolute !important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
</style>
<script>
/* Warkasa1919 Notification Bell — lightweight, no lib */
(function(){
if (window.W1919Notifications) return; // avoid double load
// Storage keys
const STORAGE_KEY_ITEMS = 'w1919_notif_items_v1';
const STORAGE_KEY_UNREAD = 'w1919_notif_unread_v1';
// utils
const $ = sel => document.querySelector(sel);
const create = (tag, attrs={}, children=[]) => {
const el = document.createElement(tag);
for(const k in attrs){
if(k === 'class') el.className = attrs[k];
else if(k === 'html') el.innerHTML = attrs[k];
else if(k === 'text') el.textContent = attrs[k];
else el.setAttribute(k, attrs[k]);
}
children.forEach(c => el.appendChild(c));
return el;
};
// load stored items
function loadItems(){ try { return JSON.parse(localStorage.getItem(STORAGE_KEY_ITEMS) || '[]'); } catch(e){ return []; } }
function saveItems(items){ localStorage.setItem(STORAGE_KEY_ITEMS, JSON.stringify(items)); }
function loadUnread(){ return Number(localStorage.getItem(STORAGE_KEY_UNREAD) || 0); }
function saveUnread(n){ localStorage.setItem(STORAGE_KEY_UNREAD, String(n)); }
// root container
let root = document.getElementById('w1919-notif-root');
if(!root){
root = document.createElement('div');
root.id = 'w1919-notif-root';
document.body.appendChild(root);
}
// create bell
const bellBtn = create('button', { class: 'w1919-bell-btn', 'aria-label':'Lonceng notifikasi', 'aria-haspopup':'dialog', 'aria-expanded':'false', title:'Notifikasi' });
bellBtn.innerHTML = '<span class="w1919-bell-icon" aria-hidden="true">🔔</span>';
const badge = create('span', { class:'w1919-badge', 'aria-hidden':'true' });
bellBtn.appendChild(badge);
// panel (hidden initially)
const panelWrap = create('div', { style:'display:none; position:relative;' });
const panel = create('div', { class:'w1919-panel', role:'dialog', 'aria-label':'Daftar notifikasi', tabindex:'-1' });
panelWrap.appendChild(panel);
// panel contents
const head = create('div', { class:'head' });
head.appendChild(create('h4', { text:'Notifikasi' }));
const clearBtn = create('button', { class:'clear-btn', text:'Bersihkan' });
head.appendChild(clearBtn);
panel.appendChild(head);
const list = create('div', { class:'w1919-list', id:'w1919-notif-list' });
panel.appendChild(list);
const emptyNote = create('div', { class:'w1919-empty', text:'Belum ada notifikasi.' });
panel.appendChild(emptyNote);
root.appendChild(panelWrap);
root.appendChild(bellBtn);
// keyboard/close helpers
function closePanel(){ panelWrap.style.display='none'; bellBtn.setAttribute('aria-expanded','false'); panelBlur(); }
function openPanel(){ renderList(); panelWrap.style.display='block'; bellBtn.setAttribute('aria-expanded','true'); panel.focus(); markAllRead(); }
function panelBlur(){ /* nothing for now */ }
// render
function renderList(){
const items = loadItems().slice().reverse(); // newest first
list.innerHTML = '';
if(items.length===0){
list.style.display='none';
emptyNote.style.display='block';
return;
}
list.style.display='flex';
emptyNote.style.display='none';
items.forEach((it, idx) => {
const a = create('a', { class:'w1919-item', href: it.url || '#', title: it.title || '', role:'link' });
a.addEventListener('click', (e) => {
// when clicked, mark as read and optionally navigate
e.preventDefault();
// open in new tab
if(it.url) window.open(it.url,'_blank');
markItemRead(it.id);
renderList();
});
if(it.image){
const img = create('img', { src: it.image, alt: '' });
a.appendChild(img);
}
const meta = create('div', { class:'meta' });
meta.appendChild(create('div', { class:'title', text: it.title || 'Tanpa judul' }));
const t = new Date(it.time || Date.now());
meta.appendChild(create('div', { class:'time', text: t.toLocaleString() }));
a.appendChild(meta);
list.appendChild(a);
});
}
function updateBadge(){
const n = loadUnread();
if(n>0){ badge.style.display='inline-flex'; badge.textContent = n>99? '99+': String(n); }
else badge.style.display='none';
}
function pushNotification(obj){
// obj: { title, body, url, image }
const items = loadItems();
const id = 'w1919_'+Date.now()+'_'+Math.floor(Math.random()*1000);
items.push({ id, title: obj.title||'Notifikasi', body: obj.body||'', url: obj.url||'', image: obj.image||'', time: Date.now(), read:false });
saveItems(items);
const unread = loadUnread() + 1;
saveUnread(unread);
updateBadge();
// visual micro-toast (optional)
toast(obj.title, obj.body);
}
function toast(title, body){
// small non-intrusive toast for immediate feedback
const existing = document.getElementById('w1919-toast');
if(existing) existing.remove();
const t = create('div', { id:'w1919-toast', class:'w1919-toast', html:`<strong>${escapeHtml(title)}</strong><div style="font-size:13px;margin-top:4px;color:#475569">${escapeHtml(body||'')}</div>` });
Object.assign(t.style, { position:'fixed', right:'16px', bottom:'18px', zIndex:99999, background:'#0f172a', color:'#fff', padding:'12px 14px', borderRadius:'10px', boxShadow:'0 8px 30px rgba(2,6,23,0.4)', maxWidth:'92vw' });
document.body.appendChild(t);
setTimeout(()=> t.style.opacity='0', 3000);
setTimeout(()=> t.remove(), 3800);
}
function escapeHtml(str=''){ return String(str).replace(/[&<>"']/g, s => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[s])); }
function markAllRead(){ saveUnread(0); updateBadge(); const items = loadItems().map(i=>({...i, read:true})); saveItems(items); }
function markItemRead(id){
const items = loadItems().map(i => i.id===id ? {...i, read:true} : i );
saveItems(items);
// recalc unread
const unread = items.filter(i=>!i.read).length;
saveUnread(unread);
updateBadge();
}
// clear (remove all)
clearBtn.addEventListener('click', () => {
localStorage.removeItem(STORAGE_KEY_ITEMS);
localStorage.removeItem(STORAGE_KEY_UNREAD);
renderList(); updateBadge();
});
// bell interactions
bellBtn.addEventListener('click', (e) => {
const open = panelWrap.style.display === 'block';
if(open) closePanel(); else openPanel();
});
// click outside to close
document.addEventListener('click', (e) => {
if(!panelWrap.contains(e.target) && !bellBtn.contains(e.target)) closePanel();
});
// keyboard
document.addEventListener('keydown', (e) => {
if(e.key === 'Escape') closePanel();
});
// initial render
updateBadge();
renderList();
// expose global API
window.W1919Notifications = {
push: function(obj){
pushNotification(obj);
},
list: function(){ return loadItems(); },
unreadCount: function(){ return loadUnread(); },
markAllRead: markAllRead,
mountTo: function(selector){
const el = document.querySelector(selector);
if(el){
// move root inside element
el.appendChild(root);
}
},
// helper for server push (example only: should be adapted to real push)
simulatePushFromServer: function(){ // demo: add sample
this.push({ title:'Contoh Berita Baru', body:'Baca artikel terbaru di warkasa1919.com', url:'/', image:'' });
}
};
// SEO + noscript fallback: inject a small hidden link list for crawlers & non-js users
(function insertSEOFallback(){
const seo = document.createElement('div');
seo.setAttribute('aria-hidden','true');
seo.style.position='absolute'; seo.style.left='-9999px'; seo.style.height='1px'; seo.style.overflow='hidden';
seo.innerHTML = '<strong>Notifikasi:</strong> <a href="/notifikasi">Lihat notifikasi terbaru</a>';
document.body.appendChild(seo);
})();
// small hydration: if notifications page anchor exists, mount there
window.addEventListener('DOMContentLoaded', () => {
const mount = document.getElementById('w1919-notif-mount');
if(mount) mount.appendChild(root);
});
})();
</script>
<!-- Optional structured data (SEO) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Warkasa1919",
"url": "https://warkasa1919.com",
"potentialAction": {
"@type": "SearchAction",
"target": "https://warkasa1919.com/?s={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
<!-- END: Warkasa1919 Notification Bell -->
Contoh penggunaan (panggil dari mana saja di situs, mis. ketika artikel baru diterbitkan)
Letakkan ini pada script server-side atau panggil via AJAX dari server Anda untuk menambahkan notifikasi ke seluruh pengguna (contoh: setelah artikel publish, panggil endpoint yang memanggil W1919Notifications.push(...) pada client via WebSocket / SSE / push service). Untuk pengujian lokal, buka console browser dan jalankan:
// tambahkan notifikasi manual (bisa dijalankan dari console untuk uji)
W1919Notifications.push({
title: 'Artikel Baru: Rahasia Kopi Pagi',
body: 'Baca sekarang: 5 kebiasaan pagi yang mengubah hidup.',
url: '/artikel/rahasia-kopi-pagi',
image: 'https://warkasa1919.com/assets/thumbs/kopi.jpg'
});
// lihat daftar
console.log(W1919Notifications.list());
// unread count
console.log(W1919Notifications.unreadCount());
Integrasi lebih lanjut (opsional, rekomendasi)
-
Push nyata (recommended): Hubungkan ke Push API (service worker) atau gunakan provider seperti Firebase Cloud Messaging / OneSignal untuk mengirim push browser nyata. Saya bisa bantu tambahkan Service Worker + step-by-step integrasi jika mau.
-
Server sync: Simpan notifikasi di DB (per user) lalu gunakan WebSocket / SSE untuk mendorong notifikasi ke klien agar badge sinkron antar device.
-
Analytics: Catat klik notifikasi di server untuk metrik CTR.
-
SEO: Saya sudah menyertakan fallback link dan JSON-LD. Untuk hasil terbaik, sediakan halaman
/notifikasiyang merender notifikasi server-side sehingga search engine dapat mengindeks daftar berita penting.
Kalau setuju, saya bisa:
-
Tambahkan versi Blogger-ready (HTML siap-paste ke template Blogger) jika Anda mau; atau
-
Tambahkan Service Worker + contoh FCM untuk push nyata agar lonceng benar-benar bisa mendorong notifikasi ke pengguna (desktop & mobile) — saya buatkan langkah lengkap dengan file service-worker.js.
Mau saya lanjut ke salah satu dari dua opsi itu sekarang?
