diff --git a/email-templates/README.md b/email-templates/README.md new file mode 100644 index 00000000..0c1e4892 --- /dev/null +++ b/email-templates/README.md @@ -0,0 +1,33 @@ +# Email Templates for Newsletter Tiers + +This directory contains HTML email templates for the four newsletter subscription tiers: + +1. **research-updates.html** - Monthly research insights (scholarly audience) +2. **implementation-notes.html** - Bi-weekly implementation guidance (practitioners) +3. **governance-discussions.html** - Sporadic values/governance topics (stakeholders) +4. **project-updates.html** - Quarterly project milestones (general audience) + +## Template Variables + +All templates use Mustache-style {{variables}} for dynamic content: + +- `{{name}}` - Subscriber name +- `{{unsubscribe_link}}` - Unsubscribe URL +- `{{preferences_link}}` - Preferences management URL +- `{{website_link}}` - https://agenticgovernance.digital +- `{{docs_link}}` - https://agenticgovernance.digital/docs.html +- `{{github_link}}` - https://github.com/AgenticGovernance/tractatus-framework +- Content-specific variables documented in each template + +## Usage + +Templates are loaded by the newsletter admin UI and processed with a template engine (e.g., Handlebars, Mustache) before sending via email service. + +## Design Principles + +- Inline CSS for email client compatibility +- Responsive design (mobile-friendly) +- Accessible (semantic HTML, sufficient contrast) +- Brand-consistent (blue gradient header, clean typography) +- Clear CTA buttons with good hit targets +- Unsubscribe link always visible in footer diff --git a/email-templates/TEMPLATE_SPECS.md b/email-templates/TEMPLATE_SPECS.md new file mode 100644 index 00000000..7f7c5246 --- /dev/null +++ b/email-templates/TEMPLATE_SPECS.md @@ -0,0 +1,293 @@ +# Newsletter Email Template Specifications + +## Overview + +Four newsletter tiers with distinct audiences, frequencies, and content focus. + +--- + +## 1. Research Updates (Monthly) + +**Audience**: Researchers, academics, AI safety professionals +**Frequency**: Monthly (1st of each month) +**Tone**: Scholarly, rigorous, honest about uncertainties + +### Structure + +``` +HEADER + - Blue gradient background (#2563eb to #1e40af) + - Title: "Tractatus Research Updates" + - Subtitle: "Monthly insights from AI governance research" + +GREETING + - "Hi {{name}}," + +RESEARCH HIGHLIGHTS (2-3 items) + - Each highlight in a card: + - Title + - 2-3 sentence summary + - "Read More" button linking to full blog post + +KEY FINDINGS + - Bulleted list (3-5 items) + - Empirical observations from the month + - Focus on what WORKED and what DIDN'T + +OPEN QUESTIONS + - 2-3 areas where we're still uncertain + - Invitation for feedback/collaboration + - Link to feedback form + +CALL TO ACTION + - "Explore Our Research" button → /blog + +FOOTER + - Brand info + - Links: Website | Documentation | GitHub + - Unsubscribe | Update Preferences +``` + +### Template Variables +- `{{name}}` - Subscriber name +- `{{highlight_1_title}}`, `{{highlight_1_summary}}`, `{{highlight_1_link}}` +- `{{highlight_2_title}}`, `{{highlight_2_summary}}`, `{{highlight_2_link}}` +- `{{finding_1}}`, `{{finding_2}}`, `{{finding_3}}` +- `{{question_1}}`, `{{question_2}}` +- `{{feedback_link}}` - /feedback +- `{{blog_link}}` - /blog +- `{{unsubscribe_link}}`, `{{preferences_link}}` + +--- + +## 2. Implementation Notes (Bi-weekly) + +**Audience**: Practitioners, developers, AI engineers +**Frequency**: Bi-weekly (1st and 15th) +**Tone**: Practical, code-focused, honest about trade-offs + +### Structure + +``` +HEADER + - Title: "Tractatus Implementation Notes" + - Subtitle: "Practical patterns for governed AI systems" + +GREETING + - "Hi {{name}}," + +IMPLEMENTATION SPOTLIGHT + - Featured pattern or technique + - Why it matters + - Code snippet or example + - "View Full Example" button + +QUICK WINS (3-4 items) + - Short, actionable tips + - 1-2 sentences each + - Link to detailed docs + +GOTCHAS & TRADE-OFFS + - Common pitfalls + - Known limitations + - When NOT to use a pattern + +COMMUNITY SPOTLIGHT (optional) + - Highlight community implementation + - Link to GitHub PR or discussion + +CALL TO ACTION + - "Explore Patterns" button → /implementer.html + +FOOTER + - Standard footer +``` + +### Template Variables +- `{{name}}` +- `{{spotlight_title}}`, `{{spotlight_why}}`, `{{spotlight_code}}`, `{{spotlight_link}}` +- `{{tip_1}}`, `{{tip_2}}`, `{{tip_3}}`, `{{tip_4}}` +- `{{gotcha_1}}`, `{{gotcha_2}}` +- `{{community_name}}`, `{{community_description}}`, `{{community_link}}` (optional) +- `{{patterns_link}}` - /implementer.html +- `{{unsubscribe_link}}`, `{{preferences_link}}` + +--- + +## 3. Governance Discussions (Sporadic) + +**Audience**: Stakeholders, policy makers, ethicists +**Frequency**: As needed (values/governance topics arise) +**Tone**: Inclusive, deliberative, pluralistic + +### Structure + +``` +HEADER + - Title: "Tractatus Governance Discussion" + - Subtitle: "Values-sensitive topics requiring deliberation" + +GREETING + - "Hi {{name}}," + +TOPIC INTRODUCTION + - What's the issue? + - Why does it matter? + - Who's affected? + +CURRENT THINKING + - Our current position (tentative) + - Reasoning and constraints + - Acknowledged trade-offs + +OPEN FOR DELIBERATION + - Specific questions for community input + - Timeline for feedback + - How to participate + +RELATED PERSPECTIVES + - Links to relevant discussions + - Academic references + - Community viewpoints + +CALL TO ACTION + - "Join the Discussion" button → Specific deliberation thread + +FOOTER + - Standard footer +``` + +### Template Variables +- `{{name}}` +- `{{topic_title}}`, `{{topic_description}}`, `{{topic_why}}`, `{{topic_affected}}` +- `{{current_position}}`, `{{reasoning}}`, `{{tradeoffs}}` +- `{{question_1}}`, `{{question_2}}`, `{{question_3}}` +- `{{deadline}}`, `{{participate_link}}` +- `{{perspective_1_title}}`, `{{perspective_1_link}}` +- `{{perspective_2_title}}`, `{{perspective_2_link}}` +- `{{discussion_link}}` +- `{{unsubscribe_link}}`, `{{preferences_link}}` + +--- + +## 4. Project Updates (Quarterly) + +**Audience**: General audience, supporters, curious observers +**Frequency**: Quarterly (Jan 1, Apr 1, Jul 1, Oct 1) +**Tone**: Accessible, transparent, narrative-driven + +### Structure + +``` +HEADER + - Title: "Tractatus Project Update" + - Subtitle: "Q{{quarter}} {{year}} - Where We've Been, Where We're Going" + +GREETING + - "Hi {{name}}," + +QUARTER IN REVIEW + - Narrative summary (3-4 paragraphs) + - Major milestones + - What we learned + - What surprised us + +BY THE NUMBERS + - Stats in visual cards: + - New features shipped + - Community contributions + - Documentation additions + - Feedback items processed + +WHAT'S NEXT + - Upcoming priorities + - Timeline (realistic, not aspirational) + - How community can help + +FEATURED CONTENT + - 2-3 highlighted blog posts or docs + - Brief descriptions + - "Read More" buttons + +COMMUNITY SHOUTOUTS (optional) + - Thank contributors + - Highlight collaborations + +CALL TO ACTION + - "Explore the Framework" button → /index.html + +FOOTER + - Standard footer +``` + +### Template Variables +- `{{name}}` +- `{{quarter}}`, `{{year}}` +- `{{review_paragraph_1}}`, `{{review_paragraph_2}}`, `{{review_paragraph_3}}` +- `{{features_count}}`, `{{contributions_count}}`, `{{docs_count}}`, `{{feedback_count}}` +- `{{priority_1}}`, `{{priority_2}}`, `{{priority_3}}` +- `{{timeline}}`, `{{how_to_help}}` +- `{{featured_1_title}}`, `{{featured_1_desc}}`, `{{featured_1_link}}` +- `{{featured_2_title}}`, `{{featured_2_desc}}`, `{{featured_2_link}}` +- `{{shoutouts}}` (optional HTML block) +- `{{explore_link}}` - /index.html +- `{{unsubscribe_link}}`, `{{preferences_link}}` + +--- + +## Design Guidelines + +### Colors +- Primary Blue: #2563eb +- Dark Blue: #1e40af +- Text: #1f2937 +- Gray text: #6b7280 +- Background: #f9fafb +- White: #ffffff + +### Typography +- Font family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif +- Body: 16px, line-height 1.6 +- Headings: 20-28px, font-weight 600-700 + +### Layout +- Max width: 600px +- Padding: 20-40px +- Mobile-responsive +- Clear visual hierarchy + +### Buttons +- Background: #2563eb +- Text: #ffffff +- Padding: 12px 24px +- Border-radius: 6px +- Font-weight: 600 + +### Cards/Highlights +- Background: #f9fafb +- Border-left: 4px solid #2563eb +- Padding: 15px +- Margin-bottom: 15px + +--- + +## Implementation Notes + +1. **Email Service**: Use SendGrid, Mailgun, or AWS SES for sending +2. **Template Engine**: Handlebars or Mustache for variable substitution +3. **Testing**: Use Litmus or Email on Acid for client compatibility testing +4. **Tracking**: Include open tracking pixel and click tracking (with disclosure) +5. **Unsubscribe**: MUST be one-click (CAN-SPAM compliance) +6. **Plain Text**: Generate plain-text versions for all templates +7. **Preheader Text**: Add compelling preheader for inbox preview + +--- + +## Next Steps + +1. Create HTML templates from these specs +2. Build template editor in admin UI +3. Integrate with email sending service +4. Set up automated sending schedule +5. Create analytics dashboard for open rates, click rates, unsubscribes + diff --git a/public/about.html b/public/about.html index c089cb45..b3739de4 100644 --- a/public/about.html +++ b/public/about.html @@ -385,5 +385,8 @@ + + + diff --git a/public/about/values.html b/public/about/values.html index 124553df..07f3f965 100644 --- a/public/about/values.html +++ b/public/about/values.html @@ -475,5 +475,7 @@ + <\!-- Feedback System (Governed by Tractatus + Agent Lightning) --> + diff --git a/public/admin/newsletter-management.html b/public/admin/newsletter-management.html index c52d27af..2bff5a11 100644 --- a/public/admin/newsletter-management.html +++ b/public/admin/newsletter-management.html @@ -37,10 +37,68 @@ + +
+

