Waymark:自托管的滚动地图故事编辑器
Interactive Maps for Storytelling
Waymark 是一款自托管编辑器,用于制作随滚动移动的地图故事,只需 Node.js 22.13 或更新版本,无需 npm install,git clone 后运行 npm start 即可使用。它内置 NOAA、NASA、USGS 等实时政府数据图层,支持筛选、缓冲区与六边形分箱等分析工具,并可绘制点线面标注。故事可发布为公开链接,或导出为纯静态网站文件,部署到任意 Web 服务器。
Write a story. Scroll it. Publish it anywhere.
Waymark is a self-hosted editor for scroll-driven map stories.
Add maps that move as you read, live government data, and photos, then share a link or export a website.
Quick start · Features · Export · Self-hosting · Story format
⚡ Start in a minute
You only need Node.js 22.13 or newer. There's nothing to npm install.
git clone https://github.com/chrispy90/waymark.git
cd waymark
npm startThen open http://localhost:5180.
- Create your account. The first visit makes you the owner.
- Answer a few questions. Pick what you're making and a look, add a title and your places, and Waymark sets up the story for you.
- Click anything to edit it. When you're happy with it, press Publish.
Tip
Windows: if PowerShell says "running scripts is disabled", run node server.js instead. It does the same thing.
Docker: docker compose up -d also works, and keeps your data in a named volume.
✨ What you can make
A map that moves with the story
The map stays put while story cards scroll past, and it glides to each card's place. You can tilt it, rotate it, switch layers, or swap it for a photo or video on any card.
Scroll effects, built in
Effects from the best long-form sites, available as blocks. Add one and it works on phones too. Each effect switches off for readers who ask their device to reduce motion.
You also get chapters that recolor the page, big numbers that count up, and parallax, zoom and wipe-in photos and covers.
Live data from NOAA, NASA, USGS and more
Open Browse public data and click Add. Every layer arrives styled, with popups, a legend and credit to its source. No keys and no sign-ups.
| Source | Layers |
|---|---|
| USGS | Earthquakes (live) · stream gauges by state |
| NOAA | Weather warnings (live) · radar (live) · tide stations · tsunamis · significant earthquakes · volcanoes · plate boundaries |
| NASA | Natural events (EONET) · daily satellite imagery · Earth at night |
| Census · NPS · USFS · EPA · NIFC | States and counties · park units · national forests · Superfund sites · active wildfires and perimeters |
| Natural Earth | Countries · cities · rivers |
| ArcGIS Online | Search thousands more public layers |
Live layers refresh every time someone opens the story, so a published map stays current.
Filter it, then analyze it
Filters narrow a layer with plain rules, like magnitude ≥ 5, in the last 7 days or state = Texas. They apply to the map, the insights, the analysis and the export.
Analyze previews every result on a map before you keep it. The tools are buffer, hex bins, count in areas, distance to nearest, within distance, center points, outline and filter. Results become ordinary layers, and you can re-run them on fresh data.
You can also show points as a heatmap or clusters. Bring your own data too: GeoJSON, CSV, or any public ArcGIS layer.
Draw right on the map
Press Draw on any map in the editor and sketch points, lines, freehand routes, areas, boxes, circles and text labels right on it. Give each shape its own colors, fill, line width, dashes or dots, and an arrowhead, plus a name and a description that readers see when they click it. Distances and areas are measured as you draw. Select a shape to move, restyle or duplicate it, trace over any other layer, and reopen a saved drawing whenever you like.
An editor that gets out of the way
Click anything in the live preview to edit it. You also get drag to reorder, undo and redo, a command palette (Ctrl/⌘+K), a phone preview and autosave. Drafts stay private until you publish again.
Make it loud: the Brutalist look
Switch any story to Brutalist for raw monospace and heavy display type, hard black edges and offset shadows, acid-yellow stickers, a ticker headline, outlined words that fill in as you scroll, and numbered chapters. Or start from the Brutalist zine template.
Every story works on a phone
Start with a few questions, not a demo to delete
New story asks what you're making, how it should look, its title and cover, and where it goes. Then it builds the story around your places and chapters, with short prompts where your words go. A checklist in the editor walks you through the rest: cover photo, your own text, photos, a phone check and publishing.
| Kind of story | What you get |
|---|---|
| A journey | A moving map with one card per place, plus an overview of the whole route |
| A walking tour | Numbered stops with room for a photo and a description each |
| An illustrated essay | Your chapters as full-screen titles, with space to write and a photo |
| A data story | One big map ready for live or uploaded data, with numbers and context |
| Start from scratch | A title and an empty page |
Every kind comes in five looks. Each one sets the type, colors, chapter colors, map style and scroll animation. You can switch later under Story → Look & feel, and restyle existing chapters and maps to match in one click.
| Look | Style |
|---|---|
| Classic | Warm paper and serif type. The original Waymark look |
| Night | Dark and cinematic, so photos and maps glow |
| Brutalist | Raw type, hard edges, loud color and a ticker headline |
| Atlas | An old atlas: parchment, engraved type and small caps, sepia maps in a double frame with a compass rose, and chapters numbered as plates |
| Aurora | Dark glass and drifting light, glowing gradient type, frosted cards and bento-style numbers. Points and lines on its maps glow like fireflies |
Prefer to learn from something finished? Explore an example opens a complete story to take apart: the moving map, walk, essay, Brutalist zine and Down the Potomac samples. Any story can also use Before & after, a block with two maps in one view and a draggable divider.
🚀 Publish it anywhere
Publish gives a story a public link like /s/your-story, with a preview image when the link is shared.
Export as a website turns a story into plain files that run on any web server, with no Node, no database and no Waymark.
| Format | Use it for |
|---|---|
| Website folder (.zip) | Apache, nginx, IIS, shared hosting, S3, GitHub Pages, Netlify, or just double-click index.html |
| Tomcat web app (.war) | Drop it in Tomcat's webapps/ folder (Tomcat 8.5 to 11) |
The export includes the page, your images, a snapshot of your map data and, optionally, the map library. Every path is relative, so it works in any folder. You can also export from the command line:
node server.js export my-story --war --out /opt/tomcat/webapps/my-story.warAll export options
node server.js stories # find the story's link or id node server.js export my-story # published version, as a .zip node server.js export my-story --draft # the unpublished draft
Options: --war, --draft, --out <file>, --no-images, --no-data and --no-vendor. The last three keep images, data or the map library as links instead of copying them. Only the map tiles and web fonts load from the internet.
🏠 Host it yourself
Your stories, accounts and images all live in one data/ folder. Waymark runs anywhere that has Node 22.13+ or Docker and a persistent disk: a VPS, a home server, Fly.io, Railway, Render and so on.
- Put it behind HTTPS. Caddy is the easiest:
caddy reverse-proxy --from stories.example.com --to localhost:5180 - In
.env, setAPP_URL=https://stories.example.comandCOOKIE_SECURE=1. - Once your accounts exist, set
ALLOW_SIGNUP=0to close sign-ups. - Back up
data/. It holds everything.
Settings (.env)
Copy .env.example to .env and edit it. Everything is optional.
| Setting | Default | What it does |
|---|---|---|
APP_NAME, APP_TAGLINE |
Waymark | Rename the app (interface, emails, link previews) |
PORT |
5180 |
Port to listen on |
HOST |
this computer only | Set to 0.0.0.0 to let other devices on your network connect |
APP_URL |
http://localhost:PORT |
Your site's public address, used in password reset emails |
DATA_DIR |
./data |
Where the database (storymaps.db) and uploaded images are stored |
COOKIE_SECURE |
off | Set to 1 when the site is served over HTTPS |
ALLOW_SIGNUP |
on | Set to 0 to stop new sign-ups once your accounts exist |
SMTP_* |
off | Email for password resets. See below |
Password reset and email
People can reset a forgotten password from the sign-in page. To deliver the emails, set the SMTP_* settings in .env. Any SMTP provider works, for example Postmark, Resend, SendGrid, Mailgun, or a Gmail app password.
Without email set up, the reset link prints in the terminal where the server runs. That's fine for personal or local use. You can also reset passwords yourself from the command line:
npm run users # list accounts npm run reset-link -- someone@example.com # print a reset link npm run set-password -- someone@example.com # type a new password directly
Other ways to run it
npm run dev |
Restarts automatically when you edit server code |
npm test |
Runs the test suite against a temporary database |
docker compose up -d |
Runs in Docker, with data kept in a named volume |
📚 Under the hood
Waymark is plain JavaScript with no build step and no dependencies. The server uses only Node's built-in modules, including node:sqlite. The browser app is ordinary ES modules. Maps are drawn with MapLibre GL JS on free OpenFreeMap and Sentinel-2 basemaps.
Project layout
server.js entry point + startup checks
server/index.js HTTP server: API routes, uploads, published pages, static files
server/db.js SQLite schema (built-in node:sqlite)
server/auth.js password hashing, sessions, reset tokens, rate limiting
server/mail.js minimal SMTP client
server/cli.js admin commands
server/brand.js app name and tagline
public/ the web app (plain ES modules, no build step)
js/render.js story → page; all block renderers
js/effects.js scroll-driven effects engine (one rAF loop, on-screen elements only)
js/ui.js icons, logo, toasts, menus, dialogs
js/maps.js MapLibre helpers: basemaps, views, data layers (incl. heatmap/clusters), legend, popups
js/analysis.js geospatial tools and layer statistics (Turf.js, loaded only in the editor)
js/workbench.js Add data, Analyze and Draw workspaces
js/catalog.js public data catalog (government sources + ArcGIS Online search)
js/export-core.js static-site / .war exporter (shared by the editor and `node server.js export`)
js/export.js the export dialog
js/zip.js minimal zip writer
js/builder.js the editor
js/templates.js starter templates
welcome.html landing page for signed-out visitors (itself a story)
stories/, data/ sample story and sample GeoJSON
docs/SCHEMA.md the story file format
test/ end-to-end API tests (node:test)
API
Every request that changes data must send the header X-Waymark: 1. This guards against cross-site request forgery: another site can't add a custom header without a CORS preflight, and the server never allows one.
POST /api/auth/signup {email, password, name} · POST /api/auth/login · POST /api/auth/logout · GET /api/me |
Accounts |
POST /api/auth/forgot {email} · GET /api/auth/reset/:token · POST /api/auth/reset {token, password} · POST /api/auth/password {current, password} |
Password reset and change |
GET /api/stories · POST /api/stories {story} |
List and create your stories |
GET /api/stories/:id · PUT /api/stories/:id {story, version} · DELETE /api/stories/:id |
Read, save and delete a draft. A save with an outdated version returns 409 |
POST /api/stories/:id/publish {slug?} · POST /api/stories/:id/unpublish |
Publish copies the draft to the public copy |
GET /api/public/:slug |
A published story's JSON |
POST /api/uploads (raw image body) · GET /api/uploads · DELETE /api/uploads/:id |
Images: JPEG, PNG, WebP or GIF, checked by their bytes. 12 MB limit |
Security
- Passwords and sessions: passwords are hashed with scrypt. Sessions are random tokens stored hashed, in an
HttpOnly; SameSite=Laxcookie. Sign-in, sign-up and reset requests are rate-limited. - Password reset: reset links are single-use and expire after an hour, and using one signs out every existing session. A reset request gets the same answer whether or not the email has an account. Links are always built from
APP_URL, never from the request's Host header. - Story content: story text is cleaned against an allow-list of HTML tags. Every page sends a Content-Security-Policy that only runs scripts from this site and unpkg, with no inline scripts.
- Uploads: uploaded files are checked by their contents, which rules out SVG, and served with
nosniffand adefault-src 'none'policy.
Story file format
Stories are plain JSON. See docs/SCHEMA.md.
🤝 Contributing
Bug reports, ideas and pull requests are welcome. Start with CONTRIBUTING.md. Please report security problems privately, as described in SECURITY.md. Each release is listed in the changelog.
Background
This project started as a rebuild of the ArcGIS StoryMaps reading and authoring experience. It was written from scratch using only public behavior and open formats. Esri's open-source classic templates (Map Journal, Map Series, Apache-2.0) and mapbox/storytelling (BSD-3) were studied for data models and UX patterns only. No code was copied, and the current closed-source ArcGIS StoryMaps product was not used.
License
Waymark is released under the MIT License.
It loads these open-source libraries from a CDN (an exported story can bundle MapLibre, along with its license): MapLibre GL JS (BSD-3-Clause) and, in the editor only, Turf.js (MIT).
Map tiles and data come from their providers, which are credited on every map: OpenStreetMap contributors and OpenFreeMap (ODbL), Sentinel-2 cloudless by EOX (CC-BY 4.0), and USGS, NOAA, NASA, US Census Bureau, NPS, USDA Forest Service, EPA, NIFC and Natural Earth (public domain). If you host Waymark for others, keep those credits visible.
来源:Hacker News · github.com











