UNPKG

@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
#!/usr/bin/env node 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 = `# Zemenay Blog Configuration NEXT_PUBLIC_SUPABASE_URL=your_supabase_url_here NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here # Optional: Custom configuration # NEXT_PUBLIC_BLOG_TITLE=My Awesome Blog # NEXT_PUBLIC_BLOG_DESCRIPTION=A blog powered by Zemenay Blog `; 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); });