Initialize bincio_wiki project structure
- Add brutsalvadi/astro-bloomz as git submodule at site/ - Add edit/ FastAPI sidecar (read/write markdown pages, trigger rebuild) - Add scripts/dev.sh and scripts/build.sh (symlink pages/, run Astro + Pagefind) - Add .gitignore
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Site build output (managed by submodule)
|
||||||
|
site/dist/
|
||||||
|
site/node_modules/
|
||||||
|
site/.astro/
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "site"]
|
||||||
|
path = site
|
||||||
|
url = https://github.com/brutsalvadi/astro-bloomz
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fastapi>=0.115.0
|
||||||
|
uvicorn[standard]>=0.30.0
|
||||||
|
pydantic>=2.0.0
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
"""FastAPI edit sidecar for BincioWiki — reads and writes markdown pages."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
# Resolved at startup relative to the project root (one level above this file)
|
||||||
|
_ROOT = Path(__file__).parent.parent
|
||||||
|
pages_dir: Path = _ROOT / os.environ.get("WIKI_PAGES_DIR", "pages")
|
||||||
|
site_dir: Path = _ROOT / "site"
|
||||||
|
|
||||||
|
_SAFE_SLUG = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_\-/]*$")
|
||||||
|
|
||||||
|
app = FastAPI(title="BincioWiki Edit Sidecar", docs_url=None, redoc_url=None)
|
||||||
|
|
||||||
|
app.add_middleware(GZipMiddleware, minimum_size=1024)
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origin_regex=r"https?://localhost(:\d+)?",
|
||||||
|
allow_methods=["GET", "POST", "DELETE"],
|
||||||
|
allow_headers=["Content-Type"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _slug_to_path(slug: str) -> Path:
|
||||||
|
if not _SAFE_SLUG.match(slug):
|
||||||
|
raise HTTPException(400, "Invalid page slug — only alphanumeric, hyphens, underscores, and slashes allowed")
|
||||||
|
resolved = (pages_dir / f"{slug}.md").resolve()
|
||||||
|
if not str(resolved).startswith(str(pages_dir.resolve())):
|
||||||
|
raise HTTPException(400, "Path traversal detected")
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
class PageBody(BaseModel):
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/pages")
|
||||||
|
async def list_pages() -> JSONResponse:
|
||||||
|
"""Return all wiki page slugs."""
|
||||||
|
if not pages_dir.exists():
|
||||||
|
return JSONResponse({"pages": []})
|
||||||
|
slugs = [
|
||||||
|
str(p.relative_to(pages_dir).with_suffix(""))
|
||||||
|
for p in sorted(pages_dir.rglob("*.md"))
|
||||||
|
]
|
||||||
|
return JSONResponse({"pages": slugs})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/pages/{slug:path}")
|
||||||
|
async def get_page(slug: str) -> JSONResponse:
|
||||||
|
"""Return raw markdown content for a page."""
|
||||||
|
path = _slug_to_path(slug)
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(404, "Page not found")
|
||||||
|
return JSONResponse({"slug": slug, "content": path.read_text(encoding="utf-8")})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/pages/{slug:path}")
|
||||||
|
async def save_page(slug: str, body: PageBody) -> JSONResponse:
|
||||||
|
"""Create or update a wiki page."""
|
||||||
|
path = _slug_to_path(slug)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(body.content, encoding="utf-8")
|
||||||
|
return JSONResponse({"slug": slug, "saved": True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/pages/{slug:path}")
|
||||||
|
async def delete_page(slug: str) -> JSONResponse:
|
||||||
|
"""Delete a wiki page."""
|
||||||
|
path = _slug_to_path(slug)
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(404, "Page not found")
|
||||||
|
path.unlink()
|
||||||
|
return JSONResponse({"slug": slug, "deleted": True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rebuild")
|
||||||
|
async def rebuild() -> JSONResponse:
|
||||||
|
"""Trigger an astro build of the site."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["npm", "run", "build"],
|
||||||
|
cwd=site_dir,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
return JSONResponse({
|
||||||
|
"success": result.returncode == 0,
|
||||||
|
"stdout": result.stdout[-3000:] if result.stdout else "",
|
||||||
|
"stderr": result.stderr[-3000:] if result.stderr else "",
|
||||||
|
})
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise HTTPException(504, "Build timed out after 120s")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the static wiki site and run Pagefind for search indexing.
|
||||||
|
# Usage: scripts/build.sh
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
|
||||||
|
# Symlink wiki pages into the site's content collection
|
||||||
|
mkdir -p pages
|
||||||
|
mkdir -p site/src/content/entries
|
||||||
|
LINK="$ROOT/site/src/content/entries/_wiki"
|
||||||
|
if [ ! -L "$LINK" ]; then
|
||||||
|
ln -sf "$ROOT/pages" "$LINK"
|
||||||
|
echo "Linked pages/ → site/src/content/entries/_wiki"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build Astro site
|
||||||
|
cd site && npm run build
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
# Run Pagefind to index the static output
|
||||||
|
if command -v pagefind &>/dev/null; then
|
||||||
|
echo "Running Pagefind..."
|
||||||
|
pagefind --site site/dist
|
||||||
|
else
|
||||||
|
echo "Pagefind not installed — skipping search index (npm install -g pagefind)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Build complete: site/dist/"
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Start the Astro dev server and optionally the edit sidecar.
|
||||||
|
# Usage: scripts/dev.sh [--edit]
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
|
||||||
|
# Symlink wiki pages into the site's content collection
|
||||||
|
mkdir -p pages
|
||||||
|
mkdir -p site/src/content/entries
|
||||||
|
LINK="$ROOT/site/src/content/entries/_wiki"
|
||||||
|
if [ ! -L "$LINK" ]; then
|
||||||
|
ln -sf "$ROOT/pages" "$LINK"
|
||||||
|
echo "Linked pages/ → site/src/content/entries/_wiki"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start edit sidecar if requested
|
||||||
|
if [[ "$*" == *"--edit"* ]]; then
|
||||||
|
echo "Starting edit sidecar on :8001..."
|
||||||
|
(cd "$ROOT" && uvicorn edit.server:app --reload --port 8001) &
|
||||||
|
SIDECAR_PID=$!
|
||||||
|
trap "kill $SIDECAR_PID 2>/dev/null" EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Astro dev server
|
||||||
|
export PUBLIC_EDIT_URL="${WIKI_EDIT_URL:-}"
|
||||||
|
cd site && npm run dev
|
||||||
Submodule
+1
Submodule site added at 2d51d17f55
Reference in New Issue
Block a user