Salin seluruh blok berikut dan tempel di <head> (CSS) + di area header/body (HTML) + sebelum </body> (JS) — atau gabungkan sesuai struktur template Anda.
<!-- ============================================
LONCENG NOTIFIKASI RINGAN UNTUK warkasa1919.com
- Copy CSS ke <head>
- Paste HTML di header/nav tempat Anda mau
- Paste JS sebelum </body>
- Mode "remote": ganti NOTIF_ENDPOINT ke url JSON Anda
============================================ -->
<!-- ====== CSS (paste ke <head>) ====== -->
<style>
/* Reset kecil untuk komponen */
.wk-notif { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; position: relative; display:inline-block; z-index:9999; }
.wk-bell-btn {
background: transparent;
border: none;
cursor: pointer;
position: relative;
padding: 6px;
display: inline-flex;
align-items:center;
justify-content:center;
border-radius:999px;
transition: transform .12s ease, box-shadow .12s ease;
}
.wk-bell-btn:active{ transform: scale(.98); }
.wk-bell-btn:focus{ outline: 3px solid rgba(81, 203, 238, 0.4); outline-offset: 2px; }
/* Bell SVG size */
.wk-bell-svg { width:28px; height:28px; display:block; }
/* Badge */
.wk-badge {
position: absolute;
top: 2px;
right: 2px;
min-width:18px;
height:18px;
padding:0 5px;
font-size:12px;
line-height:18px;
border-radius:999px;
background:#e23b3b;
color:white;
display:inline-flex;
align-items:center;
justify-content:center;
box-shadow: 0 1px 3px rgba(0,0,0,.2);
}
/* Dropdown panel */
.wk-panel {
position:absolute;
right:0;
top:40px;
width:320px;
max-height:420px;
background: #fff;
border-radius:10px;
box-shadow: 0 10px 30px rgba(0,0,0,.12);
overflow:hidden;
display:flex;
flex-direction:column;
transform-origin: top right;
transition: opacity .16s ease, transform .16s ease;
opacity:0;
pointer-events:none;
transform: translateY(-6px) scale(.98);
border: 1px solid rgba(0,0,0,.04);
}
/* visible */
.wk-panel.open { opacity:1; pointer-events:auto; transform: translateY(0) scale(1); }
/* Header inside panel */
.wk-panel .hdr {
display:flex;
align-items:center;
justify-content:space-between;
padding:10px 12px;
border-bottom:1px solid rgba(0,0,0,.04);
gap:8px;
}
.wk-controls { display:flex; gap:8px; align-items:center; }
.wk-controls button { background:transparent; border:none; cursor:pointer; font-size:13px; color:#333; padding:6px; border-radius:6px; }
.wk-controls button:hover{ background:rgba(0,0,0,.04); }
/* List */
.wk-list { overflow:auto; padding:8px; display:flex; flex-direction:column; gap:8px; }
.wk-item {
display:flex; gap:10px; align-items:flex-start; padding:8px; border-radius:8px; transition: background .12s;
}
.wk-item.unread { background: linear-gradient(90deg, rgba(226,59,59,0.04), transparent); }
.wk-item:hover { background: rgba(0,0,0,.02); }
.wk-item .meta { font-size:12px; color:#666; }
.wk-item h4 { margin:0; font-size:14px; line-height:1.2; color:#111; }
.wk-item p { margin:4px 0 0 0; font-size:13px; color:#444; }
/* Tag chips */
.wk-tag { font-size:11px; padding:3px 6px; border-radius:6px; background: rgba(0,0,0,.06); color:#222; }
/* Empty state */
.wk-empty { padding:28px; text-align:center; color:#666; font-size:14px; }
/* Footer */
.wk-footer { padding:8px; border-top:1px solid rgba(0,0,0,.04); display:flex; justify-content:space-between; gap:8px; align-items:center; }
/* Responsive: full width on small screens */
@media (max-width:480px){
.wk-panel { left:8px; right:8px; width: auto; top:44px; border-radius:12px; }
.wk-bell-svg { width:26px; height:26px; }
}
</style>
<!-- ====== HTML (tempatkan di header/nav) ====== -->
<div class="wk-notif" id="wk-notif" aria-live="polite">
<button class="wk-bell-btn" id="wk-bell-btn" aria-haspopup="true" aria-expanded="false" aria-label="Notifikasi">
<!-- simple bell SVG -->
<svg class="wk-bell-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M15 17H9c0 1.66 1.34 3 3 3s3-1.34 3-3z" fill="currentColor" opacity=".9"/><path d="M18 16v-5c0-3.07-1.63-5.64-4.5-6.32V4a1.5 1.5 0 10-3 0v.68C7.63 5.36 6 7.92 6 11v5l-1.99 2H20l-2-2z" fill="currentColor"/></svg>
<span class="wk-badge" id="wk-badge" style="display:none">0</span>
</button>
<div class="wk-panel" id="wk-panel" role="dialog" aria-label="Daftar notifikasi" aria-hidden="true">
<div class="hdr">
<strong>Notifikasi</strong>
<div class="wk-controls">
<button id="wk-filter-btn" aria-haspopup="true" aria-expanded="false" title="Filter">Filter</button>
<button id="wk-mark-all" title="Tandai semua sudah dibaca">Tandai semua</button>
</div>
</div>
<div class="wk-list" id="wk-list">
<!-- items di-render lewat JS -->
<div class="wk-empty" id="wk-empty">Belum ada notifikasi.</div>
</div>
<div class="wk-footer">
<small id="wk-foot-info">Menampilkan 0 notifikasi</small>
<div>
<button id="wk-open-all" title="Buka semua di halaman notifikasi">Lihat semua</button>
</div>
</div>
</div>
</div>
<!-- ====== OPTIONAL: Toast area (untuk "ada konten baru") ====== -->
<div id="wk-toast-root" style="position:fixed; bottom:18px; right:18px; z-index:99999; pointer-events:none;"></div>
<!-- ====== JS (paste sebelum </body>) ====== -->
<script>
(function(){
/* =========== Konfigurasi =========== */
const MODE = 'remote'; // 'static' atau 'remote'
const NOTIF_POLL_INTERVAL = 45 * 1000; // polling tiap 45 detik (hanya untuk contoh)
const NOTIF_ENDPOINT = '/api/notifications.json'; // ganti dengan endpoint Anda (jika MODE==='remote')
const STORAGE_KEY = 'warkasa1919_notif_read_v1';
/* Contoh data statis (digunakan jika MODE === 'static') */
const STATIC_DATA = [
{ id: 'p1', type:'premium', title:'Artikel Premium: "Rahasia SEO 2025"', body:'Khusus untuk pelanggan — 3 tips cepat.', url:'/premium/seo-2025', ts: Date.now()-3600e3 },
{ id: 'b1', type:'paid', title:'Promo Paket Penulisan', body:'Diskon 25% untuk 5 artikel.', url:'/jasa/penulisan', ts: Date.now()-7200e3 },
{ id: 's1', type:'service', title:'Jasa Pembuatan Website', body:'Portfolio dan harga tersedia.', url:'/jasa/web', ts: Date.now()-86400e3 }
];
/* =========== Utility =========== */
function el(id){ return document.getElementById(id); }
function timeAgo(ts){
const s = Math.floor((Date.now()-ts)/1000);
if (s<60) return s + 's';
if (s<3600) return Math.floor(s/60)+'m';
if (s<86400) return Math.floor(s/3600)+'h';
return Math.floor(s/86400)+'d';
}
/* =========== State =========== */
let items = [];
let readMap = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
/* =========== DOM refs =========== */
const bellBtn = el('wk-bell-btn');
const panel = el('wk-panel');
const listEl = el('wk-list');
const badge = el('wk-badge');
const empty = el('wk-empty');
const markAllBtn = el('wk-mark-all');
const openAllBtn = el('wk-open-all');
const footInfo = el('wk-foot-info');
const toastRoot = el('wk-toast-root');
/* =========== Render =========== */
function renderList(filter='all'){
listEl.innerHTML = '';
const filtered = items.filter(it => filter==='all' ? true : it.type===filter);
if (filtered.length===0){
empty.style.display='block';
} else {
empty.style.display='none';
}
filtered.slice(0,50).forEach(it => {
const unread = !readMap[it.id];
const item = document.createElement('div');
item.className = 'wk-item' + (unread ? ' unread' : '');
item.tabIndex = 0;
item.setAttribute('role','button');
item.innerHTML = `
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:8px;justify-content:space-between">
<h4>${escapeHtml(it.title)}</h4>
<span class="wk-tag">${escapeHtml(it.type)}</span>
</div>
<p>${escapeHtml(it.body || '')}</p>
<div class="meta">${timeAgo(it.ts)} • ${it.source || 'Warkasa'}</div>
</div>
`;
item.addEventListener('click', function(e){
markRead(it.id);
if (it.url) window.location.href = it.url;
});
listEl.appendChild(item);
});
footInfo.textContent = `Menampilkan ${filtered.length} notifikasi`;
updateBadge();
}
function updateBadge(){
const unread = items.filter(it => !readMap[it.id]).length;
if (unread>0){
badge.style.display='inline-flex';
badge.textContent = unread>99 ? '99+' : unread;
} else {
badge.style.display='none';
}
}
/* =========== Mark read ========= */
function markRead(id){
readMap[id] = Date.now();
localStorage.setItem(STORAGE_KEY, JSON.stringify(readMap));
renderList(currentFilter);
}
function markAllRead(){
items.forEach(i => readMap[i.id] = Date.now());
localStorage.setItem(STORAGE_KEY, JSON.stringify(readMap));
renderList(currentFilter);
}
/* =========== Fetch / Load ========= */
async function loadRemote(){
try{
const res = await fetch(NOTIF_ENDPOINT, {cache:'no-store'});
if (!res.ok) throw new Error('Fetch error');
const json = await res.json();
// json expected as array of {id,type,title,body,url,ts,source}
if (!Array.isArray(json)) throw new Error('Invalid JSON');
handleNewItems(json);
}catch(err){
console.warn('wk-notif: remote load failed', err);
// fallback: jika belum ada item, gunakan static
if (items.length===0) handleNewItems(STATIC_DATA);
}
}
function loadStatic(){
handleNewItems(STATIC_DATA);
}
/* =========== New item handling / toast =========== */
function handleNewItems(arr){
// merge by id, newest first
const map = {};
arr.forEach(a => map[a.id] = a);
items.forEach(i => map[i.id] = i);
items = Object.values(map).sort((a,b) => (b.ts||0) - (a.ts||0));
// show toast for items not seen before
arr.forEach(a => {
if (!readMap[a.id] && !seenIdsBefore.has(a.id)){
showToast(`${a.title}`, a.type, a.url);
}
seenIdsBefore.add(a.id);
});
renderList(currentFilter);
}
/* escaped HTML helper */
function escapeHtml(s){
if (!s) return '';
return String(s).replace(/[&<>"']/g, function(m){ return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]; });
}
/* =========== Toast =========== */
function showToast(title, type, url){
const node = document.createElement('div');
node.style.pointerEvents='auto';
node.style.marginTop='8px';
node.style.padding='12px 14px';
node.style.borderRadius='10px';
node.style.boxShadow='0 8px 20px rgba(0,0,0,.12)';
node.style.background='#fff';
node.style.minWidth='220px';
node.style.cursor='pointer';
node.innerHTML = `<strong style="display:block;margin-bottom:4px">${escapeHtml(title)}</strong><small style="color:#666">${escapeHtml(type)}</small>`;
node.addEventListener('click', function(){
if (url) window.location.href = url;
});
toastRoot.appendChild(node);
setTimeout(()=> node.style.transform='translateX(0)', 10);
setTimeout(()=> {
node.style.transition='opacity .4s, transform .4s';
node.style.opacity='0';
node.style.transform='translateY(8px)';
setTimeout(()=> node.remove(), 450);
}, 6000);
}
/* =========== small helpers & state =========== */
let pollTimer = null;
let seenIdsBefore = new Set();
let currentFilter = 'all';
/* =========== Events =========== */
bellBtn.addEventListener('click', function(){
const expanded = panel.classList.toggle('open');
bellBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
panel.setAttribute('aria-hidden', expanded ? 'false' : 'true');
if (expanded) {
// when open, mark visible ones read after a short delay? (optional)
}
});
// click outside to close
document.addEventListener('click', function(e){
if (!document.getElementById('wk-notif').contains(e.target)){
panel.classList.remove('open');
bellBtn.setAttribute('aria-expanded','false');
panel.setAttribute('aria-hidden','true');
}
});
// keyboard close
document.addEventListener('keydown', function(e){
if (e.key==='Escape') {
panel.classList.remove('open');
bellBtn.setAttribute('aria-expanded','false');
panel.setAttribute('aria-hidden','true');
}
});
markAllBtn.addEventListener('click', function(){
markAllRead();
});
openAllBtn.addEventListener('click', function(){
// as contoh, redirect ke halaman notifikasi/premium
window.location.href = '/notifikasi';
});
// simple filter dropdown prompt (ke-sederhanaan supaya tidak perlu UI tambahan)
document.getElementById('wk-filter-btn').addEventListener('click', function(){
const choice = prompt('Filter notifikasi: ketik "all", "premium", "paid", atau "service"', currentFilter);
if (!choice) return;
const v = choice.trim().toLowerCase();
currentFilter = (['all','premium','paid','service'].includes(v)) ? v : 'all';
renderList(currentFilter);
});
/* =========== Init load/poll ========= */
if (MODE==='static'){
loadStatic();
} else {
loadRemote();
pollTimer = setInterval(loadRemote, NOTIF_POLL_INTERVAL);
}
// initial seen ids
items.forEach(i => seenIdsBefore.add(i.id));
// expose small API (opsional)
window.WarkasaNotif = {
push(item){
// item: {id,type,title,body,url,ts}
item.ts = item.ts || Date.now();
handleNewItems([item]);
},
markRead,
markAllRead,
getAll: ()=> items.slice()
};
// initial render after small delay
setTimeout(()=> renderList(currentFilter), 200);
})();
</script>
Penjelasan singkat & langkah integrasi
-
Mode penggunaan
-
MODE = 'static' → cocok kalau Anda ingin menaruh notifikasi manual (cepat).
-
MODE = 'remote' → cocok kalau Anda punya endpoint JSON (contoh: /api/notifications.json) yang mengembalikan array notifikasi { id, type, title, body, url, ts, source }.
-
Format JSON yang diharapkan (contoh)
Anda bisa sediakan endpoint (serverless, PHP, atau file statis) yang mengembalikan:
[
{ "id":"p1", "type":"premium", "title":"Judul Premium", "body":"Ringkasan", "url":"/premium/1", "ts":1698710400000, "source":"Warkasa" }
]
ts adalah timestamp milidetik.
-
Integrasi ke Blogger
-
Paste CSS ke <head> template.
-
Paste HTML di bagian header (mis. sebelum </header>).
-
Paste JS tepat sebelum </body>.
-
Jika Anda pakai Blogger yang membatasi fetch ke domain, letakkan JSON pada hosting Anda atau sebagai Data Feed via Google Script/endpoint yang mengizinkan CORS.
-
Menambahkan push asli (opsional)
-
Kustomisasi cepat
-
Ubah warna badge, ukuran, dan label chip lewat CSS.
-
Tambah ikon per jenis (premium = crown, paid = wallet, service = wrench) dengan menambahkan elemen sebelum judul di wk-item.
Di bawah ini kode yang telah upgrade versi sebelumnya menjadi:
✅ Langsung siap-pasang ke Blogger (Editor HTML / Tema → Edit HTML)
✅ Tanpa prompt — sudah diganti menjadi Dropdown Filter UI
✅ Sangat ringan (tanpa library eksternal)
✅ Responsif, modern, SEO-friendly
✅ Mendukung Premium / Berbayar / Jasa
✅ Bisa fetch data dari JSON (opsional–nanti saya bantu buat feed-nya)
Cukup copy-paste semua kode di bawah.
✅ CODE LONCENG NOTIFIKASI BLOGGER
Tempel CSS di <head>
<!-- 🛎️ Notifikasi Warkasa1919 - Blogger Ready -->
<style>
.wk-notif{font-family:system-ui;position:relative;display:inline-block;z-index:9999}
.wk-bell-btn{background:transparent;border:none;cursor:pointer;padding:6px;display:flex;align-items:center;border-radius:50%}
.wk-bell-svg{width:26px;height:26px}
.wk-badge{position:absolute;top:0;right:0;min-width:18px;height:18px;padding:0 5px;font-size:12px;line-height:18px;border-radius:50%;background:#e23b3b;color:#fff;display:none;align-items:center;justify-content:center}
.wk-panel{position:absolute;right:0;top:40px;width:320px;max-height:420px;background:#fff;border-radius:12px;box-shadow:0 8px 28px rgba(0,0,0,.15);overflow:hidden;display:none;flex-direction:column;border:1px solid rgba(0,0,0,.06)}
.wk-panel.open{display:flex}
.wk-header{padding:10px 12px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}
.wk-filter{font-size:13px;padding:6px;border:1px solid #ddd;border-radius:6px;background:#fff;cursor:pointer}
.wk-list{padding:8px;overflow:auto}
.wk-item{padding:8px;border-radius:8px;margin-bottom:6px;cursor:pointer;background:#fafafa}
.wk-item.unread{background:#fff2f2}
.wk-tag{font-size:10px;padding:2px 6px;border-radius:6px;background:#eee;margin-left:6px}
.wk-footer{padding:8px;border-top:1px solid #eee;text-align:right}
@media(max-width:480px){.wk-panel{right:8px;left:8px;width:auto}}
</style>
Tempel HTML pada navbar Blogger (di atas atau sebelum menu)
<div class="wk-notif" id="wkNotif">
<button class="wk-bell-btn" id="wkBell">
<svg class="wk-bell-svg" viewBox="0 0 24 24"><path d="M15 17H9a3 3 0 006 0z"/><path d="M18 16v-5a6 6 0 00-12 0v5l-2 2h16l-2-2z"/></svg>
<span class="wk-badge" id="wkBadge">0</span>
</button>
<div class="wk-panel" id="wkPanel">
<div class="wk-header">
<strong>Notifikasi</strong>
<select id="wkFilter" class="wk-filter">
<option value="all">Semua</option>
<option value="premium">Premium</option>
<option value="paid">Berbayar</option>
<option value="service">Jasa</option>
</select>
</div>
<div class="wk-list" id="wkList">Loading...</div>
<div class="wk-footer">
<button id="wkMarkAll">Tandai Semua Dibaca</button>
</div>
</div>
</div>
Tempel JS sebelum </body>
<script>
(function(){
const mode="static";
const endpoint="/api/notifications.json";
const badge=document.getElementById("wkBadge");
const bell=document.getElementById("wkBell");
const panel=document.getElementById("wkPanel");
const list=document.getElementById("wkList");
const filter=document.getElementById("wkFilter");
const markAll=document.getElementById("wkMarkAll");
let items=[],read=JSON.parse(localStorage.getItem("notif_read")||"{}");
const staticData=[
{id:"1",type:"premium",title:"Artikel Premium Baru",body:"Cara SEO Bisnis 2025",url:"#",ts:Date.now()},
{id:"2",type:"paid",title:"Diskon Jasa Penulisan",body:"Diskon 30% bulan ini",url:"#",ts:Date.now()},
{id:"3",type:"service",title:"Jasa Pembuatan Website",body:"Paket lengkap mulai 1jt",url:"#",ts:Date.now()}
];
function timeAgo(t){return Math.floor((Date.now()-t)/1000/60)+"m";}
function render(){
let f=filter.value;
let d=items.filter(x=>f==="all"||x.type===f);
list.innerHTML=d.map(x=>`
<div class="wk-item ${!read[x.id]?'unread':''}" onclick="location.href='${x.url}'">
<b>${x.title}</b><span class="wk-tag">${x.type}</span><br>
<small>${x.body}</small><br>
<small>${timeAgo(x.ts)} lalu</small>
</div>`).join("")||"<div style='padding:15px'>Tidak ada notifikasi</div>";
let u=items.filter(x=>!read[x.id]).length;
badge.style.display=u? "flex":"none";
badge.textContent=u;
}
function markAllRead(){
items.forEach(i=>read[i.id]=true);
localStorage.setItem("notif_read",JSON.stringify(read));
render();
}
bell.onclick=()=>panel.classList.toggle("open");
document.addEventListener("click",e=>{
if(!document.getElementById("wkNotif").contains(e.target))panel.classList.remove("open");
});
filter.onchange=render;
markAll.onclick=markAllRead;
function load(){
items=mode==="remote"?[]:staticData;
render();
}
load();
})();
</script>
Cara memasang di Blogger
-
Tema → Edit HTML
-
Tempel CSS di atas </head>
-
Tempel HTML di <header> atau navbar
-
Tempel Script sebelum </body>
-
Save
Hasil fitur
| Fitur |
Status |
| Responsive |
✅ |
| Dropdown filter |
✅ |
| Static + siap remote |
✅ |
| Lightweight |
✅ |
| Tekan lonceng → panel keluar |
✅ |
| Mark all read |
✅ |
Berikutnya adalah versi khusus 1-file (CSS + JS) yang langsung dipasang di <head> Blogger dan otomatis muncul di pojok kanan blog, jadi Anda tidak perlu menambah HTML di body.
📌 Fitur versi ini
✅ Langsung tempel di <head>
✅ UI lonceng fixed floating kanan atas
✅ Dropdown filter Premium / Berbayar / Jasa
✅ Ringan — tanpa library
✅ Mobile responsive
✅ Auto-inject tombol & panel
✅ Kode Tempel di <head> Blogger
Tema → Edit HTML → sebelum </head> paste ini
<!-- 🛎️ Warkasa1919 Notification Bell (Floating) -->
<style>
#wkBellWrap{position:fixed;top:20px;right:20px;z-index:9999}
.wk-notif{font-family:system-ui;position:relative;display:inline-block}
.wk-bell-btn{background:#fff;border:1px solid #ddd;cursor:pointer;padding:7px;border-radius:50%;box-shadow:0 4px 12px rgba(0,0,0,.15)}
.wk-bell-svg{width:26px;height:26px}
.wk-badge{position:absolute;top:-5px;right:-5px;min-width:18px;height:18px;padding:0 5px;font-size:12px;line-height:18px;border-radius:50%;background:#e23b3b;color:#fff;display:none;align-items:center;justify-content:center}
.wk-panel{position:fixed;top:70px;right:20px;width:320px;max-height:420px;background:#fff;border-radius:12px;box-shadow:0 8px 28px rgba(0,0,0,.18);overflow:hidden;display:none;flex-direction:column;border:1px solid rgba(0,0,0,.06);z-index:9999}
.wk-panel.open{display:flex}
.wk-header{padding:10px 12px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}
.wk-filter{font-size:13px;padding:6px;border:1px solid #ddd;border-radius:6px;background:#fff;cursor:pointer}
.wk-list{padding:8px;overflow:auto}
.wk-item{padding:8px;border-radius:8px;margin-bottom:6px;cursor:pointer;background:#fafafa}
.wk-item.unread{background:#fff2f2}
.wk-tag{font-size:10px;padding:2px 6px;border-radius:6px;background:#eee;margin-left:6px}
.wk-footer{padding:8px;border-top:1px solid #eee;text-align:right}
@media(max-width:480px){
#wkBellWrap{top:10px;right:10px}
.wk-panel{right:10px;left:10px;width:auto}
}
</style>
<script>
document.addEventListener("DOMContentLoaded",function(){
document.body.insertAdjacentHTML("beforeend",`
<div id="wkBellWrap">
<div class="wk-notif" id="wkNotif">
<button class="wk-bell-btn" id="wkBell">
<svg class="wk-bell-svg" viewBox="0 0 24 24"><path d="M15 17H9a3 3 0 006 0z"/><path d="M18 16v-5a6 6 0 00-12 0v5l-2 2h16l-2-2z"/></svg>
<span class="wk-badge" id="wkBadge">0</span>
</button>
</div>
</div>
<div class="wk-panel" id="wkPanel">
<div class="wk-header">
<strong>Notifikasi</strong>
<select id="wkFilter" class="wk-filter">
<option value="all">Semua</option>
<option value="premium">Premium</option>
<option value="paid">Berbayar</option>
<option value="service">Jasa</option>
</select>
</div>
<div class="wk-list" id="wkList">Loading...</div>
<div class="wk-footer"><button id="wkMarkAll">Tandai Dibaca</button></div>
</div>
`);
const mode="static"; // ganti "remote" jika sudah punya API
const staticData=[
{id:"1",type:"premium",title:"Artikel Premium Baru",body:"Strategi SEO Bisnis 2025",url:"#",ts:Date.now()},
{id:"2",type:"paid",title:"Diskon Jasa Konten",body:"Diskon 30% bulan ini",url:"#",ts:Date.now()},
{id:"3",type:"service",title:"Jasa Website Profesional",body:"Mulai 1jt full setup",url:"#",ts:Date.now()}
];
const badge=document.getElementById("wkBadge");
const bell=document.getElementById("wkBell");
const panel=document.getElementById("wkPanel");
const list=document.getElementById("wkList");
const filter=document.getElementById("wkFilter");
const markAll=document.getElementById("wkMarkAll");
let items=staticData;
let read=JSON.parse(localStorage.getItem("notif_read")||"{}");
function timeAgo(t){return Math.floor((Date.now()-t)/60000)+"m";}
function render(){
let f=filter.value;
let data=items.filter(x=>f==="all"||x.type===f);
list.innerHTML=data.map(x=>`
<div class="wk-item ${!read[x.id]?'unread':''}" onclick="location.href='${x.url}'">
<b>${x.title}</b><span class="wk-tag">${x.type}</span><br>
<small>${x.body}</small><br>
<small>${timeAgo(x.ts)} lalu</small>
</div>`).join("") || "<div style='padding:18px'>Tidak ada notifikasi</div>";
let unread=items.filter(x=>!read[x.id]).length;
badge.style.display=unread?"flex":"none";
badge.textContent=unread;
}
bell.onclick=()=>panel.classList.toggle("open");
document.addEventListener("click",e=>{if(!panel.contains(e.target)&&!bell.contains(e.target))panel.classList.remove("open")});
filter.onchange=render;
markAll.onclick=()=>{items.forEach(i=>read[i.id]=true);localStorage.setItem("notif_read",JSON.stringify(read));render()};
render();
});
</script>
✅ Hasil setelah dipasang
-
Lonceng muncul otomatis pojok kanan atas
-
Klik → panel notifikasi buka
-
Dropdown filter
-
“Tandai Dibaca” berfungsi
Berikutnya adalah versi siap-pasang yang otomatis mengambil posting dari feed Blogger (tanpa Anda edit data manual).
Fitur utama yang saya tambahkan:
-
Ambil feed Blogger via JSONP (bypass CORS, bekerja untuk domain Blogger & custom domain seperti warkasa1919.com)
-
Otomatis memetakan label/post tag ke jenis notifikasi: premium, paid (berbayar), service (jasa)
-
Menampilkan hanya posting yang berlabel sesuai (atau semua jika Anda mau)
-
Dedup, toast untuk item baru, persistensi read/unread di localStorage
-
Polling terjadwal (opsional) — interval default 2 menit (bisa diubah)
-
Siap-paste hanya di <head> Blogger (tidak perlu edit body)
Langkah pemasangan singkat:
-
Tema → Edit HTML → sebelum </head> paste seluruh kode di bawah.
-
Ganti FEED_URL hanya jika Anda ingin feed khusus; default sudah diarahkan ke https://warkasa1919.com/feeds/posts/default?alt=json-in-script.
-
Simpan. Lonceng akan otomatis mengambil posting bertag Premium, Berbayar, dan Jasa dan menampilkannya.
Kode — paste seluruh blok ini ke dalam <head> tema Blogger Anda
<!-- 🛎️ Warkasa1919 - Notifikasi otomatis dari Feed Blogger (JSONP) -->
<style>
/* minimal styling (floating bell + panel) */
#wkBellWrap{position:fixed;top:20px;right:20px;z-index:99999}
.wk-bell-btn{background:#fff;border:1px solid #ddd;cursor:pointer;padding:7px;border-radius:50%;box-shadow:0 6px 18px rgba(0,0,0,.12)}
.wk-bell-svg{width:26px;height:26px}
.wk-badge{position:absolute;top:-5px;right:-5px;min-width:18px;height:18px;padding:0 5px;font-size:12px;line-height:18px;border-radius:50%;background:#e23b3b;color:#fff;display:none;align-items:center;justify-content:center}
.wk-panel{position:fixed;top:70px;right:20px;width:360px;max-height:60vh;background:#fff;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.18);overflow:hidden;display:none;flex-direction:column;border:1px solid rgba(0,0,0,.06);z-index:99999}
.wk-panel.open{display:flex}
.wk-header{padding:10px 12px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}
.wk-filter{font-size:13px;padding:6px;border:1px solid #ddd;border-radius:6px;background:#fff;cursor:pointer}
.wk-list{padding:8px;overflow:auto}
.wk-item{padding:10px;border-radius:8px;margin-bottom:8px;cursor:pointer;background:#fafafa}
.wk-item.unread{background:#fff6f6}
.wk-item h4{margin:0;font-size:14px}
.wk-item p{margin:6px 0 0 0;font-size:13px;color:#444}
.wk-tag{font-size:10px;padding:3px 6px;border-radius:6px;background:#eee;margin-left:6px;vertical-align:middle}
.wk-footer{padding:8px;border-top:1px solid #eee;text-align:right}
@media(max-width:480px){
#wkBellWrap{top:10px;right:10px}
.wk-panel{right:10px;left:10px;width:auto}
}
</style>
<script>
/*
Warkasa1919 - Blogger Feed -> Notification (JSONP)
Paste this into <head>. Does JSONP requests to Blogger feed and maps labels:
- 'premium' => premium
- 'berbayar' or 'paid' => paid
- 'jasa' or 'service' => service
If a post has none of those labels, it's ignored (no notification).
Config below: FEED_URL, MAX_RESULTS, POLL_MS
*/
(function(){
/* ================== CONFIG ================== */
const FEED_URL = 'https://warkasa1919.com/feeds/posts/default?alt=json-in-script'; // default - ganti hanya jika perlu
const MAX_RESULTS = 20; // berapa post diambil tiap polling
const POLL_MS = 2 * 60 * 1000; // polling interval (ms) — default 2 menit
const AUTO_SHOW_TYPES = ['premium','paid','service']; // jenis yang ditampilkan
/* ============================================ */
/* inject base DOM (no need edit body) */
document.addEventListener('DOMContentLoaded', function(){
document.body.insertAdjacentHTML('beforeend', `
<div id="wkBellWrap">
<div class="wk-notif" id="wkNotif">
<button class="wk-bell-btn" id="wkBell" aria-label="Notifikasi">
<svg class="wk-bell-svg" viewBox="0 0 24 24"><path d="M15 17H9a3 3 0 006 0z"/><path d="M18 16v-5a6 6 0 00-12 0v5l-2 2h16l-2-2z"/></svg>
<span class="wk-badge" id="wkBadge">0</span>
</button>
</div>
</div>
<div class="wk-panel" id="wkPanel" role="dialog" aria-label="Daftar notifikasi">
<div class="wk-header">
<strong>Notifikasi</strong>
<select id="wkFilter" class="wk-filter" aria-label="Filter notifikasi">
<option value="all">Semua</option>
<option value="premium">Premium</option>
<option value="paid">Berbayar</option>
<option value="service">Jasa</option>
</select>
</div>
<div class="wk-list" id="wkList"><div style="padding:12px">Memuat...</div></div>
<div class="wk-footer"><button id="wkMarkAll">Tandai Semua Dibaca</button></div>
</div>
<div id="wkToastRoot" style="position:fixed;bottom:18px;right:18px;z-index:999999;pointer-events:none"></div>
`);
// DOM refs
const bell = document.getElementById('wkBell');
const panel = document.getElementById('wkPanel');
const list = document.getElementById('wkList');
const badge = document.getElementById('wkBadge');
const filter = document.getElementById('wkFilter');
const markAll = document.getElementById('wkMarkAll');
const toastRoot = document.getElementById('wkToastRoot');
// state
let items = []; // { id, type, title, body, url, ts, source }
let read = JSON.parse(localStorage.getItem('warkasa_notif_read_v1') || '{}');
let seenIds = new Set(JSON.parse(localStorage.getItem('warkasa_notif_seen_v1') || '[]'));
let pollingTimer = null;
let currentFilter = 'all';
/* helper: time ago */
function timeAgo(ts){
const s = Math.floor((Date.now()-ts)/1000);
if (s<60) return s + 's';
if (s<3600) return Math.floor(s/60)+'m';
if (s<86400) return Math.floor(s/3600)+'h';
return Math.floor(s/86400)+'d';
}
/* render list according to filter */
function render(){
const f = filter.value || currentFilter;
currentFilter = f;
const arr = items.filter(it => f==='all' ? true : it.type===f);
if (arr.length===0){
list.innerHTML = '<div style="padding:14px">Tidak ada notifikasi.</div>';
} else {
list.innerHTML = arr.map(it => {
const unread = !read[it.id];
return `<div class="wk-item ${unread?'unread':''}" data-id="${it.id}" role="button" tabindex="0">
<h4>${escapeHtml(it.title)} <span class="wk-tag">${escapeHtml(it.type)}</span></h4>
<p>${escapeHtml(it.body || '')}</p>
<small style="color:#666">${timeAgo(it.ts)} • ${escapeHtml(it.source||'Warkasa')}</small>
</div>`;
}).join('');
// attach click handlers
Array.from(list.querySelectorAll('.wk-item')).forEach(node => {
node.addEventListener('click', function(){
const id = this.getAttribute('data-id');
const it = items.find(x=>x.id===id);
if (!it) return;
markRead(id);
if (it.url) window.open(it.url, '_self');
});
});
}
updateBadge();
}
function updateBadge(){
const unread = items.filter(i=>!read[i.id]).length;
badge.style.display = unread ? 'flex' : 'none';
badge.textContent = unread>99 ? '99+' : unread;
}
function markRead(id){
read[id] = Date.now();
localStorage.setItem('warkasa_notif_read_v1', JSON.stringify(read));
render();
}
function markAllRead(){
items.forEach(i => read[i.id] = Date.now());
localStorage.setItem('warkasa_notif_read_v1', JSON.stringify(read));
render();
}
/* small HTML escape */
function escapeHtml(s){
if (!s) return '';
return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
}
/* toast for new item */
function showToast(title, type, url){
const node = document.createElement('div');
node.style.pointerEvents = 'auto';
node.style.marginTop = '8px';
node.style.padding = '12px 14px';
node.style.borderRadius = '10px';
node.style.boxShadow = '0 8px 20px rgba(0,0,0,.12)';
node.style.background = '#fff';
node.style.minWidth = '220px';
node.style.cursor = 'pointer';
node.innerHTML = `<strong style="display:block">${escapeHtml(title)}</strong><small style="color:#666">${escapeHtml(type)}</small>`;
node.addEventListener('click', ()=> { if (url) window.open(url,'_self'); });
toastRoot.appendChild(node);
setTimeout(()=> {
node.style.transition='opacity .5s, transform .5s';
node.style.opacity='0';
node.style.transform='translateY(8px)';
setTimeout(()=> node.remove(), 550);
}, 5500);
}
/* ================= JSONP fetch & parser for Blogger feed ================= */
// callback name must be global
window.WarkasaFeedCallback = function(json){
try {
const feed = json.feed || {};
const entries = feed.entry || [];
const parsed = entries.map(e => {
// id: prefer post id (g:id in feed) or link href
const id = e.gd$comments ? (e.id && e.id.$t) : (e.id && e.id.$t) || (e.link && (e.link.find(l=>l.rel==='alternate')||{}).href) || (e.title && e.title.$t);
// title
const title = e.title && e.title.$t || 'Tanpa Judul';
// content
const body = (e.summary && e.summary.$t) || (e.content && e.content.$t) || '';
// link
const alt = (e.link && e.link.find(l=>l.rel==='alternate')) || {};
const url = alt.href || '#';
// labels/categories
const cats = (e.category || []).map(c => (c.term || '').toLowerCase());
// detect type by labels
let type = null;
if (cats.some(t => t.includes('premium'))) type = 'premium';
else if (cats.some(t => t.includes('berbayar') || t.includes('paid'))) type = 'paid';
else if (cats.some(t => t.includes('jasa') || t.includes('service'))) type = 'service';
else type = 'other';
// published timestamp
const ts = e.published ? Date.parse(e.published.$t) : Date.now();
return { id: id+'', title, body: stripHtml(body).slice(0,200), url, type, ts, source: (feed.title && feed.title.$t) || 'Warkasa' };
});
// keep only desired types (configurable)
const filtered = parsed.filter(p => AUTO_SHOW_TYPES.includes(p.type));
// merge with existing items (avoid dup)
const map = {};
items.concat(filtered).forEach(it => { map[it.id] = it; });
items = Object.values(map).sort((a,b) => (b.ts||0)-(a.ts||0));
// toast for brand-new unseen items
filtered.forEach(it => {
if (!seenIds.has(it.id) && !read[it.id]) {
showToast(it.title, it.type, it.url);
}
seenIds.add(it.id);
});
// persist seen ids
localStorage.setItem('warkasa_notif_seen_v1', JSON.stringify(Array.from(seenIds)));
// initial render
render();
} catch (err) {
console.warn('WarkasaFeedCallback error', err);
}
};
// helper: strip html tags
function stripHtml(s){ return s ? s.replace(/(<([^>]+)>)/gi, '') : ''; }
// insert JSONP <script> to fetch feed
function fetchFeed(){
// build url: FEED_URL + params
const url = FEED_URL + (FEED_URL.indexOf('?')===-1 ? '?' : '&') + 'max-results=' + MAX_RESULTS + '&callback=WarkasaFeedCallback';
const s = document.createElement('script');
s.src = url + '&_=' + Date.now(); // cache-bust
s.async = true;
s.onerror = function(){ console.warn('Warkasa: feed load error'); s.remove(); };
s.onload = function(){ setTimeout(()=> s.remove(), 5000); };
document.body.appendChild(s);
}
// first load + polling
fetchFeed();
pollingTimer = setInterval(fetchFeed, POLL_MS);
/* ============== UI events ============== */
bell.addEventListener('click', ()=> panel.classList.toggle('open'));
document.addEventListener('click', function(e){
if (!document.getElementById('wkBellWrap').contains(e.target) && !panel.contains(e.target)) {
panel.classList.remove('open');
}
});
filter.addEventListener('change', render);
markAll.addEventListener('click', function(){ items.forEach(i => read[i.id] = Date.now()); localStorage.setItem('warkasa_notif_read_v1', JSON.stringify(read)); render(); });
// keyboard accessibility: close with Esc
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') panel.classList.remove('open'); });
// initial empty state
render();
}); // DOMContentLoaded end
})();
</script>
Penjelasan teknis singkat (penting)
-
Bagaimana pemetaan berkerja: skrip membaca category/label posting. Jika Anda menandai posting dengan label Premium, Berbayar (atau Paid), atau Jasa (atau Service), posting tadi akan masuk ke notifikasi dengan tipe sesuai. Label diperiksa case-insensitive. Jika Anda pakai label lain, sesuaikan AUTO_SHOW_TYPES atau atur label yang sesuai di dashboard Blogger.
-
JSONP: saya menggunakan alt=json-in-script (JSONP) + callback=WarkasaFeedCallback. Blogger menyediakan format ini, jadi tidak perlu backend tambahan dan tidak terblokir CORS.
-
Jika feed custom / subfolder: ganti FEED_URL di konfigurasi menjadi URL feed Anda (tetap tambahkan ?alt=json-in-script jika belum ada).
-
Interval polling: default 2 menit (POLL_MS). Kurangi/increase sesuai kebutuhan, tapi jangan terlalu sering agar tidak membebani server.
-
Penyimpanan: localStorage menyimpan read/seen agar pengguna tidak melihat toast berulang.
Itulah beberapa kode Lonceng Notifikasi Ringan & Responsif di Blogger yang bisa Anda pakai untuk mempercantik tampilan blog. Selamat mencoba!