5.0 KiB
Spine — initial scaffold
A minimal working Emacs web server that serves templated HTML. This is the first concrete slice of spine-spec.md, resolving the open "glue language + web framework" decision: Emacs owns the HTTP socket and serves the UI directly.
Architecture decision
Emacs end-to-end. No external web process, no emacsclient IPC for reads.
The app is a single Emacs instance running simple-httpd. This keeps the build
simple (one language, two packages) and is adequate for Spine's complexity level.
Packages
| Package | Version | Source | Purpose |
|---|---|---|---|
simple-httpd |
MELPA latest | MELPA | HTTP server, defservlet routing |
mustache |
MELPA latest | MELPA | Mustache template rendering |
ht |
MELPA latest | MELPA | Hash table for template context |
Project structure
spine/
├── scripts/
│ └── run # start the server as a daemon
├── spine.el # entry point: bootstrap, server start, handlers
├── templates/
│ └── hello.mustache # smoke-test template
├── spine-spec.md # design brief (existing)
├── spine-mockup-a-agenda.html
├── spine-mockup-b-journal.html
└── README.md
spine.elis the sole Elisp file for now. Split later when a seam earns it.- Templates live in
templates/as.mustachefiles. - Mockups stay as reference artifacts; they are not served.
Package bootstrap
spine.el uses package.el (built-in). On first run it installs missing
packages from MELPA; subsequent starts skip the install step.
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)
(dolist (pkg '(simple-httpd mustache ht))
(unless (package-installed-p pkg)
(package-refresh-contents)
(package-install pkg)))
package-refresh-contents runs only when a package is missing — not on every
start.
Server startup
(defvar spine-port 8080
"Port for the Spine HTTP server.")
(defvar spine-host nil
"Host for the Spine HTTP server.
nil means localhost only. Set to a string like \"0.0.0.0\" to listen on
all interfaces.")
(defvar spine-template-dir
(expand-file-name "templates/" (file-name-directory load-file-name))
"Directory containing .mustache templates.")
(setq httpd-host spine-host)
(setq httpd-port spine-port)
(httpd-start)
(message "Spine listening on http://%s:%d"
(or spine-host "localhost") spine-port)
- Port defaults to 8080, overridable via
spine-port. - Host defaults to nil (localhost-only), overridable via
spine-host. spine-template-dirresolves relative tospine.el's directory so the app works regardless ofdefault-directory.
Template rendering
A single helper wraps Mustache rendering:
(defun spine-render (name context)
"Render templates/NAME.mustache with CONTEXT (an ht hash table).
Returns the rendered string."
(mustache-render
(with-temp-buffer
(insert-file-contents
(expand-file-name name spine-template-dir))
(buffer-string))
context))
- Reads the template file from disk on every request. No caching — correct and simple; add caching if profiling shows a need.
contextis anhthash table whose keys map to{{key}}placeholders.
Hello World handler
(defservlet hello text/html ()
(insert (spine-render "hello.mustache"
(ht ("title" "Spine")
("message" "Hello from Emacs.")))))
templates/hello.mustache:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{title}}</title>
</head>
<body>
<h1>{{message}}</h1>
</body>
</html>
- Accessible at
http://localhost:8080/hello. - The root
/is unbound and will showsimple-httpd's default directory listing — acceptable for a smoke test.
Invocation
Production use — Emacs daemon with a named socket, no GUI frame:
scripts/run [HOST:]PORT
scripts/run 9090→http://localhost:9090/helloscripts/run 0.0.0.0:8080→http://0.0.0.0:8080/hello(all interfaces)scripts/run→http://localhost:8080/hello
Stop: emacsclient --socket-name=spine --eval '(kill-emacs)'
The socket name spine is separate from the default Emacs daemon socket,
so your personal daemon is unaffected.
Development use — interactive Emacs in the terminal:
emacs --quick --load spine.el
--quick (aliases: -Q) skips init files for a clean, repeatable start.
Acceptance
emacs --quick --load spine.elstarts without errors.- Browsing
http://localhost:8080/helloshows "Hello from Emacs." - Templates load relative to
spine.el's directory, notdefault-directory. - Second invocation skips package install (packages already present).
Out of scope
- Book data, Org file integration, any handler beyond
/hello. - Static file serving (CSS, JS, icons).
- Tests — the scaffold is small enough to smoke-test manually.