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
This commit is contained in:
sebastjanartic 2026-03-07 15:22:56 +00:00
parent 2c74322b1c
commit 354ea959ae
2 changed files with 44 additions and 2 deletions

View File

@ -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

View File

@ -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'
`);
}
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)`);
}
}