UNPKG

blogstart

Version:

Automatically setup SEO-optimized blog routes for Next.js projects

484 lines (421 loc) β€’ 17.3 kB
#!/usr/bin/env node const fs = require("fs"); const path = require("path"); // Get the directory where the package is being installed const projectRoot = process.cwd(); // Add error handling for ES modules projects process.on("uncaughtException", (error) => { console.error("❌ SlashBlog Start setup failed:", error.message); console.log( "πŸ’‘ Try running manually: node node_modules/@slashblog/start/setup.js" ); process.exit(1); }); process.on("unhandledRejection", (reason, promise) => { console.error("❌ SlashBlog Start setup failed:", reason); console.log( "πŸ’‘ Try running manually: node node_modules/@slashblog/start/setup.js" ); process.exit(1); }); // Find the package directory (where templates are located) function getPackageDir() { console.log("πŸ” Looking for package directory..."); console.log("πŸ“ __dirname:", __dirname); console.log("πŸ“ projectRoot:", projectRoot); // First try __dirname (local development or when run directly) const templatePath1 = path.join(__dirname, "templates"); console.log("πŸ” Checking:", templatePath1); if (fs.existsSync(templatePath1)) { console.log("βœ… Found templates at:", templatePath1); return __dirname; } // Try node_modules/@slashblog/start (when installed via npm) const nodeModulesPath = path.join( projectRoot, "node_modules", "@slashblog", "start" ); const templatePath2 = path.join(nodeModulesPath, "templates"); console.log("πŸ” Checking:", templatePath2); if (fs.existsSync(templatePath2)) { console.log("βœ… Found templates at:", templatePath2); return nodeModulesPath; } // Try relative to this script location const scriptDir = path.dirname( require.main ? require.main.filename : __filename ); const templatePath3 = path.join(scriptDir, "templates"); console.log("πŸ” Checking:", templatePath3); if (fs.existsSync(templatePath3)) { console.log("βœ… Found templates at:", templatePath3); return scriptDir; } // Try one level up from __dirname const parentDir = path.dirname(__dirname); const templatePath4 = path.join(parentDir, "templates"); console.log("πŸ” Checking:", templatePath4); if (fs.existsSync(templatePath4)) { console.log("βœ… Found templates at:", templatePath4); return parentDir; } // List what's actually in __dirname for debugging console.log("πŸ“‚ Contents of __dirname:", fs.readdirSync(__dirname)); console.log("πŸ“‚ Contents of projectRoot:", fs.readdirSync(projectRoot)); throw new Error("Could not find @slashblog/start templates directory"); } // Check if it's a TypeScript project function isTypeScriptProject() { return fs.existsSync(path.join(projectRoot, "tsconfig.json")); } // Check if app directory exists (App Router) function isAppRouter() { return ( fs.existsSync(path.join(projectRoot, "app")) || fs.existsSync(path.join(projectRoot, "src", "app")) ); } // Check if pages directory exists (Pages Router) function isPagesRouter() { return ( fs.existsSync(path.join(projectRoot, "pages")) || fs.existsSync(path.join(projectRoot, "src", "pages")) ); } // Get the correct app or pages directory path function getAppDir() { if (fs.existsSync(path.join(projectRoot, "app"))) { return path.join(projectRoot, "app"); } if (fs.existsSync(path.join(projectRoot, "src", "app"))) { return path.join(projectRoot, "src", "app"); } if (fs.existsSync(path.join(projectRoot, "pages"))) { return path.join(projectRoot, "pages"); } if (fs.existsSync(path.join(projectRoot, "src", "pages"))) { return path.join(projectRoot, "src", "pages"); } return null; } // Create blog directory structure function createBlogStructure() { console.log("πŸ” Detecting project structure..."); const isTS = isTypeScriptProject(); const fileExtension = isTS ? ".tsx" : ".jsx"; console.log(`πŸ“ TypeScript project: ${isTS ? "Yes" : "No"}`); console.log(`πŸ“„ File extension: ${fileExtension}`); let blogDir; let routerType; // Determine router type and set blog directory const appDirPath = getAppDir(); if (!appDirPath) { console.log( "❌ No Next.js app or pages directory found. Please make sure this is a Next.js project." ); return; } if (isAppRouter()) { blogDir = path.join(appDirPath, "blog"); routerType = "app"; console.log("🎯 Detected: App Router"); } else if (isPagesRouter()) { blogDir = path.join(appDirPath, "blog"); routerType = "pages"; console.log("🎯 Detected: Pages Router"); } else { console.log( "❌ No Next.js app or pages directory found. Please make sure this is a Next.js project." ); return; } // Check if blog directory already exists if (fs.existsSync(blogDir)) { console.log("⚠️ Blog directory already exists at:", blogDir); console.log( "πŸ”„ Overwriting existing blog files with SlashBlog Start templates..." ); console.log( "πŸ’‘ Your existing files will be backed up with .backup extension" ); // Backup existing files const existingFiles = fs.readdirSync(blogDir, { recursive: true }); existingFiles.forEach((file) => { const filePath = path.join(blogDir, file); if ( (fs.statSync(filePath).isFile() && file.endsWith(".tsx")) || file.endsWith(".jsx") ) { const backupPath = filePath + ".backup"; fs.copyFileSync(filePath, backupPath); console.log(`πŸ“‹ Backed up: ${file} β†’ ${file}.backup`); } }); } else { console.log(`πŸ“ Creating blog directory: ${blogDir}`); // Create blog directory fs.mkdirSync(blogDir, { recursive: true }); } // Sample blog data is not needed since content is fetched remotely // createSampleBlogData(); if (routerType === "app") { // App Router structure createAppRouterStructure(blogDir, isTS, fileExtension); } else { // Pages Router structure createPagesRouterStructure(blogDir, isTS, fileExtension); } console.log("βœ… Blog setup completed successfully!"); console.log("πŸ“ Created blog routes in:", blogDir); console.log( `πŸ”— Blog configured for website ID: ${process.env.BLOGSTART_WEBSITE_ID}` ); console.log("πŸš€ You can now access your blog at /blog"); console.log(""); console.log("πŸ“ Next steps:"); console.log("1. Ensure you have Tailwind CSS installed for styling"); console.log("2. Your blog will automatically fetch the latest content"); console.log("3. Build and deploy your Next.js application"); console.log(""); console.log("πŸ“– See USAGE.md for detailed instructions"); } function createAppRouterStructure(blogDir, isTS, fileExtension) { try { console.log("πŸ—οΈ Creating App Router structure..."); // Create [slug] directory const slugDir = path.join(blogDir, "[slug]"); console.log(`πŸ“ Creating slug directory: ${slugDir}`); fs.mkdirSync(slugDir, { recursive: true }); const packageDir = getPackageDir(); console.log(`πŸ“¦ Package directory: ${packageDir}`); // Check if templates exist const indexTemplatePath = path.join( packageDir, "templates", "app-router", `page${isTS ? ".tsx" : ".jsx"}` ); const slugTemplatePath = path.join( packageDir, "templates", "app-router", `slug-page${isTS ? ".tsx" : ".jsx"}` ); console.log(`πŸ“„ Index template: ${indexTemplatePath}`); console.log(`πŸ“„ Slug template: ${slugTemplatePath}`); console.log( `βœ… Index template exists: ${fs.existsSync(indexTemplatePath)}` ); console.log(`βœ… Slug template exists: ${fs.existsSync(slugTemplatePath)}`); // Check if templates exist before reading if (!fs.existsSync(indexTemplatePath)) { throw new Error(`Index template not found: ${indexTemplatePath}`); } if (!fs.existsSync(slugTemplatePath)) { throw new Error(`Slug template not found: ${slugTemplatePath}`); } // Read templates console.log("πŸ“– Reading index template..."); const blogIndexTemplate = fs.readFileSync(indexTemplatePath, "utf8"); console.log("πŸ“– Reading slug template..."); const blogSlugTemplate = fs.readFileSync(slugTemplatePath, "utf8"); console.log("βœ… Templates read successfully"); // Replace placeholder URL with Firebase Cloud Storage URL const websiteId = process.env.BLOGSTART_WEBSITE_ID; const firebaseBaseUrl = `https://firebasestorage.googleapis.com/v0/b/slashblog-ai.firebasestorage.app/o/blogs%2Fblog-content-${websiteId}.json?alt=media`; const updatedIndexTemplate = blogIndexTemplate .replace(/WEBSITE_ID_PLACEHOLDER/g, websiteId) .replace(/FIREBASE_BASE_URL_PLACEHOLDER/g, firebaseBaseUrl); const updatedSlugTemplate = blogSlugTemplate .replace(/WEBSITE_ID_PLACEHOLDER/g, websiteId) .replace(/FIREBASE_BASE_URL_PLACEHOLDER/g, firebaseBaseUrl); // Create files const indexFilePath = path.join(blogDir, `page${fileExtension}`); const slugFilePath = path.join(slugDir, `page${fileExtension}`); console.log(`πŸ’Ύ Creating index file: ${indexFilePath}`); console.log(`πŸ’Ύ Creating slug file: ${slugFilePath}`); fs.writeFileSync(indexFilePath, updatedIndexTemplate); console.log(`βœ… Index file created: ${indexFilePath}`); fs.writeFileSync(slugFilePath, updatedSlugTemplate); console.log(`βœ… Slug file created: ${slugFilePath}`); fs.writeFileSync(slugFilePath, updatedSlugTemplate); console.log("βœ… App Router files created successfully"); } catch (error) { console.error("❌ Error creating App Router structure:", error.message); throw error; } } // Create sample blog data file function createSampleBlogData() { const sampleDataPath = path.join( projectRoot, "public", "sample-blog-posts.json" ); // Create public directory if it doesn't exist const publicDir = path.join(projectRoot, "public"); if (!fs.existsSync(publicDir)) { fs.mkdirSync(publicDir, { recursive: true }); } const sampleData = { posts: [ { keyword: "website audit service", platform: "Shopify", slug: "shopify-website-audit-service", title: "Shopify Website Audit Service", description: "Complete audit of your Shopify site, combining SEO, performance, and layout analysis. From meta tags and page speed to core web vitals and mobile usability, you'll get a holistic score and detailed checklist of what to improve.", cta: "Audit your Shopify site now β†’", relatedPages: [ { slug: "shopify-website-audit", title: "Shopify Website Audit" }, { slug: "seo-shopify-website-audit", title: "SEO Shopify Website Audit", }, ], content: { intro: "A full audit of your Shopify site is essential if you're serious about growth. SEO, structure, mobile usabilityβ€”all of it needs regular checks to stay ahead.", platformImpact: "Shopify users often deal with platform-specific limitations or quirks. Whether it's bloated plugins, rigid themes, or hidden performance drains, these affect how your site ranks and converts.", commonMistakes: [ "Relying too heavily on third-party apps or plugins that slow down load times", "Neglecting image optimization or lazy loading", "Using default Shopify themes without proper structure", "Skipping mobile responsiveness testing", "Missing alt tags, meta titles, or page hierarchy issues", ], howWeHelp: "We run a custom-tailored audit for Shopify websites. We scan for performance issues, SEO flaws, and layout inefficiencies. You'll get a prioritized list of improvements and real data backing every suggestion.", }, }, { keyword: "nextjs seo optimization", platform: "Next.js", slug: "nextjs-seo-optimization", title: "Next.js SEO Optimization", description: "Maximize your Next.js app's search engine visibility with our SEO optimization service. We handle metadata, structured data, and performance for better rankings.", cta: "Boost your Next.js SEO β†’", relatedPages: [ { slug: "nextjs-metadata", title: "Next.js Metadata Guide" }, ], content: { intro: "Next.js offers powerful SEO capabilities, but many developers don't leverage them fully. Proper SEO setup can dramatically improve your search rankings.", platformImpact: "Next.js developers often focus on functionality over SEO. Without proper metadata, structured data, and performance optimization, even great apps can struggle in search results.", commonMistakes: [ "Missing or incorrect metadata setup", "Poor URL structure and routing", "No structured data implementation", "Ignoring Core Web Vitals", "Inadequate image optimization", ], howWeHelp: "We audit your Next.js application for SEO best practices, implement proper metadata generation, optimize performance, and ensure your app is search engine friendly.", }, }, ], }; console.log(`πŸ“„ Creating sample blog data: ${sampleDataPath}`); fs.writeFileSync(sampleDataPath, JSON.stringify(sampleData, null, 2)); console.log("βœ… Sample blog data created"); } function createPagesRouterStructure(blogDir, isTS, fileExtension) { try { console.log("πŸ—οΈ Creating Pages Router structure..."); const packageDir = getPackageDir(); console.log(`πŸ“¦ Package directory: ${packageDir}`); // Check if templates exist const indexTemplatePath = path.join( packageDir, "templates", "pages-router", `index${isTS ? ".tsx" : ".jsx"}` ); const slugTemplatePath = path.join( packageDir, "templates", "pages-router", `[slug]${isTS ? ".tsx" : ".jsx"}` ); console.log(`πŸ“„ Index template: ${indexTemplatePath}`); console.log(`πŸ“„ Slug template: ${slugTemplatePath}`); console.log( `βœ… Index template exists: ${fs.existsSync(indexTemplatePath)}` ); console.log(`βœ… Slug template exists: ${fs.existsSync(slugTemplatePath)}`); // Read templates const blogIndexTemplate = fs.readFileSync(indexTemplatePath, "utf8"); const blogSlugTemplate = fs.readFileSync(slugTemplatePath, "utf8"); // Replace placeholder URL with Firebase Cloud Storage URL const websiteId = process.env.BLOGSTART_WEBSITE_ID; const firebaseBaseUrl = `https://firebasestorage.googleapis.com/v0/b/slashblog-ai.firebasestorage.app/o/blogs%2Fblog-content-${websiteId}.json?alt=media`; const updatedIndexTemplate = blogIndexTemplate .replace(/WEBSITE_ID_PLACEHOLDER/g, websiteId) .replace(/FIREBASE_BASE_URL_PLACEHOLDER/g, firebaseBaseUrl); const updatedSlugTemplate = blogSlugTemplate .replace(/WEBSITE_ID_PLACEHOLDER/g, websiteId) .replace(/FIREBASE_BASE_URL_PLACEHOLDER/g, firebaseBaseUrl); // Create files const indexFilePath = path.join(blogDir, `index${fileExtension}`); const slugFilePath = path.join(blogDir, `[slug]${fileExtension}`); console.log(`πŸ’Ύ Creating index file: ${indexFilePath}`); console.log(`πŸ’Ύ Creating slug file: ${slugFilePath}`); fs.writeFileSync(indexFilePath, updatedIndexTemplate); fs.writeFileSync(slugFilePath, updatedSlugTemplate); console.log("βœ… Pages Router files created successfully"); } catch (error) { console.error("❌ Error creating Pages Router structure:", error.message); throw error; } } // Main setup function function setup() { console.log("πŸš€ Setting up @slashblog/start..."); console.log("πŸ“ Project root:", projectRoot); console.log("πŸ“‹ Node version:", process.version); console.log("πŸ“‹ Platform:", process.platform); try { // Check if this is a Next.js project const packageJsonPath = path.join(projectRoot, "package.json"); if (!fs.existsSync(packageJsonPath)) { console.log( "❌ No package.json found. Please make sure this is a Node.js project." ); return; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); const hasNext = packageJson.dependencies?.next || packageJson.devDependencies?.next; if (!hasNext) { console.log( "❌ Next.js not found in dependencies. Please make sure this is a Next.js project." ); return; } // Check if we can find templates try { const packageDir = getPackageDir(); console.log("πŸ“¦ Found package templates at:", packageDir); } catch (error) { console.log( "❌ Could not find @slashblog/start templates:", error.message ); console.log("πŸ“ Current directory:", projectRoot); console.log("πŸ“ Script directory:", __dirname); return; } createBlogStructure(); } catch (error) { console.log("❌ Setup failed:", error.message); console.log("Please check the error above and try again."); } } // Run setup setup();