feat: Add subscribe CTA to blog hero and individual posts

Move newsletter subscription CTA from buried bottom section to prominent
hero placement with "New" badge and RSS link. Add post-level subscribe
prompt after article content. Replace inline newsletter modal with
reusable newsletter.js component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
TheFlow 2026-02-09 16:56:36 +13:00
parent c5b11866de
commit 560ba3ed12
3 changed files with 45 additions and 244 deletions

View file

@ -213,6 +213,22 @@
<!-- Post Body -->
<div id="post-body" class="blog-content prose prose-lg max-w-none mb-12"></div>
<!-- Subscribe CTA -->
<div class="border-t border-gray-200 pt-8 mb-12">
<div class="bg-gradient-to-br from-indigo-50 to-blue-50 rounded-lg p-8 text-center">
<h3 class="text-xl font-bold text-gray-900 mb-2">Enjoyed this article?</h3>
<p class="text-gray-600 mb-6">Subscribe to stay updated on AI governance research and insights.</p>
<button data-newsletter-trigger
class="inline-flex items-center bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-800 transition shadow-md border-2 border-blue-900"
aria-label="Subscribe to newsletter">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Subscribe
</button>
</div>
</div>
<!-- Post Footer -->
</article>
@ -227,6 +243,9 @@
<!-- Footer -->
<!-- Newsletter Component -->
<script src="/js/components/newsletter.js?v=0.1.2.1770608028605"></script>
<!-- Load Blog Post JavaScript -->
<script src="/js/blog-post.js?v=0.1.2.1770608028605"></script>

View file

@ -61,9 +61,31 @@
<h1 class="text-5xl font-bold text-gray-900 mb-6">
Tractatus Blog
</h1>
<p class="text-xl text-gray-600 max-w-3xl mx-auto mb-8">
<p class="text-xl text-gray-600 max-w-3xl mx-auto mb-6">
Insights on AI governance, safety frameworks, and the boundary between automation and human judgment.
</p>
<p class="inline-flex items-center gap-2 text-sm text-indigo-700 font-medium mb-6">
<span class="bg-indigo-600 text-white text-xs font-bold uppercase px-2 py-0.5 rounded">New</span>
Now publishing — subscribe to follow our research
</p>
<div class="flex items-center justify-center gap-4">
<button data-newsletter-trigger
class="inline-flex items-center bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-800 transition shadow-md border-2 border-blue-900"
aria-label="Subscribe to newsletter">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Subscribe
</button>
<a href="/api/blog/rss"
class="inline-flex items-center text-gray-600 hover:text-indigo-700 px-4 py-3 rounded-lg font-medium border border-gray-300 hover:border-indigo-300 transition"
aria-label="RSS Feed">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M6.18 15.64a2.18 2.18 0 010 4.36 2.18 2.18 0 010-4.36zM4 4.44A15.56 15.56 0 0119.56 20h-2.83A12.73 12.73 0 004 7.27V4.44zm0 5.66a9.9 9.9 0 019.9 9.9h-2.83A7.07 7.07 0 004 12.93v-2.83z"/>
</svg>
RSS
</a>
</div>
</div>
</div>
</div>
@ -163,108 +185,14 @@
</div>
</div>
<!-- CTA Section -->
<div class="bg-indigo-50 py-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl font-bold text-gray-900 mb-4">Stay Updated</h2>
<p class="text-lg text-gray-600 mb-8 max-w-2xl mx-auto">
Get notified when we publish new insights on AI governance and safety frameworks.
</p>
<button
id="open-newsletter-modal"
class="inline-block bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-800 transition shadow-md border-2 border-blue-900"
aria-label="Open newsletter subscription"
>
Subscribe to Newsletter
</button>
</div>
</div>
<!-- Newsletter Modal -->
<div id="newsletter-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<div class="flex justify-between items-start mb-4">
<h3 class="text-2xl font-bold text-gray-900">Subscribe to Newsletter</h3>
<button
id="close-newsletter-modal"
class="text-gray-400 hover:text-gray-600 transition"
aria-label="Close newsletter modal"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<p class="text-gray-600 mb-6">
Get updates on AI safety research, framework developments, and case studies delivered to your inbox.
</p>
<form id="newsletter-form" class="space-y-4">
<div>
<label for="newsletter-email" class="block text-sm font-medium text-gray-700 mb-2">
Email Address <span class="text-red-600">*</span>
</label>
<input
type="email"
id="newsletter-email"
name="email"
required
placeholder="you@example.com"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
>
</div>
<div>
<label for="newsletter-name" class="block text-sm font-medium text-gray-700 mb-2">
Name (optional)
</label>
<input
type="text"
id="newsletter-name"
name="name"
placeholder="Your name"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
>
</div>
<div id="newsletter-success" class="hidden bg-green-50 border border-green-200 rounded-md p-3 text-sm text-green-800">
<strong>Success!</strong> You've been subscribed to our newsletter.
</div>
<div id="newsletter-error" class="hidden bg-red-50 border border-red-200 rounded-md p-3 text-sm text-red-800">
<strong>Error:</strong> <span id="newsletter-error-message">Failed to subscribe. Please try again.</span>
</div>
<div class="flex gap-3">
<button
type="submit"
id="newsletter-submit"
class="flex-1 bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-800 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
Subscribe
</button>
<button
type="button"
id="cancel-newsletter"
class="px-6 py-3 border border-gray-300 rounded-lg font-semibold text-gray-700 hover:bg-gray-50 transition"
>
Cancel
</button>
</div>
</form>
<p class="text-xs text-gray-500 mt-4">
We respect your privacy. Unsubscribe at any time.
</p>
</div>
</div>
<!-- Footer -->
<!-- Internationalization (must load first for footer translations) -->
<script src="/js/i18n-simple.js?v=0.1.2.1770608028605"></script>
<script src="/js/components/language-selector.js?v=0.1.2.1770608028605"></script>
<!-- Newsletter Component -->
<script src="/js/components/newsletter.js?v=0.1.2.1770608028605"></script>
<!-- Load Blog JavaScript -->
<script src="/js/blog.js?v=0.1.2.1770608028605"></script>

