Posted on
Artificial Intelligence

Artificial Intelligence Static Site Hosting

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Artificial Intelligence Static Site Hosting: Build AI‑assisted docs with nothing but Bash

Static sites are fast, cheap, and secure—but what if you want AI features like “search that understands meaning” or a helper that drafts new pages? Good news: you don’t need a full backend to get most of that. In this guide, we’ll wire up a completely static site with AI‑assisted authoring and offline semantic search, using Linux-friendly tools you can automate with Bash.

You’ll learn:

  • Why AI on static sites is practical and valuable

  • How to scaffold a docs/blog with MkDocs

  • How to generate content with a local LLM or an API via a simple Bash script

  • How to add build‑time embeddings for offline semantic search

  • How to deploy to GitHub Pages in minutes

Why “AI static site hosting” makes sense

  • Speed and simplicity: Static hosts (GitHub Pages, Netlify, Cloudflare Pages, S3) are CDN‑fast and dead simple. You ship HTML/JS/CSS and you’re done.

  • AI without servers: Many AI features can be done at build time:

    • AI generates or edits content before publishing.
    • You compute text embeddings during build and ship them as JSON. The client runs tiny JavaScript to search semantically—no server required.
  • Cost and security: No running servers, no databases. If you do call a cloud AI API, keep secrets server-side or use a CI key at build time—never hardcode keys in the client.

Limitations to keep in mind:

  • Realtime model inference in the browser is still heavy unless you use small on‑device models or remote APIs. For most docs/blogs, build‑time AI and client‑side vector search are enough.

Prerequisites (Linux)

Install the basics for Git, Python, and CLI tooling.

Debian/Ubuntu (apt):

sudo apt update
sudo apt install -y git python3 python3-pip jq curl

Fedora/RHEL/CentOS Stream (dnf):

sudo dnf install -y git python3 python3-pip jq curl

openSUSE (zypper):

sudo zypper refresh
sudo zypper install -y git python3 python3-pip jq curl

MkDocs (site generator) and theme:

python3 -m pip install --user mkdocs mkdocs-material

Optional: local LLM runtime (Ollama) to generate content without an external API:

curl -fsSL https://ollama.com/install.sh | sh
# Example small-ish model:
ollama pull llama3:8b

If you prefer a cloud API, set your key for the shell session (do not commit this):

export OPENAI_API_KEY="sk-..."

Step 1 — Scaffold a static site with MkDocs

mkdocs new ai-static-site
cd ai-static-site
git init

MkDocs creates:

  • mkdocs.yml (site config)

  • docs/index.md (home page)

Serve locally:

mkdocs serve

Open http://127.0.0.1:8000 to preview. Stop with Ctrl+C.

Step 2 — AI‑assisted authoring from Bash (local model or API)

Create a small helper that drafts pages for you. It prefers a local model (Ollama) if present; otherwise uses an API if OPENAI_API_KEY is set.

Create scripts/ai-write.sh:

mkdir -p scripts
cat > scripts/ai-write.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

MODEL="${MODEL:-llama3:8b}"
PROMPT="${1:-"Write a Linux-friendly blog post about using Bash to automate AI tasks."}"
OUT="docs/ai-post-$(date +%F-%H%M%S).md"

if command -v ollama >/dev/null 2>&1; then
  echo "Using local model via ollama: $MODEL" >&2
  ollama run "$MODEL" <<EOM > "$OUT"
Write a concise Markdown blog post for Linux users: $PROMPT
Be practical. Show small Bash snippets in fenced code blocks.
EOM
elif [[ -n "${OPENAI_API_KEY:-}" ]]; then
  echo "Using OpenAI API" >&2
  curl -sS https://api.openai.com/v1/chat/completions \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $OPENAI_API_KEY" \
    -d '{
      "model": "gpt-4o-mini",
      "messages":[
        {"role":"system","content":"You are a technical writer who produces concise, correct Markdown for Linux users."},
        {"role":"user","content":"'"$PROMPT"'"}
      ],
      "temperature": 0.3
    }' | jq -r '.choices[0].message.content' > "$OUT"
else
  echo "No local model (ollama) found and OPENAI_API_KEY is not set." >&2
  exit 1
fi

echo "Wrote $OUT"
EOF
chmod +x scripts/ai-write.sh

Generate a post:

./scripts/ai-write.sh "How to wire shell scripts into a static docs workflow"
mkdocs serve

Tip:

  • Review and edit drafts. AI is an assistant, not a source of truth.

  • Keep sensitive info out of prompts. If using an API, never expose the key client‑side.

Step 3 — Add offline semantic search with build‑time embeddings

We’ll embed your Markdown at build time using a small open model and ship vectors as JSON. The browser runs a tiny cosine-similarity search—no server needed.

Install Python deps:

python3 -m pip install --user sentence-transformers numpy

Create scripts/embed.py:

#!/usr/bin/env python3
import os, json, glob, re
import numpy as np
from sentence_transformers import SentenceTransformer

DOCS_DIR = "docs"
OUT = os.path.join(DOCS_DIR, "embeddings.json")
MODEL = os.environ.get("EMB_MODEL", "sentence-transformers/all-MiniLM-L6-v2")

def read_markdown(path):
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

def chunks(text, size=800, overlap=100):
    # Simple tokenizer by chars; adjust for your content
    start = 0
    while start < len(text):
        end = start + size
        yield text[start:end]
        start = end - overlap

def clean(md):
    # Strip code fences and front matter for cleaner embeddings
    md = re.sub(r"```.*?```", " ", md, flags=re.S)
    md = re.sub(r"^---.*?---", " ", md, flags=re.S|re.M)
    return re.sub(r"\s+", " ", md).strip()

