@asgerami/zemenay-blog
Version:
Plug-and-play blog system for Next.js - Get a fully functional blog running in minutes with zero configuration
580 lines (484 loc) ⢠17.9 kB
JavaScript
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const readline = require("readline");
// Colors for console output
const colors = {
reset: "\x1b[0m",
bright: "\x1b[1m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
};
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function log(message, color = "reset") {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function question(query) {
return new Promise((resolve) => rl.question(query, resolve));
}
async function main() {
log("\nš Zemenay Blog - Plug & Play Setup", "cyan");
log(
"This will add a complete blog system to your Next.js project!\n",
"bright"
);
try {
// Step 1: Check if we're in a Next.js project
await checkNextProject();
// Step 2: Install dependencies automatically
await installDependencies();
// Step 3: Create configuration files automatically
await createConfigFiles();
// Step 4: Create sample pages automatically
await createSamplePages();
// Step 5: Setup Supabase (optional)
await setupSupabase();
// Step 6: Final instructions
showFinalInstructions();
} catch (error) {
log(`\nā Setup failed: ${error.message}`, "red");
process.exit(1);
} finally {
rl.close();
}
}
async function checkNextProject() {
log("š Checking project setup...", "blue");
const isNextProject =
fs.existsSync("next.config.js") || fs.existsSync("next.config.ts");
if (!isNextProject) {
log("ā This doesn't appear to be a Next.js project.", "red");
const createNew = await question(
"Would you like to create a new Next.js project? (y/n): "
);
if (createNew.toLowerCase() === "y") {
await createNextProject();
} else {
throw new Error("Please run this in a Next.js project directory.");
}
}
log("ā
Next.js project detected", "green");
}
async function createNextProject() {
log("\nš¦ Creating new Next.js project...", "blue");
const projectName =
(await question("Enter project name (default: my-blog): ")) || "my-blog";
try {
execSync(
`npx create-next-app@latest ${projectName} --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"`,
{
stdio: "inherit",
}
);
process.chdir(projectName);
log(`ā
Created Next.js project: ${projectName}`, "green");
} catch (error) {
throw new Error("Failed to create Next.js project");
}
}
async function installDependencies() {
log("\nš¦ Installing dependencies...", "blue");
try {
// Install the blog package
execSync("npm install @asgerami/zemenay-blog", { stdio: "inherit" });
log("ā
Zemenay Blog package installed", "green");
// Install additional dependencies if not already present
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
const hasTailwind =
packageJson.dependencies?.tailwindcss ||
packageJson.devDependencies?.tailwindcss;
if (!hasTailwind) {
execSync(
"npm install -D tailwindcss @tailwindcss/typography @tailwindcss/postcss autoprefixer postcss",
{ stdio: "inherit" }
);
log("ā
Tailwind CSS dependencies installed", "green");
} else {
log("ā
Tailwind CSS already configured", "green");
}
} catch (error) {
throw new Error("Failed to install dependencies");
}
}
async function createConfigFiles() {
log("\nāļø Creating configuration files...", "blue");
// Create Tailwind config if it doesn't exist
if (!fs.existsSync("tailwind.config.js")) {
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/@asgerami/zemenay-blog/dist/**/*.{js,ts,jsx,tsx,mdx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
'zemenay': {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
}
},
},
},
plugins: [
require('@tailwindcss/typography'),
],
}`;
fs.writeFileSync("tailwind.config.js", tailwindConfig);
log("ā
Tailwind config created", "green");
} else {
log("ā
Tailwind config already exists", "green");
}
// Create PostCSS config if it doesn't exist
if (!fs.existsSync("postcss.config.js")) {
const postcssConfig = `module.exports = {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}`;
fs.writeFileSync("postcss.config.js", postcssConfig);
log("ā
PostCSS config created", "green");
} else {
log("ā
PostCSS config already exists", "green");
}
// Update globals.css to include Zemenay Blog styles
const cssPaths = [
"app/globals.css",
"src/app/globals.css",
"styles/globals.css",
];
let cssPath = null;
for (const path of cssPaths) {
if (fs.existsSync(path)) {
cssPath = path;
break;
}
}
if (cssPath) {
let cssContent = fs.readFileSync(cssPath, "utf8");
// Add Zemenay Blog import if not already present
if (
!cssContent.includes("@import '@asgerami/zemenay-blog/dist/styles.css'")
) {
const importStatement = `/* Zemenay Blog - Import the package styles */
@import '@asgerami/zemenay-blog/dist/styles.css';
`;
// Add after @tailwind directives
if (cssContent.includes("@tailwind")) {
const tailwindEnd = cssContent.lastIndexOf("@tailwind");
const nextLine = cssContent.indexOf("\n", tailwindEnd) + 1;
cssContent =
cssContent.slice(0, nextLine) +
importStatement +
cssContent.slice(nextLine);
} else {
cssContent = importStatement + cssContent;
}
fs.writeFileSync(cssPath, cssContent);
log("ā
Global CSS updated with Zemenay Blog styles", "green");
} else {
log("ā
Global CSS already includes Zemenay Blog styles", "green");
}
} else {
log("ā ļø Could not find globals.css file", "yellow");
}
}
async function createSamplePages() {
log("\nš Creating sample pages...", "blue");
// Create app directory if it doesn't exist
if (!fs.existsSync("app")) {
fs.mkdirSync("app", { recursive: true });
}
// Create pages directory structure
const dirs = ["app/blog", "app/admin", "app/admin/analytics"];
dirs.forEach((dir) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// Create main page (only if it doesn't exist)
if (!fs.existsSync("app/page.tsx")) {
const mainPage = `"use client";
import Link from 'next/link';
import { ThemeToggle, BlogList } from '@asgerami/zemenay-blog';
export default function Home() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800">
<div className="container mx-auto px-4 py-16">
<div className="text-center">
<div className="flex justify-end mb-8">
<ThemeToggle />
</div>
<h1 className="text-5xl font-bold text-gray-900 dark:text-white mb-6">
Welcome to Your Blog
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-12 max-w-2xl mx-auto">
Your Zemenay Blog is ready! Start creating amazing content with our powerful,
modern blogging platform.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-16">
<Link
href="/blog"
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition-colors"
>
š View Blog
</Link>
<Link
href="/admin"
className="bg-green-600 hover:bg-green-700 text-white px-8 py-3 rounded-lg font-semibold transition-colors"
>
āļø Admin Panel
</Link>
<Link
href="/admin/analytics"
className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-3 rounded-lg font-semibold transition-colors"
>
š Analytics
</Link>
</div>
{/* Demo Blog List */}
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-8">
Recent Blog Posts
</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<BlogList
limit={3}
showSearch={false}
/>
</div>
</div>
</div>
</div>
</div>
);
}`;
fs.writeFileSync("app/page.tsx", mainPage);
log("ā
Main page created", "green");
} else {
log("ā
Main page already exists", "green");
}
// Create blog page
const blogPage = `"use client";
import { BlogList, SearchWithFilters, ThemeToggle } from '@asgerami/zemenay-blog';
import Link from 'next/link';
export default function BlogPage() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-8">
<div>
<Link href="/" className="text-blue-600 hover:text-blue-700 mb-4 inline-block">
ā Back to Home
</Link>
<h1 className="text-4xl font-bold text-gray-900 dark:text-white">
Blog Posts
</h1>
</div>
<ThemeToggle />
</div>
<div className="mb-8">
<SearchWithFilters />
</div>
<BlogList />
</div>
</div>
);
}`;
fs.writeFileSync("app/blog/page.tsx", blogPage);
log("ā
Blog page created", "green");
// Create admin page
const adminPage = `import { AdminPanel } from '@asgerami/zemenay-blog';
export default function AdminPage() {
return <AdminPanel />;
}`;
fs.writeFileSync("app/admin/page.tsx", adminPage);
log("ā
Admin page created", "green");
// Create analytics page
const analyticsPage = `import { AnalyticsDashboard, AdminLayout } from '@asgerami/zemenay-blog';
export default function AnalyticsPage() {
return (
<AdminLayout
title="š Blog Analytics"
description="Comprehensive insights into your blog's performance and audience engagement."
>
<AnalyticsDashboard />
</AdminLayout>
);
}`;
fs.writeFileSync("app/admin/analytics/page.tsx", analyticsPage);
log("ā
Analytics page created", "green");
}
async function setupSupabase() {
log("\nšļø Supabase Setup", "blue");
const setupDb = await question(
"Would you like to set up Supabase database? (y/n): "
);
if (setupDb.toLowerCase() !== "y") {
log("ā ļø Skipping Supabase setup. You can set it up later.", "yellow");
return;
}
log("\nš Supabase Setup Instructions:", "cyan");
log("1. Go to https://supabase.com and create a new project", "bright");
log("2. Once your project is ready, go to the SQL Editor", "bright");
log(
"3. Copy and run the SQL files from the zemenay-blog-sql/ directory",
"bright"
);
log("4. Get your Project URL and API Key from Settings > API", "bright");
log("5. Update your .env.local file with the credentials", "bright");
// Create .env.local template
const envContent = `
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url_here
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here
`;
fs.writeFileSync(".env.local", envContent);
log("ā
Environment file created (.env.local)", "green");
// Create SQL files directory
if (!fs.existsSync("zemenay-blog-sql")) {
fs.mkdirSync("zemenay-blog-sql");
}
// Create sample SQL files
const sqlFiles = {
"01-main-schema.sql": `-- Zemenay Blog Main Schema
-- Run this first in your Supabase SQL Editor
-- Create posts table
CREATE TABLE IF NOT EXISTS posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
featured_image_id UUID
);
-- Create categories table
CREATE TABLE IF NOT EXISTS categories (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
description TEXT,
color TEXT DEFAULT '#3b82f6',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create tags table
CREATE TABLE IF NOT EXISTS tags (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
color TEXT DEFAULT '#10b981',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE tags ENABLE ROW LEVEL SECURITY;
-- Create policies for public read access
CREATE POLICY "Allow public read access on posts" ON posts FOR SELECT USING (true);
CREATE POLICY "Allow public read access on categories" ON categories FOR SELECT USING (true);
CREATE POLICY "Allow public read access on tags" ON tags FOR SELECT USING (true);
`,
"02-junction-tables.sql": `-- Zemenay Blog Junction Tables
-- Run this second
-- Create junction tables
CREATE TABLE IF NOT EXISTS post_categories (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
category_id UUID NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(post_id, category_id)
);
CREATE TABLE IF NOT EXISTS post_tags (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(post_id, tag_id)
);
-- Enable RLS and create policies
ALTER TABLE post_categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE post_tags ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow public read access on post_categories" ON post_categories FOR SELECT USING (true);
CREATE POLICY "Allow public read access on post_tags" ON post_tags FOR SELECT USING (true);
`,
"03-sample-data.sql": `-- Zemenay Blog Sample Data
-- Run this third to add sample posts
-- Insert sample categories
INSERT INTO categories (name, slug, description, color) VALUES
('Technology', 'technology', 'Tech-related posts', '#3b82f6'),
('Design', 'design', 'Design and UX posts', '#10b981'),
('Business', 'business', 'Business insights', '#f59e0b');
-- Insert sample tags
INSERT INTO tags (name, slug, color) VALUES
('React', 'react', '#61dafb'),
('Next.js', 'nextjs', '#000000'),
('TypeScript', 'typescript', '#3178c6'),
('UI/UX', 'ui-ux', '#8b5cf6');
-- Insert sample posts
INSERT INTO posts (title, content, slug) VALUES
('Welcome to Your New Blog', 'This is your first blog post! Start writing amazing content and share your thoughts with the world.', 'welcome-to-your-new-blog'),
('Getting Started with Next.js', 'Next.js is a powerful React framework that makes building full-stack web applications simple and efficient.', 'getting-started-with-nextjs'),
('The Future of Web Development', 'Web development is constantly evolving. Here are the trends that will shape the future of the web.', 'future-of-web-development');
-- Add categories and tags to posts
INSERT INTO post_categories (post_id, category_id)
SELECT p.id, c.id FROM posts p, categories c
WHERE p.slug = 'welcome-to-your-new-blog' AND c.slug = 'technology';
INSERT INTO post_tags (post_id, tag_id)
SELECT p.id, t.id FROM posts p, tags t
WHERE p.slug = 'welcome-to-your-new-blog' AND t.slug IN ('React', 'Next.js');
`,
};
Object.entries(sqlFiles).forEach(([filename, content]) => {
fs.writeFileSync(path.join("zemenay-blog-sql", filename), content);
});
log("ā
SQL files created in zemenay-blog-sql/ directory", "green");
}
function showFinalInstructions() {
log("\nš Setup Complete!", "green");
log("\nYour Zemenay Blog is ready! Here's what to do next:\n", "bright");
log("1. š Start your development server:", "cyan");
log(" npm run dev\n", "bright");
log("2. š Visit your blog:", "cyan");
log(" - Homepage: http://localhost:3000", "bright");
log(" - Blog: http://localhost:3000/blog", "bright");
log(" - Admin: http://localhost:3000/admin", "bright");
log(" - Analytics: http://localhost:3000/admin/analytics\n", "bright");
log("3. šļø Set up your database (if not done):", "cyan");
log(" - Run the SQL files in zemenay-blog-sql/ directory", "bright");
log(" - Update .env.local with your Supabase credentials\n", "bright");
log("4. āļø Create your first post:", "cyan");
log(" - Go to /admin and sign in", "bright");
log(" - Click 'New Post' to create content\n", "bright");
log("š Documentation: https://github.com/zemenay/zemenay-blog", "magenta");
log("š Support: https://github.com/zemenay/zemenay-blog/issues", "magenta");
log("\nHappy blogging! š", "green");
}
// Run the setup
main().catch((error) => {
log(`\nā Setup failed: ${error.message}`, "red");
process.exit(1);
});