171 lines
4.9 KiB
Markdown
171 lines
4.9 KiB
Markdown
# Spine — initial scaffold
|
|
|
|
A minimal working Emacs web server that serves templated HTML. This is the
|
|
first concrete slice of [spine-spec.md](../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
|
|
│ └── stop # stop the 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.el` is the sole Elisp file for now. Split later when a seam earns it.
|
|
- Templates live in `templates/` as `.mustache` files.
|
|
- 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-dir` resolves relative to `spine.el`'s directory so the app
|
|
works regardless of `default-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.
|
|
- `context` is an `ht` hash 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`:
|
|
|
|
```html
|
|
<!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 show `simple-httpd`'s default directory
|
|
listing — acceptable for a smoke test.
|
|
|
|
## Invocation
|
|
|
|
Production use — Emacs daemon with a named socket, no GUI frame:
|
|
|
|
```bash
|
|
scripts/run [HOST:]PORT
|
|
```
|
|
|
|
- `scripts/run 9090` → `http://localhost:9090/hello`
|
|
Stop: `scripts/stop`
|
|
|
|
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:
|
|
|
|
```bash
|
|
emacs --quick --load spine.el
|
|
```
|
|
|
|
`--quick` (aliases: `-Q`) skips init files for a clean, repeatable start.
|
|
|
|
## Acceptance
|
|
|
|
- [ ] `emacs --quick --load spine.el` starts without errors.
|
|
- [ ] Browsing `http://localhost:8080/hello` shows "Hello from Emacs."
|
|
- [ ] Templates load relative to `spine.el`'s directory, not `default-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.
|