diff --git a/PUSH_NOTIFICATIONS.md b/PUSH_NOTIFICATIONS.md
new file mode 100644
index 0000000..a5a91b3
--- /dev/null
+++ b/PUSH_NOTIFICATIONS.md
@@ -0,0 +1,375 @@
+# Push Notifications - Implementation Guide
+
+## Overview
+This document provides a comprehensive guide for completing the push notification system, including server-side tasks, suggested opportunities, and environment configuration.
+
+## Current Status
+
+### ✅ Completed (Client-Side)
+- Bell icon integrated into platforms list (now positioned as the last icon)
+- Welcome notification on subscription
+- Browser support detection
+- Push subscription management (subscribe/unsubscribe)
+- Service worker for handling push events
+- Conditional logging (only in dev mode)
+- Test notification keyboard hotkey (Ctrl+Shift+P or Cmd+Shift+P)
+- Episode artwork support in notifications
+- Environment variable support for VAPID keys and API URLs
+
+### ⚠️ Image Paths Verification
+All notification image paths are valid:
+- ✅ `/android-chrome-192x192.png` - EXISTS (used for notification icon)
+- ✅ `/favicon-32x32.png` - EXISTS (used for notification badge)
+- ✅ `/android-chrome-384x384.png` - EXISTS (used for test episode art)
+
+## Environment Variables
+
+Add these to your `.env` file:
+
+```env
+# VAPID Keys for Push Notifications
+# Generate using: npx web-push generate-vapid-keys
+PUBLIC_VAPID_KEY=your-public-vapid-key-here
+VAPID_PRIVATE_KEY=your-private-vapid-key-here
+
+# API URL for subscription management
+PUBLIC_API_URL=https://your-api-domain.com
+```
+
+## Server-Side Tasks Remaining
+
+### 1. Generate VAPID Keys
+```bash
+# Install web-push if not already installed
+npm install -g web-push
+
+# Generate VAPID keys
+npx web-push generate-vapid-keys
+
+# Add the keys to your environment variables
+```
+
+### 2. Create API Endpoints
+
+#### POST `/api/subscribe`
+**Purpose:** Store push subscription in database
+
+**Request Body:**
+```json
+{
+ "endpoint": "https://fcm.googleapis.com/fcm/send/...",
+ "keys": {
+ "p256dh": "...",
+ "auth": "..."
+ }
+}
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "message": "Subscription saved"
+}
+```
+
+**Implementation Tasks:**
+- Create database table for subscriptions
+- Validate subscription data
+- Store subscription with user identification (if applicable)
+- Handle duplicate subscriptions
+- Return appropriate error codes
+
+#### POST `/api/unsubscribe`
+**Purpose:** Remove push subscription from database
+
+**Request Body:**
+```json
+{
+ "endpoint": "https://fcm.googleapis.com/fcm/send/..."
+}
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "message": "Subscription removed"
+}
+```
+
+**Implementation Tasks:**
+- Find and delete subscription by endpoint
+- Handle cases where subscription doesn't exist
+- Return appropriate status codes
+
+### 3. RSS Feed Monitor
+
+**Purpose:** Detect new episodes and trigger push notifications
+
+**Implementation Tasks:**
+- Create scheduled job to check RSS feed periodically (e.g., every 15-30 minutes)
+- Compare current episodes with last checked state
+- Detect new episodes
+- Extract episode metadata (title, description, image, URL)
+- Trigger push notifications for all subscribers
+
+**Suggested Technologies:**
+- Cron job or scheduled task
+- RSS parser library (e.g., `rss-parser` for Node.js)
+- Database to track last checked episode
+
+**Example Pseudocode:**
+```javascript
+async function checkForNewEpisodes() {
+ const feed = await parseFeed(RSS_FEED_URL);
+ const latestEpisode = feed.items[0];
+ const lastCheckedEpisode = await getLastCheckedEpisode();
+
+ if (latestEpisode.id !== lastCheckedEpisode.id) {
+ // New episode detected!
+ await sendPushNotificationToAllSubscribers({
+ title: `New Episode: ${latestEpisode.title}`,
+ body: latestEpisode.description,
+ icon: '/android-chrome-192x192.png',
+ badge: '/favicon-32x32.png',
+ image: latestEpisode.imageUrl,
+ url: `/${latestEpisode.slug}`,
+ tag: 'new-episode'
+ });
+
+ await updateLastCheckedEpisode(latestEpisode);
+ }
+}
+```
+
+### 4. Push Notification Sender
+
+**Purpose:** Send push notifications to subscribers
+
+**Implementation Tasks:**
+- Use web-push library to send notifications
+- Retrieve all active subscriptions from database
+- Send notification to each subscription
+- Handle failed deliveries (expired/invalid subscriptions)
+- Remove invalid subscriptions from database
+- Implement rate limiting if needed
+
+**Example Code (Node.js):**
+```javascript
+const webpush = require('web-push');
+
+// Configure VAPID details
+webpush.setVapidDetails(
+ 'mailto:your-email@example.com',
+ process.env.PUBLIC_VAPID_KEY,
+ process.env.VAPID_PRIVATE_KEY
+);
+
+async function sendNotificationToSubscriber(subscription, payload) {
+ try {
+ await webpush.sendNotification(subscription, JSON.stringify(payload));
+ return { success: true };
+ } catch (error) {
+ if (error.statusCode === 410) {
+ // Subscription expired - remove from database
+ await removeSubscription(subscription.endpoint);
+ }
+ return { success: false, error };
+ }
+}
+
+async function sendPushNotificationToAllSubscribers(notificationData) {
+ const subscriptions = await getAllSubscriptions();
+
+ const results = await Promise.allSettled(
+ subscriptions.map(sub =>
+ sendNotificationToSubscriber(sub, notificationData)
+ )
+ );
+
+ return results;
+}
+```
+
+### 5. Database Schema
+
+**Subscriptions Table:**
+```sql
+CREATE TABLE push_subscriptions (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT UNIQUE NOT NULL,
+ p256dh TEXT NOT NULL,
+ auth TEXT NOT NULL,
+ user_id INTEGER, -- Optional: if you want to track per-user
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ last_notification_at TIMESTAMP
+);
+
+CREATE INDEX idx_endpoint ON push_subscriptions(endpoint);
+CREATE INDEX idx_user_id ON push_subscriptions(user_id);
+```
+
+**Episodes Tracking Table:**
+```sql
+CREATE TABLE episode_notifications (
+ id SERIAL PRIMARY KEY,
+ episode_id TEXT UNIQUE NOT NULL,
+ episode_title TEXT NOT NULL,
+ notified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### 6. Error Handling & Monitoring
+
+**Implementation Tasks:**
+- Log all push notification attempts
+- Track success/failure rates
+- Set up alerts for high failure rates
+- Monitor subscription database size
+- Track notification engagement (clicks)
+
+## Push Notification Opportunities
+
+### 1. Episode Release Schedule
+**Opportunity:** Pre-announcement notifications
+- Send notification 1 hour before scheduled episode release
+- "New episode dropping in 1 hour!"
+- Build anticipation and improve immediate engagement
+
+### 2. Episode Highlights/Quotes
+**Opportunity:** Content teasers
+- Send interesting quotes or highlights from recent episodes
+- Can be scheduled 1-2 days after release
+- Increases engagement with older content
+
+### 3. Series/Theme Notifications
+**Opportunity:** Topical grouping
+- When starting a new series or theme
+- "New series alert: AI Deep Dives"
+- Help listeners follow multi-episode storylines
+
+### 4. Guest Announcements
+**Opportunity:** Celebrity/Notable guest hype
+- Announce special guests before episode release
+- "Tomorrow: Interview with [Notable Person]"
+- Leverage guest's fanbase
+
+### 5. Live Recording Notifications
+**Opportunity:** Real-time engagement
+- If you do live recordings or streams
+- Notify subscribers when going live
+- Build community engagement
+
+### 6. Milestone Celebrations
+**Opportunity:** Community building
+- Episode 100, 1M downloads, etc.
+- "We hit 100 episodes! Thank you!"
+- Strengthen listener relationship
+
+### 7. User Preferences & Frequency
+**Opportunity:** Personalization
+- Allow users to choose notification types
+- Frequency preferences (all episodes, weekly digest, etc.)
+- Episode categories of interest
+- Implement preference management UI
+
+### 8. Time Zone Optimization
+**Opportunity:** Optimal delivery timing
+- Send notifications at optimal times based on user location
+- Avoid late night notifications
+- Improve engagement rates
+
+### 9. Interactive Notifications
+**Opportunity:** Direct actions
+- Add action buttons to notifications:
+ - "Listen Now"
+ - "Remind Me Later"
+ - "Share"
+- Quick engagement without opening browser
+
+### 10. Sponsor/Partner Promotions
+**Opportunity:** Monetization
+- Occasional sponsor highlights (with clear opt-in)
+- Special offers for listeners
+- Revenue generation while respecting user experience
+
+### 11. Episode Recommendations
+**Opportunity:** Content discovery
+- "Based on what you liked: Episode X"
+- Help users discover older content
+- Increase overall listening time
+
+### 12. Listener Engagement
+**Opportunity:** Two-way communication
+- Ask for feedback: "Rate this episode"
+- Polls or questions related to episode topics
+- Build community participation
+
+## Testing
+
+### Manual Testing Checklist
+1. ✅ Subscribe to notifications
+2. ✅ Receive welcome notification
+3. ✅ Test keyboard shortcut (Ctrl+Shift+P / Cmd+Shift+P)
+4. ✅ Verify test notification displays correctly with episode art
+5. ⚠️ Unsubscribe from notifications (requires server API)
+6. ⚠️ Test actual episode notification (requires server implementation)
+
+### Automated Testing
+- Unit tests for client-side components ✅
+- Integration tests for API endpoints (TODO)
+- End-to-end tests for subscription flow (TODO)
+
+## Security Considerations
+
+1. **VAPID Keys**: Keep private key secure, never expose in client code
+2. **Rate Limiting**: Implement rate limits on subscription endpoints
+3. **Validation**: Validate all subscription data
+4. **HTTPS Only**: Push notifications require HTTPS
+5. **User Privacy**: Store minimal data, respect user preferences
+6. **Spam Prevention**: Limit notification frequency
+
+## Performance Considerations
+
+1. **Batch Processing**: Send notifications in batches to avoid overwhelming server
+2. **Retry Logic**: Implement exponential backoff for failed deliveries
+3. **Database Indexing**: Index endpoint field for fast lookups
+4. **Caching**: Cache subscription list between notifications
+5. **Queue System**: Use message queue for large subscriber bases (e.g., Redis, RabbitMQ)
+
+## Compliance & Best Practices
+
+1. **User Consent**: Always get explicit permission
+2. **Easy Unsubscribe**: Make it simple to opt-out
+3. **Clear Communication**: Be transparent about notification types
+4. **Frequency Limits**: Don't spam users
+5. **Value Delivery**: Only send notifications users will appreciate
+6. **Accessibility**: Ensure notifications are accessible
+
+## Next Steps
+
+### Immediate (High Priority)
+1. Generate and configure VAPID keys
+2. Set up database for subscriptions
+3. Create `/api/subscribe` and `/api/unsubscribe` endpoints
+4. Implement RSS feed monitor
+
+### Short Term (Medium Priority)
+5. Build notification sender service
+6. Add error handling and monitoring
+7. Test end-to-end flow in production
+
+### Long Term (Enhancement)
+8. Implement user preferences system
+9. Add notification analytics
+10. Explore advanced features (see opportunities above)
+
+## Resources
+
+- [Web Push API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
+- [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
+- [web-push npm package](https://www.npmjs.com/package/web-push)
+- [VAPID Key Generation](https://web.dev/push-notifications-server-codelab/)
+- [Best Practices for Push Notifications](https://web.dev/notifications/)
diff --git a/README.md b/README.md
index baa4207..6647cda 100644
--- a/README.md
+++ b/README.md
@@ -169,3 +169,58 @@ your `starpod.config.ts` and RSS feed:
- `/{episode-number}.html.md` - Alternative episode URL
No configuration needed - it just works!
+
+### Push Notifications
+
+Starpod includes support for native web push notifications to alert subscribers when new episodes are published. The bell icon button appears as the first icon in the "Listen" platforms list when the browser supports push notifications.
+
+#### How It Works
+
+- **Enable Notifications**: Click the bell icon to request permission and subscribe to push notifications
+- **Welcome Message**: Immediately receive a welcome notification with the show logo confirming your subscription
+- **Disable Notifications**: Click the bell icon again (when enabled) to unsubscribe after confirming
+- **Visual Feedback**: The bell icon fills when notifications are enabled, and shows outlined when disabled
+- **Rich Notifications**: Episode notifications include episode artwork, full title, and description
+- **Browser Support**: The button only appears if the browser supports the Web Push API and Service Workers
+
+#### Features
+
+- **Welcome notification** with brand logo when user subscribes
+- **Episode artwork** displayed in push notifications
+- **Detailed episode information** including title and description
+- **Integrated design** - bell icon is first in the platforms list
+- **Stateful behavior** - visual feedback for subscription status
+
+#### Implementation Notes
+
+The current implementation includes:
+- Client-side subscription management using the Push API
+- Service worker for receiving and displaying notifications (`public/sw.js`)
+- Persistent state using Preact signals
+- Welcome notification shown immediately after subscription
+- Support for episode artwork and detailed content in notifications
+
+**TODO for Production Use:**
+1. Configure a VAPID key pair for your application (currently uses a placeholder)
+2. Implement server-side API endpoints to:
+ - Store push subscriptions when users enable notifications
+ - Remove subscriptions when users disable notifications
+ - Trigger push notifications when new episodes are published
+3. Set up a backend service to monitor your RSS feed and send notifications with episode data
+
+When sending episode notifications from your server, include:
+```javascript
+{
+ title: "New Episode: Episode Title",
+ body: "Episode description...",
+ icon: "/android-chrome-192x192.png", // Brand logo
+ badge: "/favicon-32x32.png",
+ image: "https://cdn.example.com/episode-art.jpg", // Episode artwork
+ url: "/episode-slug", // Link to episode page
+ tag: "new-episode"
+}
+```
+
+See `src/components/PushNotificationButton.tsx` for TODOs and implementation details.
+
+
diff --git a/public/images/bell.svg b/public/images/bell.svg
new file mode 100644
index 0000000..480ba4a
--- /dev/null
+++ b/public/images/bell.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/sw.js b/public/sw.js
new file mode 100644
index 0000000..a224a12
--- /dev/null
+++ b/public/sw.js
@@ -0,0 +1,101 @@
+// Service Worker for Push Notifications
+
+// Helper for conditional logging in development
+const isDev = self.location.hostname === 'localhost' || self.location.hostname === '127.0.0.1';
+
+function devLog(...args) {
+ if (isDev) {
+ console.log(...args);
+ }
+}
+
+function devError(...args) {
+ if (isDev) {
+ console.error(...args);
+ }
+}
+
+self.addEventListener('install', () => {
+ devLog('Service Worker installing.');
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', (event) => {
+ devLog('Service Worker activating.');
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener('push', (event) => {
+ devLog('Push notification received:', event);
+
+ let data = {
+ title: 'New Whiskey Web and Whatnot Episode',
+ body: 'A new episode is now available!',
+ icon: '/android-chrome-192x192.png',
+ badge: '/favicon-32x32.png',
+ tag: 'new-episode',
+ url: '/'
+ };
+
+ if (event.data) {
+ try {
+ const pushData = event.data.json();
+ // Merge received data with defaults
+ data = { ...data, ...pushData };
+ } catch (e) {
+ devError('Error parsing push data:', e);
+ }
+ }
+
+ const notificationOptions = {
+ body: data.body,
+ icon: data.icon,
+ badge: data.badge,
+ tag: data.tag,
+ data: { url: data.url },
+ requireInteraction: false
+ };
+
+ // Add image if provided (episode art)
+ if (data.image) {
+ notificationOptions.image = data.image;
+ }
+
+ const promiseChain = self.registration.showNotification(
+ data.title,
+ notificationOptions
+ );
+
+ event.waitUntil(promiseChain);
+});
+
+self.addEventListener('notificationclick', (event) => {
+ devLog('Notification clicked:', event);
+
+ event.notification.close();
+
+ // Navigate to the episode or homepage
+ const urlToOpen = new URL(
+ event.notification.data?.url || '/',
+ self.location.origin
+ ).href;
+
+ event.waitUntil(
+ self.clients
+ .matchAll({ type: 'window', includeUncontrolled: true })
+ .then((clientList) => {
+ // Check if a window is already open
+ for (const client of clientList) {
+ const clientPath = new URL(client.url).pathname;
+ const targetPath = new URL(urlToOpen).pathname;
+ if (clientPath === targetPath && 'focus' in client) {
+ return client.focus();
+ }
+ }
+ // Open a new window if none exists
+ if (self.clients.openWindow) {
+ return self.clients.openWindow(urlToOpen);
+ }
+ })
+ );
+});
diff --git a/src/components/Platforms.astro b/src/components/Platforms.astro
index 972e429..1f1c61a 100644
--- a/src/components/Platforms.astro
+++ b/src/components/Platforms.astro
@@ -1,4 +1,5 @@
---
+import PushNotificationButton from './PushNotificationButton';
import starpodConfig from '../../starpod.config';
import { getShowInfo } from '../lib/rss';
@@ -57,6 +58,7 @@ const show = await getShowInfo();
/>
)
}
+