View file

@ -16,17 +16,11 @@ const activeFilters = {
sort: 'date-desc'
};
// CSRF token (fetched on page load)
let csrfToken = null;
/**
* Initialize the blog page
*/
async function init() {
try {
// Fetch CSRF token first (required for newsletter subscription)
await fetchCsrfToken();
await loadPosts();
attachEventListeners();
} catch (error) {
@ -35,25 +29,6 @@ async function init() {
}
}
/**
* Fetch CSRF token from server
* Required because nginx serves blog.html as static file (bypasses setCsrfToken middleware)
*/
async function fetchCsrfToken() {
try {
const response = await fetch('/api/csrf-token');
const data = await response.json();
if (data.csrfToken) {
csrfToken = data.csrfToken;
console.log('CSRF token fetched successfully');
}
} catch (error) {
console.warn('Failed to fetch CSRF token:', error);
// Non-critical - newsletter subscription will fail but blog browsing still works
}
}
/**
* Load all published blog posts from API
*/
@ -527,128 +502,7 @@ function escapeHtml(text) {
return div.innerHTML;
}
/**
* Newsletter Modal Functionality
*/
function setupNewsletterModal() {
const modal = document.getElementById('newsletter-modal');
const openBtn = document.getElementById('open-newsletter-modal');
const closeBtn = document.getElementById('close-newsletter-modal');
const cancelBtn = document.getElementById('cancel-newsletter');
const form = document.getElementById('newsletter-form');
const successMsg = document.getElementById('newsletter-success');
const errorMsg = document.getElementById('newsletter-error');
const errorText = document.getElementById('newsletter-error-message');
const submitBtn = document.getElementById('newsletter-submit');
// Open modal
if (openBtn) {
openBtn.addEventListener('click', () => {
modal.classList.remove('hidden');
document.getElementById('newsletter-email').focus();
});
}
// Close modal
function closeModal() {
modal.classList.add('hidden');
form.reset();
successMsg.classList.add('hidden');
errorMsg.classList.add('hidden');
}
if (closeBtn) {
closeBtn.addEventListener('click', closeModal);
}
if (cancelBtn) {
cancelBtn.addEventListener('click', closeModal);
}
// Close on backdrop click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal();
}
});
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
closeModal();
}
});
// Handle form submission
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Reset messages
successMsg.classList.add('hidden');
errorMsg.classList.add('hidden');
const email = document.getElementById('newsletter-email').value;
const name = document.getElementById('newsletter-name').value;
// Disable submit button
submitBtn.disabled = true;
submitBtn.textContent = 'Subscribing...';
try {
// Ensure we have a CSRF token
if (!csrfToken) {
await fetchCsrfToken();
}
if (!csrfToken) {
throw new Error('Unable to obtain CSRF token');
}
const response = await fetch('/api/newsletter/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
email,
name: name || null,
source: 'blog'
})
});
const data = await response.json();
if (response.ok && data.success) {
// Show success message
successMsg.classList.remove('hidden');
form.reset();
// Close modal after 2 seconds
setTimeout(() => {
closeModal();
}, 2000);
} else {
// Show error message
errorText.textContent = data.error || 'Failed to subscribe. Please try again.';
errorMsg.classList.remove('hidden');
}
} catch (error) {
console.error('Newsletter subscription error:', error);
errorText.textContent = 'Network error. Please check your connection and try again.';
errorMsg.classList.remove('hidden');
} finally {
// Re-enable submit button
submitBtn.disabled = false;
submitBtn.textContent = 'Subscribe';
}
});
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
init();
setupNewsletterModal();
});