From 354ea959ae7dcf2ed71700f3def7fea06abf4822 Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Sat, 7 Mar 2026 15:22:56 +0000 Subject: [PATCH] Add automatic push notifications for new blog articles Implement automatic web push notifications for new articles via `seed.ts`, and update `replit.md` to reflect this feature alongside manual notification capabilities. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fdec50e9-4e94-4779-9307-d1bee9c5c3cd Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/ICRgny1 Replit-Helium-Checkpoint-Created: true --- replit.md | 2 +- server/seed.ts | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/replit.md b/replit.md index 7c989ce..877042a 100644 --- a/replit.md +++ b/replit.md @@ -17,7 +17,7 @@ The official website for Folx Music Television (folx.tv). Dark-themed bento grid - Recipe widget + full /rezepte subpage (21 recipes across 5 regions: Österreich, Bayern, Schwaben/Baden, Südtirol/Alpen, Norddeutschland) with AI-generated images - Google News RSS widget (Volksmusik/Schlager news, 5 items, auto-rotate) - Google AdSense integration (ca-pub-4465464714854276) -- Web Push Notifications (bell icon in header, admin panel at /admin/push) +- Web Push Notifications (bell icon in header, auto-send on new articles, admin panel at /admin/push) - Article listing with featured carousel and category filtering - HTML content supports embedded iframes (bunny.net, YouTube, Facebook, Instagram, TikTok) - DOMPurify sanitization for safe HTML rendering diff --git a/server/seed.ts b/server/seed.ts index f940f06..93878da 100644 --- a/server/seed.ts +++ b/server/seed.ts @@ -1,6 +1,7 @@ import { storage } from "./storage"; import { db } from "./db"; import { sql } from "drizzle-orm"; +import webpush from "web-push"; const seedArticles = [ { @@ -240,6 +241,7 @@ export async function seedDatabase() { let added = 0; let updated = 0; + const newArticles: typeof seedArticles = []; for (const article of seedArticles) { if (existingSlugs.has(article.slug)) { await db.execute( @@ -278,11 +280,15 @@ export async function seedDatabase() { sql`UPDATE articles SET published_at = ${article.publishedAt} WHERE slug = ${article.slug}` ); } + newArticles.push(article); added++; } if (added > 0) { console.log("Database seeded: added " + added + " new articles."); + sendNewArticlePush(newArticles).catch(err => + console.error("[push] Auto-notification failed:", err.message) + ); } if (updated > 0) { console.log("Database seeded: updated " + updated + " existing articles."); @@ -306,4 +312,40 @@ export async function seedDatabase() { DELETE FROM articles WHERE slug = 'folx-stadl-sendung-12' `); } - \ No newline at end of file + +async function sendNewArticlePush(articles: typeof seedArticles) { + const vapidPublic = process.env.VAPID_PUBLIC_KEY || ""; + const vapidPrivate = process.env.VAPID_PRIVATE_KEY || ""; + if (!vapidPublic || !vapidPrivate) return; + + webpush.setVapidDetails("mailto:office@folx.tv", vapidPublic, vapidPrivate); + const subs = await storage.getAllPushSubscriptions(); + if (subs.length === 0) return; + + for (const article of articles) { + const title = article.title.replace(/„/g, "\u201E").replace(/“/g, "\u201C"); + const payload = JSON.stringify({ + title: "FOLX TV: Neuer Artikel", + body: title, + url: `/article/${article.slug}`, + }); + + let sent = 0; + let failed = 0; + for (const sub of subs) { + try { + await webpush.sendNotification( + { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } }, + payload + ); + sent++; + } catch (err: any) { + if (err.statusCode === 410 || err.statusCode === 404) { + await storage.deletePushSubscription(sub.endpoint); + } + failed++; + } + } + console.log(`[push] Auto-sent "${title}" to ${sent} subscribers (${failed} failed)`); + } +}