如何在2023年为博客创建自定义静态站点生成器
#教程 #node #express #ejs

介绍

How to create a custom Static Site Generator for Blogs in 2023

曾经想知道如何为您的个人项目或公司网站建立一个巨大的快速博客,而无需处理复杂的安全问题?

如果是这样,您可能已经绕过静态站点生成器。有无数的选项都涵盖了特定用例。

但是,这也是开箱即用的解决方案的问题。它们是考虑了特定用例的构建。

在CodeSphere,我们目前正在加强从主要MonorePo的网站和博客的脱钩,以在营销工程工作中变得更加倾向。我们研究了不同的选择,没有一个完美地适合我们的用例。由于我们在这里有DIY刻度,并喜欢为自己构建自定义解决方案,因此我们着手为我们的博客构建自定义静态站点生成器。事实证明,这并不像最初听起来那样难。

How to create a custom Static Site Generator for Blogs in 2023

在本文中,我将解释一下我们如何意识到这一点。只需单击几下即可免费编码,或者只是克隆并免费托管回购。

让我们开始!

项目设置

我们将使用相当基本的设置来不膨胀我们的项目。

我们的静态站点生成器将使用Express在Node.js服务器上运行。这些页面将使用EJS实施,并使用parwindcss进行样式。帖子数据本身将通过服务器上的JSON文件提供给我们的EJS模板。

您也可以在没有方风的情况下进行样式。您将在我们的存储库中的generatePosts.js文件中找到copystylesheets函数。如果您不使用它,这将直接将样式表从SRC复制到您的存储库。然后,您只需使用NPM卸载即可卸载尾风。

静态站点生成器

我们的发电机基本上由3个函数组成(如上所述,如果您在没有尾风的情况下进行的4个功能)。

  • generateIndexcontent :使用EJS动态呈现索引页模板,包括特色帖子和最近的帖子列表。
  • 生成postconten t:使用每个帖子内容的EJS模板生成单独的帖子页面。
  • Generatestaticsite :合并生成indexcontent和GeneratePostContent,并使用FS模块将完成的站点写入公共目录。
// const GhostContentAPI = require('@tryghost/content-api');
const path = require('path');
const fs = require('fs');
const ejs = require('ejs');
require('dotenv').config();

const outputDirectory = path.join(__dirname, '../public/posts/');
const indexDirectory = path.join(__dirname, '../public/');
const srcDirectory = path.join(__dirname, '../src/');
const postsData = path.join(__dirname, 'data/', 'data.json');

function generateIndexContent(posts) {
  // Use EJS to render the index template with the posts data
  const template = fs.readFileSync(path.join(__dirname, 'templates', 'indexTemplate.ejs'), 'utf-8');

  return ejs.render(template, {
      posts: posts,
      featuredPost: posts[0]
  }, {
      root: path.join(__dirname, 'templates')
  });
}

function ensureDirectoryExistence(directory) {
  if (!fs.existsSync(directory)) {
    fs.mkdirSync(directory, { recursive: true });
  }
}

function generatePostContent(post) {
  ensureDirectoryExistence(path.join(__dirname, '../public/posts/'));

  // Use EJS to render the post template with the post data
  return ejs.render(
    fs.readFileSync(path.join(__dirname, 'templates', 'postTemplate.ejs'), 'utf-8'), 
    {
      image: post.feature_image,
      title: post.title,
      content: post.html, // Content
      excerpt: post.excerpt
    }, {
      root: path.join(__dirname, 'templates') //Root directory for views
    }
  );
}

function saveToFile(filename, content) {
  fs.writeFile(filename, content, (err) => {
      if (err) {
          console.error(`Error saving ${filename}:`, err);
      } else {
          // console.log(`${filename} generated successfully!`);
      }
  });
}

async function generateStaticSite() {
  const posts = JSON.parse(fs.readFileSync(postsData, 'utf-8'));

  // Generate the main index page
  const indexContent = generateIndexContent(posts);
  saveToFile(path.join(indexDirectory, 'index.html'), indexContent);

  // Generate individual post pages
  for (const post of posts) {
    // console.log(post); // Debugging: print the post object
    const postContent = generatePostContent(post);
    saveToFile(path.join(outputDirectory, `${post.slug}.html`), postContent);
  }

  console.log("Generated static Post Pages!");

}