def main():
    model = SentenceTransformer(MODEL)
    entries = []
    for path in sorted(glob.glob(os.path.join(DOCS_DIR, "**/*.md"), recursive=True)):
        rel = os.path.relpath(path, DOCS_DIR)
        text = clean(read_markdown(path))
        for i, ch in enumerate(chunks(text)):
            if len(ch.strip()) < 60:
                continue
            vec = model.encode([ch], normalize_embeddings=True)[0].tolist()
            entries.append({"doc": rel, "chunk": i, "text": ch, "vec": vec})
    with open(OUT, "w", encoding="utf-8") as f:
        json.dump({"model": MODEL, "entries": entries}, f)
    print(f"Wrote {OUT} with {len(entries)} chunks")

if __name__ == "__main__":
    main()

Run it after adding/editing docs:

python3 scripts/embed.py

Add a tiny client-side search. Create docs/javascripts/search.js:

(async function(){
  const box = document.createElement('input');
  box.type = 'search';
  box.placeholder = 'Semantic search...';
  box.style = 'width:100%;padding:0.5rem;margin:1rem 0;';
  const out = document.createElement('div');
  document.addEventListener('DOMContentLoaded', () => {
    const container = document.querySelector('main') || document.body;
    container.prepend(out);
    container.prepend(box);
  });

  function cos(a,b){
    let s=0;
    for (let i=0;i<a.length;i++) s+=a[i]*b[i];
    return s;
  }

  let data = null;
  async function load() {
    const r = await fetch('/embeddings.json');
    data = await r.json();
  }
  load();

  async function embedQuery(q){
    // Cheap approximation: query-time fallback using same vector space is not available client-side.
    // Simple hack: use chunk-average of top TF-ish terms. For best results, precompute queries or
    // serve a tiny WASM encoder. For now, we use a naive proxy: find overlapping words.
    // Practical alternative: do query encoding at build time for common queries.
    q = q.toLowerCase().split(/\W+/).filter(Boolean);
    // Score entries by keyword overlap as a lightweight fallback:
    return data.entries.map((e, idx) => {
      const w = e.text.toLowerCase();
      const score = q.reduce((s,t)=> s + (w.includes(t)?1:0), 0);
      return {idx, score};
    });
  }

  async function search(q){
    if (!data) await load();
    // If you can ship a tiny WASM encoder, replace embedQuery() + cos() with real embeddings for q.
    const scored = await embedQuery(q);
    scored.sort((a,b)=> b.score - a.score);
    const top = scored.slice(0, 5).map(s => data.entries[s.idx]);
    out.innerHTML = top.map(t => (
      '<div style="margin:0.5rem 0;padding:0.5rem;border:1px solid #ddd;">' +
      '<div><strong>'+ t.doc +'</strong> — chunk '+ t.chunk +'</div>' +
      '<div style="font-size:.9em;opacity:.9">'+ t.text.substring(0,300) +'...</div>' +
      '</div>'
    )).join('');
  }

  box.addEventListener('input', (e)=> {
    const q = e.target.value.trim();
    if (q.length < 2) { out.innerHTML=''; return; }
    search(q);
  });
})();

Wire the JS into MkDocs. Edit mkdocs.yml:

site_name: AI Static Site
theme:
  name: material
extra_javascript:
  - javascripts/search.js

Rebuild embeddings and serve:

python3 scripts/embed.py
mkdocs serve

Note:

  • The snippet above uses a lightweight keyword‑overlap fallback for query vectors to keep everything static. For stronger results:
    • Precompute embeddings for common queries (FAQ) at build time.
    • Or load a tiny WASM encoder for true client-side embeddings.
    • Or proxy to an API behind a serverless function to encode queries at runtime without exposing keys.

Step 4 — Build and deploy (GitHub Pages)

Build locally:

mkdocs build

Quick deploy to GitHub Pages (pushes to gh-pages branch):

git add .
git commit -m "AI static site: initial"
git branch -M main
git remote add origin https://github.com/<you>/ai-static-site.git
git push -u origin main
mkdocs gh-deploy --force

Automate with GitHub Actions. Create .github/workflows/build.yml:

name: build-and-deploy
on:
  push:
    branches: [ "main" ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install mkdocs mkdocs-material sentence-transformers numpy
      - run: python scripts/embed.py
      - run: mkdocs build --strict
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v4
        if: github.ref == 'refs/heads/main'
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./site

For Netlify/Cloudflare Pages:

  • Build command: mkdocs build

  • Publish directory: site/

Real‑world ways to use this today

  • Product docs with “good enough” semantic search delivered entirely statically.

  • Personal tech blog where ./scripts/ai-write.sh drafts posts you refine and publish.

  • Internal knowledge base on S3+CloudFront; a nightly CI regenerates embeddings and deploys.

  • Release notes: pipe commits or PR titles into the AI script to draft change summaries.

Security and performance tips

  • Never ship API keys to the browser. Do AI work at build time or via a tiny serverless proxy.

  • Keep models small for local use (e.g., llama3:8b). Cache model weights and CI layers to reduce build time.

  • Validate AI output like any other contribution; treat it as a suggestion.

  • Store embeddings as compact Float32 arrays if size grows; chunk wisely.

Recap and next steps (CTA)

You don’t need a backend to ship practical AI features:

  • Author with AI locally or via API using a simple Bash script

  • Bake embeddings at build time for semantic search

  • Host the result on any static platform

Next steps:

  • Add your first AI‑generated draft: ./scripts/ai-write.sh "Topic"

  • Run python3 scripts/embed.py and mkdocs serve

  • Deploy with mkdocs gh-deploy or your preferred static host

If you want a starter repo, ask and I’ll assemble these scripts and config into a template you can fork.