RaisFastRaisFast
SSG Integration

Comments & Search for Any SSG

Add real-time comments and full-text search to Hugo, Astro, Hexo, or any static site generator with RaisFast's universal API.

Overview

RaisFast provides a universal REST API for comments and full-text search that works with any static site generator. This guide shows you how to integrate both features into any SSG — Hugo, Astro, Hexo, Eleventy, Jekyll, Next.js static export, or anything else.

Your SSG  → generates static HTML
RaisFast  → comments API + search API (via fetch / JavaScript)

Prerequisites

Step 1: Start RaisFast

raisfast

The API is available at http://localhost:9898/api/v1.

Step 2: Add Comments (Universal)

Create a raisfast-comments.js file and include it on every page where you want comments:

<!-- Add to your article/post template -->
<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="Your name" required style={{display: "block", marginBottom: "8px", padding: "8px", width: "100%", maxWidth: "400px"}}>' +
      '<textarea name="content" placeholder="Write a comment..." required style={{display: "block", marginBottom: "8px", padding: "8px", width: "100%", maxWidth: "600px", minHeight: "80px"}}></textarea>' +
      '<button type="submit">Post Comment</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>Comments (' + 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();
          });
        });
    });
})();

Framework-specific page ID

The data-page-id attribute should uniquely identify each page:

SSGTemplate Variable
Hugo{{ .RelPermalink }}
Astro{Astro.url.pathname}
Hexo<%= page.path %>
Eleventy{{ page.url }}
Jekyll{{ page.url }}
Next.js (static)window.location.pathname

Step 3: Add Search (Universal)

Create a raisfast-search.js file and add a search input to your site:

<!-- Add to your layout header or navigation -->
<div id="raisfast-search">
  <input type="text" id="rf-search-input" placeholder="Search...">
  <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"}}>No results found.</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 = "";
    }
  });
})();

Step 4: Index Your Content

Before search works, you need to push your content into RaisFast's search index. Create a build script:

// scripts/index-content.js
// Run after: hugo build / astro build / hexo generate
// Usage: 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("Indexed " + htmlFiles.length + " pages.");

Add to your build pipeline:

# Build your static site first
hugo build        # or: astro build / hexo generate

# Then index content into RaisFast
node scripts/index-content.js ./public

Step 5: Deploy

Production Architecture

Internet
  ├── Nginx / Caddy (reverse proxy)
  │     ├── /            → static files (Hugo/Astro/Hexo output)
  │     └── /api/        → RaisFast (127.0.0.1:9898)

  └── RaisFast (API + Admin)
        ├── Comments API
        ├── Search API
        └── Admin panel at /admin

Nginx Config

server {
    listen 80;
    server_name yourdomain.com;

    # Static files from your 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 Admin (optional)
    location /admin {
        proxy_pass http://127.0.0.1:9898;
    }
}

Caddy Config

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

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

Customization

Styling the Comments Widget

Add your own CSS to match your site's design:

#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;
}

Styling the Search Widget

#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;
}

Going Further

On this page