RaisFastRaisFast
SSG Integration

通用评论和搜索集成

使用 RaisFast 的通用 API 为 Hugo、Astro、Hexo 或任何静态站点生成器添加实时评论和全文搜索功能。

概述

RaisFast 提供了通用 REST API 来实现评论和全文搜索,适用于任何静态站点生成器。本指南展示如何将这两个功能集成到任何 SSG — Hugo、Astro、Hexo、Eleventy、Jekyll、Next.js 静态导出,或其他任何生成器。

你的 SSG  → 生成静态 HTML
RaisFast  → 评论 API + 搜索 API(通过 fetch / JavaScript)

前提条件

步骤 1:启动 RaisFast

raisfast

API 地址为 http://localhost:9898/api/v1

步骤 2:添加评论(通用方案)

创建 raisfast-comments.js 文件,并在需要评论的页面中引入:

<!-- 添加到文章模板 -->
<div id="raisfast-comments" data-page-id="{{ .RelPermalink }}"></div>
<script src="/js/raisfast-comments.js"></script>
// static/js/raisfast-comments.js
(function () {
  var API_BASE = "http://localhost:9898/api/v1";
  var container = document.getElementById("raisfast-comments");
  if (!container) return;

  var pageId = container.dataset.pageId;

  function renderComments(items) {
    return items
      .map(function (c) {
        return (
          '<div class="rf-comment" style={{marginBottom: "16px"}}>' +
          "<strong>" +
          c.author_name +
          "</strong>" +
          "<span style={{marginLeft: "8px", color: "#999", fontSize: "0.85em"}}>" +
          new Date(c.created_at).toLocaleDateString() +
          "</span>" +
          "<p>" +
          c.content +
          "</p>" +
          "</div>"
        );
      })
      .join("");
  }

  function renderForm() {
    return (
      '<form id="rf-comment-form" style={{"marginTop": "16px"}}>' +
      '<input name="author_name" placeholder="你的名字" required style={{display: "block", marginBottom: "8px", padding: "8px", width: "100%", maxWidth: "400px"}}>' +
      '<textarea name="content" placeholder="写下你的评论..." required style={{display: "block", marginBottom: "8px", padding: "8px", width: "100%", maxWidth: "600px", minHeight: "80px"}}></textarea>' +
      '<button type="submit">提交评论</button>' +
      "</form>"
    );
  }

  fetch(API_BASE + "/comments?post_id=" + encodeURIComponent(pageId))
    .then(function (r) {
      return r.json();
    })
    .then(function (data) {
      var items = (data.data && data.data.items) || [];
      container.innerHTML =
        '<h3>评论 (' + items.length + ")</h3>" +
        renderComments(items) +
        renderForm();

      document
        .getElementById("rf-comment-form")
        .addEventListener("submit", function (e) {
          e.preventDefault();
          var form = this;
          fetch(API_BASE + "/comments", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              post_id: pageId,
              author_name: form.author_name.value,
              content: form.content.value,
            }),
          }).then(function () {
            location.reload();
          });
        });
    });
})();

各框架的 page ID 获取方式

data-page-id 属性需要能唯一标识每个页面:

SSG模板变量
Hugo{{ .RelPermalink }}
Astro{Astro.url.pathname}
Hexo<%= page.path %>
Eleventy{{ page.url }}
Jekyll{{ page.url }}
Next.js(静态)window.location.pathname

步骤 3:添加搜索(通用方案)

创建 raisfast-search.js 文件,并在站点中添加搜索输入框:

<!-- 添加到布局头部或导航栏 -->
<div id="raisfast-search">
  <input type="text" id="rf-search-input" placeholder="搜索...">
  <div id="rf-search-results"></div>
</div>
<script src="/js/raisfast-search.js"></script>
// static/js/raisfast-search.js
(function () {
  var API_BASE = "http://localhost:9898/api/v1";
  var input = document.getElementById("rf-search-input");
  var results = document.getElementById("rf-search-results");
  if (!input || !results) return;

  var debounceTimer;

  input.addEventListener("input", function () {
    var query = this.value.trim();
    clearTimeout(debounceTimer);

    if (query.length < 2) {
      results.innerHTML = "";
      return;
    }

    debounceTimer = setTimeout(function () {
      fetch(
        API_BASE + "/search?q=" + encodeURIComponent(query)
      )
        .then(function (r) {
          return r.json();
        })
        .then(function (data) {
          var items = (data.data && data.data.items) || [];
          if (items.length === 0) {
            results.innerHTML = '<p style={{"padding": "8px", "color": "#999"}}>没有找到结果。</p>';
            return;
          }
          results.innerHTML = items
            .map(function (p) {
              return (
                '<a href="' +
                p.url +
                '" style={{display: "block", padding: "8px", textDecoration: "none", color: "inherit"}}>' +
                "<strong>" +
                p.title +
                "</strong>" +
                "<p style={{margin: "4px 0 0", fontSize: "0.85em", color: "#666"}}>" +
                p.excerpt +
                "</p>" +
                "</a>"
              );
            })
            .join("");
        });
    }, 300);
  });

  document.addEventListener("click", function (e) {
    if (
      !input.contains(e.target) &&
      !results.contains(e.target)
    ) {
      results.innerHTML = "";
      input.value = "";
    }
  });
})();

