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:
parent
dd1d83a6b8
commit
0f7a970c86
3 changed files with 45 additions and 244 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
124
public/blog.html
124
public/blog.html
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue