Add dual-source blog system with Editor.js integration: - Blog storage supporting repo-based (JSON) and database sources - Admin panel with rich text editor using Editor.js - Public news page with infinite scroll - Individual blog post viewer page - Categories management in admin - Image upload functionality - 4 SEO blog posts about WordPress with PluginCompass promotion - 3 News blog posts about Plugin Compass - API endpoints for CRUD operations - Security and validation for admin operations Closes blog feature request
455 lines
18 KiB
HTML
455 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="description" content="Loading blog post...">
|
|
<title>Blog - Plugin Compass</title>
|
|
<link rel="icon" type="image/png" href="/assets/Plugin.png">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<script>
|
|
(function applyTailwindConfig(cfg, attempts = 0) {
|
|
try {
|
|
if (typeof window.tailwind !== 'undefined') {
|
|
window.tailwind.config = cfg;
|
|
return;
|
|
}
|
|
} catch (e) {}
|
|
if (attempts >= 20) return;
|
|
setTimeout(() => applyTailwindConfig(cfg, attempts + 1), 100);
|
|
})({
|
|
theme: {
|
|
extend: {
|
|
fontFamily: { sans: ['Inter', 'sans-serif'] },
|
|
colors: {
|
|
brand: {
|
|
50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc',
|
|
400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca',
|
|
800: '#3730a3', 900: '#312e81'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
<style>
|
|
body { font-family: 'Inter', sans-serif; }
|
|
|
|
/* Blog content styles */
|
|
.blog-content {
|
|
font-size: 1.125rem;
|
|
line-height: 1.75;
|
|
color: #374151;
|
|
}
|
|
.blog-content h2 {
|
|
font-size: 1.875rem;
|
|
font-weight: 700;
|
|
margin-top: 2.5rem;
|
|
margin-bottom: 1rem;
|
|
color: #111827;
|
|
}
|
|
.blog-content h3 {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin-top: 2rem;
|
|
margin-bottom: 0.75rem;
|
|
color: #111827;
|
|
}
|
|
.blog-content h4 {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.5rem;
|
|
color: #111827;
|
|
}
|
|
.blog-content p {
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.blog-content ul, .blog-content ol {
|
|
margin-bottom: 1.25rem;
|
|
padding-left: 1.5rem;
|
|
}
|
|
.blog-content li {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.blog-content a {
|
|
color: #4f46e5;
|
|
text-decoration: underline;
|
|
}
|
|
.blog-content a:hover {
|
|
color: #4338ca;
|
|
}
|
|
.blog-content blockquote {
|
|
border-left: 4px solid #e5e7eb;
|
|
padding-left: 1rem;
|
|
margin: 1.5rem 0;
|
|
font-style: italic;
|
|
color: #6b7280;
|
|
}
|
|
.blog-content blockquote cite {
|
|
display: block;
|
|
margin-top: 0.5rem;
|
|
font-size: 0.875rem;
|
|
font-style: normal;
|
|
color: #9ca3af;
|
|
}
|
|
.blog-content pre {
|
|
background: #f3f4f6;
|
|
padding: 1rem;
|
|
border-radius: 0.5rem;
|
|
overflow-x: auto;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.blog-content code {
|
|
background: #f3f4f6;
|
|
padding: 0.2rem 0.4rem;
|
|
border-radius: 0.25rem;
|
|
font-family: monospace;
|
|
font-size: 0.875rem;
|
|
}
|
|
.blog-content pre code {
|
|
background: none;
|
|
padding: 0;
|
|
}
|
|
.blog-content img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: 0.5rem;
|
|
margin: 1.5rem 0;
|
|
}
|
|
.blog-content figure {
|
|
margin: 1.5rem 0;
|
|
}
|
|
.blog-content figcaption {
|
|
text-align: center;
|
|
font-size: 0.875rem;
|
|
color: #6b7280;
|
|
margin-top: 0.5rem;
|
|
}
|
|
.blog-content table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.blog-content th, .blog-content td {
|
|
border: 1px solid #e5e7eb;
|
|
padding: 0.75rem;
|
|
text-align: left;
|
|
}
|
|
.blog-content th {
|
|
background: #f9fafb;
|
|
font-weight: 600;
|
|
}
|
|
.blog-content .embed {
|
|
position: relative;
|
|
padding-bottom: 56.25%;
|
|
height: 0;
|
|
overflow: hidden;
|
|
margin: 1.5rem 0;
|
|
}
|
|
.blog-content .embed iframe {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Loading animation */
|
|
.loading-pulse {
|
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: .5; }
|
|
}
|
|
</style>
|
|
|
|
<!-- PostHog Analytics -->
|
|
<script src="/posthog.js"></script>
|
|
</head>
|
|
<body class="bg-gray-50 text-gray-900">
|
|
<!-- Navigation -->
|
|
<nav class="bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between h-16">
|
|
<div class="flex items-center">
|
|
<a href="/" class="flex items-center gap-2">
|
|
<img src="/assets/Plugin.png" alt="Plugin Compass" class="h-8 w-8">
|
|
<span class="font-bold text-xl text-gray-900">Plugin Compass</span>
|
|
</a>
|
|
</div>
|
|
<div class="flex items-center gap-6">
|
|
<a href="/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
|
|
<a href="/news" class="text-gray-600 hover:text-gray-900 font-medium">News</a>
|
|
<a href="/features" class="text-gray-600 hover:text-gray-900 font-medium">Features</a>
|
|
<a href="/pricing" class="text-gray-600 hover:text-gray-900 font-medium">Pricing</a>
|
|
<a href="/builder" class="bg-brand-600 text-white px-4 py-2 rounded-lg hover:bg-brand-700 transition">Get Started</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loading-state" class="min-h-screen flex items-center justify-center">
|
|
<div class="text-center">
|
|
<i class="fas fa-spinner fa-spin text-4xl text-brand-600 mb-4"></i>
|
|
<p class="text-gray-600">Loading article...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="error-state" class="min-h-screen flex items-center justify-center hidden">
|
|
<div class="text-center">
|
|
<i class="fas fa-exclamation-circle text-6xl text-red-400 mb-4"></i>
|
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">Article Not Found</h1>
|
|
<p class="text-gray-600 mb-6">The blog post you're looking for doesn't exist or has been removed.</p>
|
|
<a href="/news" class="inline-flex items-center gap-2 text-brand-600 hover:text-brand-700 font-medium">
|
|
<i class="fas fa-arrow-left"></i>
|
|
Back to News
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Article Content -->
|
|
<article id="article-content" class="hidden">
|
|
<!-- Hero Section -->
|
|
<header class="bg-white border-b border-gray-200">
|
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
<div class="flex items-center gap-3 mb-6">
|
|
<span id="post-category" class="px-3 py-1 bg-brand-100 text-brand-700 rounded-full text-sm font-medium">Category</span>
|
|
<span id="post-type" class="text-gray-400 text-sm"></span>
|
|
</div>
|
|
<h1 id="post-title" class="text-4xl md:text-5xl font-bold text-gray-900 mb-6 leading-tight"></h1>
|
|
<div class="flex items-center gap-4 text-gray-600">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center">
|
|
<i class="fas fa-user text-brand-600"></i>
|
|
</div>
|
|
<div>
|
|
<p id="post-author" class="font-medium text-gray-900"></p>
|
|
<p id="post-date" class="text-sm"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Featured Image -->
|
|
<div id="featured-image-container" class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 -mt-6 mb-12 hidden">
|
|
<img id="featured-image" src="" alt="" class="w-full h-64 md:h-96 object-cover rounded-xl shadow-lg">
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<main class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-16">
|
|
<div id="post-content" class="blog-content">
|
|
<!-- Content will be loaded here -->
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div id="post-tags" class="mt-12 pt-8 border-t border-gray-200">
|
|
<!-- Tags will be loaded here -->
|
|
</div>
|
|
|
|
<!-- Share -->
|
|
<div class="mt-8 flex items-center gap-4">
|
|
<span class="text-gray-600 font-medium">Share this article:</span>
|
|
<div class="flex gap-2">
|
|
<button onclick="shareTwitter()" class="w-10 h-10 rounded-full bg-blue-400 text-white flex items-center justify-center hover:bg-blue-500 transition">
|
|
<i class="fab fa-twitter"></i>
|
|
</button>
|
|
<button onclick="shareFacebook()" class="w-10 h-10 rounded-full bg-blue-600 text-white flex items-center justify-center hover:bg-blue-700 transition">
|
|
<i class="fab fa-facebook-f"></i>
|
|
</button>
|
|
<button onclick="shareLinkedIn()" class="w-10 h-10 rounded-full bg-blue-700 text-white flex items-center justify-center hover:bg-blue-800 transition">
|
|
<i class="fab fa-linkedin-in"></i>
|
|
</button>
|
|
<button onclick="copyLink()" class="w-10 h-10 rounded-full bg-gray-600 text-white flex items-center justify-center hover:bg-gray-700 transition" title="Copy link">
|
|
<i class="fas fa-link"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</article>
|
|
|
|
<!-- Footer -->
|
|
<footer class="bg-gray-900 text-gray-300 py-12">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<img src="/assets/Plugin.png" alt="Plugin Compass" class="h-8 w-8">
|
|
<span class="font-bold text-white text-lg">Plugin Compass</span>
|
|
</div>
|
|
<p class="text-sm">Build custom WordPress plugins with AI. Replace expensive subscriptions with tailored solutions.</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-semibold text-white mb-4">Product</h4>
|
|
<ul class="space-y-2 text-sm">
|
|
<li><a href="/features" class="hover:text-white">Features</a></li>
|
|
<li><a href="/pricing" class="hover:text-white">Pricing</a></li>
|
|
<li><a href="/builder" class="hover:text-white">Builder</a></li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-semibold text-white mb-4">Resources</h4>
|
|
<ul class="space-y-2 text-sm">
|
|
<li><a href="/news" class="hover:text-white">News</a></li>
|
|
<li><a href="/docs" class="hover:text-white">Documentation</a></li>
|
|
<li><a href="/faq" class="hover:text-white">FAQ</a></li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-semibold text-white mb-4">Legal</h4>
|
|
<ul class="space-y-2 text-sm">
|
|
<li><a href="/privacy" class="hover:text-white">Privacy Policy</a></li>
|
|
<li><a href="/terms" class="hover:text-white">Terms of Service</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-sm">
|
|
<p>© 2026 Plugin Compass. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
let postData = null;
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
const slug = window.location.pathname.split('/blog/')[1];
|
|
if (!slug) {
|
|
showError();
|
|
return;
|
|
}
|
|
|
|
await loadPost(slug);
|
|
});
|
|
|
|
// Load post
|
|
async function loadPost(slug) {
|
|
try {
|
|
const response = await fetch(`/api/blogs/${slug}`);
|
|
|
|
if (!response.ok) {
|
|
showError();
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
postData = data.post;
|
|
|
|
if (!postData) {
|
|
showError();
|
|
return;
|
|
}
|
|
|
|
renderPost(postData);
|
|
} catch (error) {
|
|
console.error('Failed to load post:', error);
|
|
showError();
|
|
}
|
|
}
|
|
|
|
// Render post
|
|
function renderPost(post) {
|
|
// Update meta tags
|
|
document.title = `${post.title} - Plugin Compass`;
|
|
document.querySelector('meta[name="description"]').content = post.excerpt || post.meta_description || '';
|
|
|
|
// Update page content
|
|
document.getElementById('post-title').textContent = post.title;
|
|
document.getElementById('post-author').textContent = post.author || 'Plugin Compass Team';
|
|
document.getElementById('post-date').textContent = formatDate(post.published_at);
|
|
document.getElementById('post-category').textContent = post.category || 'Article';
|
|
document.getElementById('post-type').textContent = post.type === 'seo' ? 'SEO Article' : 'News';
|
|
|
|
// Featured image
|
|
if (post.featured_image) {
|
|
document.getElementById('featured-image').src = post.featured_image;
|
|
document.getElementById('featured-image').alt = post.title;
|
|
document.getElementById('featured-image-container').classList.remove('hidden');
|
|
}
|
|
|
|
// Content
|
|
document.getElementById('post-content').innerHTML = post.html || '<p>No content available.</p>';
|
|
|
|
// Tags
|
|
if (post.tags && post.tags.length > 0) {
|
|
document.getElementById('post-tags').innerHTML = `
|
|
<div class="flex flex-wrap gap-2">
|
|
${post.tags.map(tag => `
|
|
<span class="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">${escapeHtml(tag)}</span>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Show content, hide loading
|
|
document.getElementById('loading-state').classList.add('hidden');
|
|
document.getElementById('article-content').classList.remove('hidden');
|
|
}
|
|
|
|
// Show error state
|
|
function showError() {
|
|
document.getElementById('loading-state').classList.add('hidden');
|
|
document.getElementById('error-state').classList.remove('hidden');
|
|
}
|
|
|
|
// Share functions
|
|
function shareTwitter() {
|
|
const url = encodeURIComponent(window.location.href);
|
|
const text = encodeURIComponent(postData?.title || 'Check out this article');
|
|
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=600,height=400');
|
|
}
|
|
|
|
function shareFacebook() {
|
|
const url = encodeURIComponent(window.location.href);
|
|
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400');
|
|
}
|
|
|
|
function shareLinkedIn() {
|
|
const url = encodeURIComponent(window.location.href);
|
|
const title = encodeURIComponent(postData?.title || '');
|
|
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}&title=${title}`, '_blank', 'width=600,height=400');
|
|
}
|
|
|
|
function copyLink() {
|
|
navigator.clipboard.writeText(window.location.href).then(() => {
|
|
// Show feedback
|
|
const btn = event.target.closest('button');
|
|
const original = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fas fa-check"></i>';
|
|
setTimeout(() => {
|
|
btn.innerHTML = original;
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
// Utility functions
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return '';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|