步骤 4:索引内容

搜索功能需要在构建后将内容推送到 RaisFast 的搜索索引中。创建构建脚本:

// scripts/index-content.js
// 在 hugo build / astro build / hexo generate 之后运行
// 用法: node scripts/index-content.js

var fs = require("fs");
var path = require("path");
var API_BASE = "http://localhost:9898/api/v1";

function walkDir(dir, ext) {
  var results = [];
  var list = fs.readdirSync(dir);
  list.forEach(function (file) {
    var filePath = path.join(dir, file);
    var stat = fs.statSync(filePath);
    if (stat && stat.isDirectory()) {
      results = results.concat(walkDir(filePath, ext));
    } else if (file.endsWith(ext)) {
      results.push(filePath);
    }
  });
  return results;
}

function extractTitle(html) {
  var match = html.match(/<h1[^>]*>(.*?)<\/h1>/);
  return match ? match[1] : "";
}

function stripHtml(html) {
  return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
}

var outputDir = process.argv[2] || "./public";
var htmlFiles = walkDir(outputDir, ".html");

htmlFiles.forEach(function (file) {
  var html = fs.readFileSync(file, "utf-8");
  var title = extractTitle(html) || path.basename(file, ".html");
  var content = stripHtml(html).substring(0, 5000);
  var url = "/" + path.relative(outputDir, file);

  fetch(API_BASE + "/search/index", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      title: title,
      content: content,
      url: url,
    }),
  });
});

console.log("已索引 " + htmlFiles.length + " 个页面。");

将脚本加入构建流程:

# 先构建静态站点
hugo build        # 或: astro build / hexo generate

# 然后索引内容到 RaisFast
node scripts/index-content.js ./public

步骤 5:部署

生产架构

Internet
  ├── Nginx / Caddy(反向代理)
  │     ├── /            → 静态文件(Hugo/Astro/Hexo 输出)
  │     └── /api/        → RaisFast(127.0.0.1:9898)

  └── RaisFast(API + 管理后台)
        ├── 评论 API
        ├── 搜索 API
        └── 管理后台 /admin

Nginx 配置

server {
    listen 80;
    server_name yourdomain.com;

    # SSG 静态文件
    location / {
        root /var/www/site/public;
        try_files $uri $uri/ /index.html;
    }

    # RaisFast API
    location /api/ {
        proxy_pass http://127.0.0.1:9898;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # RaisFast 管理后台(可选)
    location /admin {
        proxy_pass http://127.0.0.1:9898;
    }
}

Caddy 配置

yourdomain.com {
    root * /var/www/site/public
    try_files {path} /index.html
    file_server

    reverse_proxy /api/* localhost:9898
    reverse_proxy /admin localhost:9898
}

自定义样式

评论组件样式

添加自定义 CSS 以匹配你的站点设计:

#raisfast-comments {
  max-width: 720px;
  margin: 2rem auto;
}

.rf-comment {
  padding: 12px 0;
  border-bottom: 1px solid #eee;
}

#rf-comment-form input,
#rf-comment-form textarea {
  border: 1px solid #ddd;
  border-radius: 4px;
  font-family: inherit;
}

#rf-comment-form button {
  background: #0070f3;
  color: white;
  border: none;
  padding: 8px 24px;
  border-radius: 4px;
  cursor: pointer;
}

搜索组件样式

#raisfast-search {
  position: relative;
}

#rf-search-input {
  padding: 8px 16px;
  border: 1px solid #ddd;
  border-radius: 8px;
  width: 240px;
  font-size: 14px;
}

#rf-search-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  max-height: 400px;
  overflow-y: auto;
  z-index: 100;
}

#rf-search-results a:hover {
  background: #f5f5f5;
}

延伸阅读

On this page