// const stylesheets = ['index.css', 'article.css'];

// async function copyStylesheets() {
// for (const stylesheet of stylesheets) {
// const srcPath = path.join(srcDirectory, './styles', stylesheet);
// const destPath = path.join(indexDirectory, stylesheet);

// fs.copyFile(srcPath, destPath, err => {
// if (err) {
// console.log(`Error copying ${stylesheet}:`, err);
// } else {
// console.log(`${stylesheet} copied successfully!`);
// }
// });
// }
// }

generateStaticSite();
// copyStylesheets();

服务器设置

服务器本身也非常简约,以减少任何开销。从本质上讲,它初始化了EJS模板引擎,并基于帖子目录中的文件创建动态路由,这是由GenerateStaticsite函数创建的。

const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const port = 3000;
const expressLayouts = require('express-ejs-layouts');

app.use(express.json());

app.use(express.static('public'));

app.use('/scripts', express.static(__dirname + '/node_modules/flowbite/dist/'));

//Set Templating Engine
app.use(expressLayouts);
app.set("view engine", "ejs");

// Function to read the generated HTML files
function getHTMLContent(slug) {
  const filePath = path.join(__dirname, 'public', 'posts', `${slug}.html`);

  try {
    return fs.readFileSync(filePath, 'utf-8');
  } catch (error) {
    console.error(`Error reading HTML file for ${slug}:`, error);
    return null;
  }
}

// Dynamic route creation
function createDynamicRoutes() {
  const pagesDirectory = path.join(__dirname, 'public', 'posts');

  fs.readdirSync(pagesDirectory).forEach((file) => {
    if (file.endsWith('.html')) {
      const slug = file.replace('.html', '');
      app.get(`/posts/${slug}`, (req, res) => {
        const pageContent = getHTMLContent(slug);
        if (pageContent) {
          res.send(pageContent);
        } else {
          res.status(404).send(`${slug} page not found`);
        }
      });
    }
  });
}

createDynamicRoutes();

app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});

EJS模板

随着该启动和运行,唯一缺少的是EJS模板。

EJS或嵌入式JavaScript是一种模板引擎,允许我们通过将JavaScript直接嵌入模板中生成HTML内容。

使用.Render方法传递了模板中可以使用的数据,该方法由Express-Ejs-Layouts NPM软件包提供。在我们的情况下,这是执行的,当使用GenerateStaticsite函数生成静态页面时。

这使我们拥有以下模板:

indextemplatee.ejs