Send Newsletter

+ +
+ +
+ + +

Will send to all subscribers interested in this tier

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +

+ JSON object with template variables. + See template specs +

+
+ + +
+ + + +
+ + + +
+
+
-

Actions

+

Subscriber Actions

+
  • GitHub
  • diff --git a/public/js/components/newsletter.js b/public/js/components/newsletter.js new file mode 100644 index 00000000..c0d2a5c0 --- /dev/null +++ b/public/js/components/newsletter.js @@ -0,0 +1,316 @@ +/** + * Newsletter Subscription Component + * Reusable modal for newsletter subscriptions across the website + */ + +class TractausNewsletter { + constructor() { + this.isOpen = false; + this.isMobile = window.matchMedia('(max-width: 768px)').matches; + this.csrfToken = null; + this.init(); + } + + async init() { + this.renderModal(); + this.attachEventListeners(); + await this.fetchCsrfToken(); + } + + async fetchCsrfToken() { + try { + const response = await fetch('/api/csrf-token'); + const data = await response.json(); + this.csrfToken = data.csrfToken; + } catch (error) { + console.error('[Newsletter] Failed to fetch CSRF token:', error); + } + } + + renderModal() { + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + } + + attachEventListeners() { + const modal = document.getElementById('newsletter-modal'); + const backdrop = document.getElementById('newsletter-backdrop'); + const closeBtn = document.getElementById('newsletter-close'); + const form = document.getElementById('newsletter-form'); + const doneBtn = document.getElementById('newsletter-done-btn'); + const retryBtn = document.getElementById('newsletter-retry-btn'); + + // Close handlers + backdrop.addEventListener('click', () => this.close()); + closeBtn.addEventListener('click', () => this.close()); + doneBtn.addEventListener('click', () => this.close()); + retryBtn.addEventListener('click', () => this.showStep('form')); + + // Form submission + form.addEventListener('submit', (e) => { + e.preventDefault(); + this.handleSubmit(e); + }); + + // Escape key to close + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.isOpen) { + this.close(); + } + }); + + // Global trigger - look for elements with data-newsletter-trigger + document.addEventListener('click', (e) => { + if (e.target.closest('[data-newsletter-trigger]')) { + e.preventDefault(); + this.open(); + } + }); + } + + open() { + const modal = document.getElementById('newsletter-modal'); + modal.classList.remove('hidden'); + this.isOpen = true; + document.body.classList.add('overflow-hidden'); + this.showStep('form'); + } + + close() { + const modal = document.getElementById('newsletter-modal'); + modal.classList.add('hidden'); + this.isOpen = false; + document.body.classList.remove('overflow-hidden'); + + // Reset form + const form = document.getElementById('newsletter-form'); + form.reset(); + + // Re-check the research interest by default + const researchCheckbox = form.querySelector('input[value="research"]'); + if (researchCheckbox) { + researchCheckbox.checked = true; + } + } + + showStep(step) { + const steps = { + form: 'newsletter-form-step', + success: 'newsletter-success-step', + error: 'newsletter-error-step' + }; + + Object.values(steps).forEach(stepId => { + const el = document.getElementById(stepId); + if (el) el.classList.add('hidden'); + }); + + const targetStep = document.getElementById(steps[step]); + if (targetStep) { + targetStep.classList.remove('hidden'); + } + } + + async handleSubmit(e) { + const form = e.target; + const submitBtn = form.querySelector('button[type="submit"]'); + const originalBtnText = submitBtn.textContent; + + try { + // Disable submit button + submitBtn.disabled = true; + submitBtn.textContent = 'Subscribing...'; + + // Collect form data + const formData = new FormData(form); + const email = formData.get('email'); + const name = formData.get('name'); + const interestCheckboxes = form.querySelectorAll('input[name="interest"]:checked'); + const interests = Array.from(interestCheckboxes).map(cb => cb.value); + + // Prepare request data + const data = { + email, + name: name || undefined, + source: window.location.pathname, + interests + }; + + // Submit to API + const response = await fetch('/api/newsletter/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': this.csrfToken + }, + credentials: 'include', + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (response.ok && result.success) { + this.showStep('success'); + } else { + throw new Error(result.error || 'Subscription failed'); + } + + } catch (error) { + console.error('[Newsletter] Subscription error:', error); + const errorMsg = document.getElementById('newsletter-error-message'); + errorMsg.textContent = error.message || 'We couldn\'t process your subscription. Please try again.'; + this.showStep('error'); + } finally { + submitBtn.disabled = false; + submitBtn.textContent = originalBtnText; + } + } +} + +// Auto-initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + window.tractausNewsletter = new TractausNewsletter(); + }); +} else { + window.tractausNewsletter = new TractausNewsletter(); +} diff --git a/public/leader.html b/public/leader.html index b9378464..b895b36f 100644 --- a/public/leader.html +++ b/public/leader.html @@ -1052,5 +1052,8 @@ + + + diff --git a/public/researcher.html b/public/researcher.html index 22aab55a..ff1cf1f6 100644 --- a/public/researcher.html +++ b/public/researcher.html @@ -1597,5 +1597,8 @@ + + + diff --git a/src/controllers/rss.controller.js b/src/controllers/rss.controller.js new file mode 100644 index 00000000..66a81b09 --- /dev/null +++ b/src/controllers/rss.controller.js @@ -0,0 +1,134 @@ +/** + * RSS/Atom Feed Controller + * Generate RSS feeds for blog content + */ + +const BlogPost = require('../models/BlogPost.model'); +const logger = require('../utils/logger.util'); + +/** + * Generate RSS 2.0 feed for all blog posts + * GET /feed.xml + */ +async function generateMainFeed(req, res) { + try { + const posts = await BlogPost.getPublished({ limit: 20 }); + + const baseUrl = process.env.BASE_URL || 'https://agenticgovernance.digital'; + const buildDate = new Date().toUTCString(); + + const rssItems = posts.map(post => { + const pubDate = new Date(post.published_at).toUTCString(); + const link = `${baseUrl}/blog/${post.slug}`; + + return ` + + <![CDATA[${escapeXml(post.title)}]]> + ${link} + ${link} + ${pubDate} + + ${post.tags.map(tag => `${escapeXml(tag)}`).join('\n ')} + research@agenticgovernance.digital (${escapeXml(post.author.name)}) + `; + }).join('\n'); + + const rss = ` + + + Tractatus AI Safety Framework - Research Updates + ${baseUrl}/blog + Research updates, empirical findings, and honest uncertainties from the Tractatus AI Safety Framework project. + en + ${buildDate} + + ${rssItems} + +`; + + res.set('Content-Type', 'application/rss+xml; charset=UTF-8'); + res.send(rss); + + logger.info('[RSS] Main feed generated', { postCount: posts.length }); + + } catch (error) { + logger.error('[RSS] Feed generation error:', error); + res.status(500).send('Feed generation failed'); + } +} + +/** + * Generate topic-specific RSS feed + * GET /feed/:topic.xml + */ +async function generateTopicFeed(req, res) { + try { + const { topic } = req.params; + const posts = await BlogPost.getPublishedByTag(topic, { limit: 20 }); + + if (posts.length === 0) { + return res.status(404).send('Topic not found'); + } + + const baseUrl = process.env.BASE_URL || 'https://agenticgovernance.digital'; + const buildDate = new Date().toUTCString(); + + const rssItems = posts.map(post => { + const pubDate = new Date(post.published_at).toUTCString(); + const link = `${baseUrl}/blog/${post.slug}`; + + return ` + + <![CDATA[${escapeXml(post.title)}]]> + ${link} + ${link} + ${pubDate} + + ${post.tags.map(tag => `${escapeXml(tag)}`).join('\n ')} + research@agenticgovernance.digital (${escapeXml(post.author.name)}) + `; + }).join('\n'); + + const topicTitle = topic.charAt(0).toUpperCase() + topic.slice(1).replace(/-/g, ' '); + + const rss = ` + + + Tractatus - ${topicTitle} Updates + ${baseUrl}/blog?tag=${topic} + Updates on ${topicTitle} from the Tractatus AI Safety Framework project. + en + ${buildDate} + + ${rssItems} + +`; + + res.set('Content-Type', 'application/rss+xml; charset=UTF-8'); + res.send(rss); + + logger.info('[RSS] Topic feed generated', { topic, postCount: posts.length }); + + } catch (error) { + logger.error('[RSS] Topic feed generation error:', error); + res.status(500).send('Feed generation failed'); + } +} + +/** + * Escape XML special characters + */ +function escapeXml(unsafe) { + if (!unsafe) return ''; + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +module.exports = { + generateMainFeed, + generateTopicFeed +}; diff --git a/src/models/BlogPost.model.js b/src/models/BlogPost.model.js index 5de71aee..e22af254 100644 --- a/src/models/BlogPost.model.js +++ b/src/models/BlogPost.model.js @@ -158,6 +158,31 @@ class BlogPost { const collection = await getCollection('blog_posts'); return await collection.countDocuments({ status }); } + + /** + * Get published posts (alias for findPublished, used by RSS controller) + */ + static async getPublished(options = {}) { + return await this.findPublished(options); + } + + /** + * Get published posts filtered by tag + */ + static async getPublishedByTag(tag, options = {}) { + const collection = await getCollection('blog_posts'); + const { limit = 10, skip = 0, sort = { published_at: -1 } } = options; + + return await collection + .find({ + status: 'published', + tags: tag + }) + .sort(sort) + .skip(skip) + .limit(limit) + .toArray(); + } } module.exports = BlogPost; diff --git a/src/routes/rss.routes.js b/src/routes/rss.routes.js new file mode 100644 index 00000000..7f0ac25f --- /dev/null +++ b/src/routes/rss.routes.js @@ -0,0 +1,17 @@ +/** + * RSS Feed Routes + * Public feeds for blog content + */ + +const express = require('express'); +const router = express.Router(); +const rssController = require('../controllers/rss.controller'); +const { asyncHandler } = require('../middleware/error.middleware'); + +// Main RSS feed (all posts) +router.get('/feed.xml', asyncHandler(rssController.generateMainFeed)); + +// Topic-specific RSS feeds +router.get('/feed/:topic.xml', asyncHandler(rssController.generateTopicFeed)); + +module.exports = router; diff --git a/src/server.js b/src/server.js index 2ef499e0..57a16c7f 100644 --- a/src/server.js +++ b/src/server.js @@ -128,6 +128,10 @@ app.get('/blog/:slug', (req, res) => { res.redirect(301, `/blog-post.html?slug=${req.params.slug}`); }); +// RSS feeds (mounted at root level for standard RSS convention) +const rssRoutes = require('./routes/rss.routes'); +app.use('/', rssRoutes); + // Static files app.use(express.static('public'));