feat: implement comprehensive blog system
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
This commit is contained in:
454
chat/public/blog.html
Normal file
454
chat/public/blog.html
Normal file
@@ -0,0 +1,454 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user