<!DOCTYPE html>
<html>
  <head>
    <title>Codesphere blog</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link href="./main.css" rel="stylesheet" />
    <link href="./index.css" rel="stylesheet" />
    <!-- Additional meta tags, styles, scripts... -->
    <script>
      // Your JavaScript filtering logic...
    </script>
  </head>
  <body>
    <%- include("/partials/header.ejs") %>
    <section class="w-full bg-dark-bg flex justify-center items-center pb-44 pt-60 -mt-20 h-fit relative">
        <% for(let i = 0; i < 1 && i < posts.length; i++) { %> 
            <% let post = posts[i]; %>
            <div class="container w-4/5 h-full flex flex-col align-middle items-center gap-12 mx-auto justify-between z-40 featured-post">
                <!-- Image Div -->
                <div class="h-auto object-cover flex flex-shrink-0 rounded-md overflow-hidden w-full lg:w-1/3 lg:items-start lg:justify-start">
                    <img class="h-auto object-cover m-0 w-full" src="<%= post.feature_image %>" alt="<%= post.title %>" />
                </div>
                <!-- Text Div -->
                <div class="flex-shrink flex gap-4 flex-col relative text-center    ">
                    <h2 class="text-white text-5xl font-semibold break-words"><%= post.title %></h2>
                    <p class="text-gray-500 text-sm"><%= new Date(post.published_at).toLocaleDateString('en-US') %></p>
                    <p class="text-gray-300 text-md font-300"><%= post.excerpt %></p>
                    <a class="text-white text-md font-semibold" href="/posts/<%= post.slug %>">Read the full article</a>

                </div>
            </div>
        <% } %>
    </section>

    <div class="container mx-auto px-8">
        <section class="w-full flex justify-start items-start pb-44 pt-60 -mt-20 h-fit relative flex-col">
            <h1 class="text-black text-4xl font-semibold break-words">All Articles</h1>
            <div class="posts-grid grid grid-cols-3 gap-8 pt-16">
              <% for(let i = 0; i < posts.length; i++) { %>
                  <div class="post-item flex w-full flex-col">
                      <!-- Image Div -->
                      <div class="post-image h-auto object-cover flex flex-shrink-0 rounded-md overflow-hidden w-full">
                          <img class="h-auto object-cover m-0 w-full" src="<%= posts[i].feature_image %>" alt="<%= posts[i].title %>" />
                      </div>
                      <!-- Text Div -->
                      <div class="post-text flex-shrink flex gap-4 flex-col relative text-left">
                          <h2 class="text-black text-2xl font-semibold break-words mt-4"><%= posts[i].title %></h2>
                          <p class="text-gray-500 text-sm"><%= new Date(posts[i].published_at).toLocaleDateString('en-US') %></p>
                          <p class="text-gray-500 text-md font-300"><%= posts[i].excerpt %></p>
                          <a class="text-black text-md font-semibold" href="/posts/<%= posts[i].slug %>">Read the full article</a>
                      </div>
                  </div>
              <% } %>
          </div>

        </section>
    </div>
  </body>
</html>

posteTemplate.ejs

<!DOCTYPE html>

<html>
    <head>
        <title>Codesphere blog</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link href="../main.css" rel="stylesheet" />
        <link href="../article.css" rel="stylesheet" />
        <!-- Additional meta tags, styles, scripts... -->
      </head>
    <body>
        <!DOCTYPE html>
        <html lang="en">

        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title><%= title %></title>
        </head>

        <body class="bg-gray-100">

            <%- include("/partials/header.ejs") %>
            <section class="text-gray-700 body-font">
                <div class="container mx-auto flex px-5 py-24 flex-col items-center">
                    <article class="mx-auto w-full max-w-2xl format format-sm sm:format-base lg:format-lg format-blue dark:format-invert">
                        <h1 class="title-font sm:text-4xl text-3xl mb-4 font-bold text-gray-900">
                            <%= title %>
                        </h1>
                        <p class="mb-8 leading-relaxed">
                            <%= excerpt %>
                        </p>
                        <div class="post-image h-auto object-cover flex flex-shrink-0 rounded-md overflow-hidden w-full">
                            <img class="h-auto object-cover m-0 w-full" src="<%= image %>" alt="<%= title %>" />
                        </div>
                        <div class="mt-16 w-full">
                            <%- content %>
                        </div>
                    </article>
                </div>
            </section>


        </body>
        </html>

    </body>
</html>

启动服务器

我们创建了一些脚本,这些脚本将根据您是否想在博客上积极工作或在服务器上运行以进行生产。

启动服务器并观看模板和CSS文件,运行:

npm watch:generate

要启动用于生产的服务器,请运行:

npm start

Or just use Codesphere to host your project in a matter of minutes!

在CodeSphere中设置

您现在可以在代码上免费部署静态博客。

只需打开一个new workspace using the GitHub Repo link,从UI运行CI步骤,您就完成了!

How to create a custom Static Site Generator for Blogs in 2023

包起来

总而言之,构建自定义静态站点生成器似乎是一项艰巨的任务,但是采用正确的方法,这是完全可行的。通过结合Node.js,Express,EJS和TailWindCSS,我们能够创建一个针对CodeSphere的特定需求而定制的精益,高效且可定制的生成器。此设置不仅提供了灵活性,而且还提供了静态站点固有的速度和安全好处。

快乐编码!