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 -->
|
<!-- Post Body -->
|
||||||
<div id="post-body" class="blog-content prose prose-lg max-w-none mb-12"></div>
|
<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 -->
|
<!-- Post Footer -->
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|
@ -227,6 +243,9 @@
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|
||||||
|
<!-- Newsletter Component -->
|
||||||
|
<script src="/js/components/newsletter.js?v=0.1.2.1770608028605"></script>
|
||||||
|
|
||||||
<!-- Load Blog Post JavaScript -->
|
<!-- Load Blog Post JavaScript -->
|
||||||
<script src="/js/blog-post.js?v=0.1.2.1770608028605"></script>
|
<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">
|
<h1 class="text-5xl font-bold text-gray-900 mb-6">
|
||||||
Tractatus Blog
|
Tractatus Blog
|
||||||
</h1>
|
</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.
|
Insights on AI governance, safety frameworks, and the boundary between automation and human judgment.
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -163,108 +185,14 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Footer -->
|
||||||
<!-- Internationalization (must load first for footer translations) -->
|
<!-- Internationalization (must load first for footer translations) -->
|
||||||
<script src="/js/i18n-simple.js?v=0.1.2.1770608028605"></script>
|
<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>
|
<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 -->
|
<!-- Load Blog JavaScript -->
|
||||||
<script src="/js/blog.js?v=0.1.2.1770608028605"></script>
|
<script src="/js/blog.js?v=0.1.2.1770608028605"></script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,11 @@ const activeFilters = {
|
||||||
sort: 'date-desc'
|
sort: 'date-desc'
|
||||||
};
|
};
|
||||||
|
|
||||||
// CSRF token (fetched on page load)
|
|
||||||
let csrfToken = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the blog page
|
* Initialize the blog page
|
||||||
*/
|
*/
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
// Fetch CSRF token first (required for newsletter subscription)
|
|
||||||
await fetchCsrfToken();
|
|
||||||
|
|
||||||
await loadPosts();
|
await loadPosts();
|
||||||
attachEventListeners();
|
attachEventListeners();
|
||||||
} catch (error) {
|
} 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
|
* Load all published blog posts from API
|
||||||
*/
|
*/
|
||||||
|
|
@ -527,128 +502,7 @@ function escapeHtml(text) {
|
||||||
return div.innerHTML;
|
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
|
// Initialize on page load
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
init();
|
init();
|
||||||
setupNewsletterModal();
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue