Building slop.at: A Minimal Semantic Web Publisher
What We Built
A complete publishing system that takes markdown, extracts entities, builds a knowledge graph, and renders beautiful HTML - all with zero manual tagging.
The Stack
Frontend:
- FastAPI web server
- Mistune for markdown rendering
- American Typewriter font (because corporate sans-serif is boring)
- CSS with opacity-based entity highlighting
Backend:
- Oxigraph CLI for RDF triple store
- GLiNER2 for automatic entity extraction
- MCP server for Claude integration
- Git + GitHub for storage and version control
Security:
- HTML escaping to prevent XSS
- 1MB size limit per slop
- Rate limiting: 20 slops/minute per IP
- No authentication needed (keep it simple)
The Flow
- Write markdown in Claude or Obsidian
- MCP extracts entities using GLiNER2 (37 entities from our longest doc!)
- Build RDF graph with know.dev ontology
- Push to GitHub for permanent storage
- POST to web server which renders + stores in Oxigraph
- View at slop.at/s/{hash} with color-coded entity highlighting
Key Design Decisions
GitHub as Storage
User owns their data, not us. If slop.at goes down, their content is safe in their repo. Also: free hosting, version control, and Obsidian integration.
8-Character Slop IDs
Changed from full UUIDs to just the first 8 characters. Cleaner URLs, still collision-resistant for any realistic usage.
Frontmatter at Bottom
Spent way too many iterations getting the YAML frontmatter spacing right. Final solution: hide it with display: none initially, then moved to bottom with minimal styling when user requested it back.
No API Keys
Rejected environment variable approach. Too much friction, leaks into logs. Went with "deploy it and see if it gets abused" strategy. Can always add GitHub webhook auth later.
Opacity = Confidence
Entity highlighting uses opacity to show GLiNER2's confidence:
- 0.9+ confidence = opacity 1.0 (solid color)
- 0.7-0.9 = opacity 0.7 (medium)
- <0.7 = opacity 0.4 (faded)
Lets you see which extractions are solid vs tentative.
What Worked Surprisingly Well
GLiNER2 entity extraction - Caught "Poincaré Embeddings", "Hyperbolic Space", "GraphCodeBERT", and tons of technical terms with zero training on our docs. Just works.
Mistune markdown rendering - Fast, supports plugins, has HTML escaping built in. Perfect fit.
FastAPI + Oxigraph - Simple architecture: POST receives data, stores JSON + pushes to graph. GET reads JSON, highlights entities, renders HTML. That's it.
American Typewriter font - Looks way better than sans-serif for reading long technical docs. Personality matters.
The Iteration That Took Forever
Frontmatter spacing. Multiple rounds of:
- "still too much space"
- "please. really. Just take the space around the front matter away."
- "ok look. Can you just make that section visible = false or something."
Final fix: margin: 0; padding: 0; everywhere + display: none. Then brought it back at the bottom with margin-top: 2rem; padding-top: 2rem;. Perfectly balanced.
What We Tested
Posted 5 dense technical documents (ontology gardening, malware detection, SPARQL explorations, parser testing, LSP integration). Total entity extractions:
- 37 entities (ontology doc)
- 23 entities (LSP strategy)
- 21 entities (parser testing)
- 15 entities (malware detection)
- 13 entities (SPARQL results)
All rendered cleanly with color-coded highlights.
Security Hardening
Quick wins we added:
- HTML escaping in mistune (
escape=True) - Content size limits (1MB default)
- Rate limiting (20/minute per IP)
What we explicitly rejected:
- API keys via environment variables (too annoying)
- Complex authentication (not needed yet)
- Over-engineering before we have users
What's Next
From TODO.md:
- Clickable entities (show details on click)
- Word-list highlighting (catch ALL instances, not just what GLiNER2 found)
- Relationship arrows between entities
- Date/Time entity extraction
- GitHub webhook integration (proper multi-user)
The Philosophy
Boring technology that works.
- RDF because it's standard
- Oxigraph because it's fast and embeddable
- FastAPI because it's simple
- Mistune because it's reliable
- Git because everyone already has it
No blockchain, no AI hype, no unnecessary complexity. Just: write markdown → get knowledge graph → publish beautiful HTML.
Lessons Learned
1. Start with hardening, not auth HTML escaping and size limits are easy wins. Auth is complex and can wait.
2. User owns data = better UX GitHub storage means users trust the system more. They can clone, backup, edit in any tool.
3. Opacity for confidence is intuitive Bright colors = high confidence. Faded = uncertain. No numbers needed.
4. 8 characters is plenty UUIDs are overkill. Short hashes are friendlier and collision-resistant enough.
5. Fonts matter American Typewriter makes slop.at feel different from every other markdown renderer. Character counts.
The Best Part
Spent hours fighting CSS spacing, but when it clicked: zero padding achieved! 🐐✨
That moment when the frontmatter finally tucked itself nicely at the bottom with perfect spacing... chef's kiss.
Built in one session. From "let's start fresh" to "this is actually pretty solid." The power of simple tools and clear constraints.