Library / SDK
surmon-china/surmon.me avatar
surmon-china/surmon.me

surmon.me: a Vue SSR blog front end whose API lives in another repository

🌱 My digital vihāra, built by me.

2,124 stars577 forksVueMIT

At a glance

What is it?
surmon.me is the public rendering layer of a personal blog: Vue 3 with server-side rendering, a Node process for both dev and production, and every piece of content held by a separate NodePress API. Valuable as a reference, incomplete as a starting point.
Who is it for?
Adopt surmon.me when you want a finished Vue 3 server-rendered site and accept that content lives elsewhere; it is a public rendering layer with a complete sibling set, not a blog in a box. Leave it when you want one repository holding Markdown and a deploy that ends at a file host.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly Vue, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A blog front end whose backend lives in another repository

surmon.me is one repository inside a set. This one carries the public blog: the pages, the routing, the styling and the Node process that renders and serves them. Almost nothing else lives here. Posts, users, comments and media belong to NodePress, a RESTful API service built with NestJS, so a fresh clone gives you a site with no content until that service answers. Three sibling projects draw the rest of the boundary. surmon.me.admin is the writing interface, built with React and Veact. surmon.me.ai is a blog AI agent built on Cloudflare. surmon.me.native is the native client, built with React Native. Repository topics confirm the front-end side of that split: vue, pinia, server-side-rendering, ssr, vite-ssr and vue-disqus all sit there. The consequence for an engineer is blunt. You are adopting one layer of a four-part system, and the other three layers have to be picked, hosted and kept running on their own schedule.

The dev server is a Node process, not a browser preview

The development command does not start a browser-only preview. pnpm run dev runs NODE_ENV=development tsx watch ./src/bff.ts, which means the entry point during development is a TypeScript Node file restarted on change, not an index.html served by Vite alone. The manifest says the same thing in three places:

json
  "main": "dist/bff.js",
  "scripts": {
    "dev": "NODE_ENV=development tsx watch ./src/bff.ts",
    "build": "NODE_ENV=production node ./bundler/index.js",
    "start": "NODE_ENV=production node ./dist/bff.js",
    "lint": "eslint src",

Read that block as a deployment contract. The buildable artifact is a Node program, not a folder of static files, and the bundler directory at the repository root is where that program gets assembled. The top-level entries agree: public/ is there, and nothing host-specific is. Topics such as server-side-rendering and vite-ssr mark the rendering side of the work.

Four pnpm commands take a fresh clone to a running blog

Install with pnpm and your toolchain is settled for you, because pnpm-lock.yaml is the only lock file among the top-level entries. From there the documented path is short.

bash
# install dependencies
$ pnpm install

$ pnpm run dev
$ pnpm run build
$ pnpm run start

# lint test
$ pnpm run lint

Sit on pnpm run dev while editing. When the site is ready, pnpm run build runs the bundler in production mode and pnpm run start serves the compiled dist/bff.js, the same entry point named in package.json. pnpm run lint runs eslint across src, and the repository ships eslint.config.js and prettier.config.js for that check. One line of the block hands control to a shell script kept under scripts/, which makes publishing from a workstation optional rather than part of an ordinary build.

pangu, lozad and marked explain the reading experience

Read the dependency list as a feature list nobody wrote down. Four marked-family packages, marked, marked-highlight, marked-mangle and marked-xhtml, cover article rendering, with highlight.js at 11.11 for code colouring. pangu is in there for something a Latin-only template rarely needs: spacing between CJK characters and Latin words, which is what keeps a mixed-language post readable without hand-inserted gaps. lozad at 1.16 handles images that arrive in view, and html-to-image with qrcode add browser-side image rendering on top of that.

json
    "@braintree/sanitize-url": "^7.1.x",
    "@dotenvx/dotenvx": "^1.57.x",
    "@redis/client": "^5.x",
    "@sentry/vue": "^10.x",
    "geojson": "^0.5.0",
    "mapbox-gl": "^3.20.x",
    "marked": "^17.x",
    "pangu": "^7.x",
    "pinia": "^3.x",
    "rss": "^1.2.2",
    "sitemap": "^9.x",
    "swiper": "^12.x",

Past reading, the same list covers syndication through the rss package, fast-xml-parser and sitemap, mapping with mapbox-gl and geojson, carousels with swiper at 12, and operations with @redis/client at 5, @sentry/vue at 10, axios beside agentkeepalive and ua-parser-js, plus a lru-cache at 11 for whatever runs hot. Vue is pinned at 3.5, vue-router at 5.0 and pinia at 3.

Three environment files sit at the repository root

Look at the top-level entries again: .env, .env.development and .env.production sit next to .gitignore and .editorconfig, all committed. That pairing deserves a second look, because configuration here is read at runtime rather than baked in at build time. @dotenvx/dotenvx is listed under dependencies instead of devDependencies, so the compiled dist/bff.js is written to read its settings from whatever machine it lands on. The three mode files line up with the NODE_ENV values the scripts set inline on the command line. What the repository does not hand you is the content of those files. The setup section names no environment key, and it says nothing about which NodePress endpoint the front end is meant to call. Treat the first run as wiring work: find the connection in the source and on the NodePress side, because the documented four commands stop before that question is answered.

The format script calls npm while every other command calls pnpm

There is a contradiction in the manifest worth naming, because it is what a contributor hits first. Every command in the setup section is pnpm, the tree tracks pnpm-lock.yaml and nothing else, yet the format script reads like it belongs to another ecosystem:

json
    "format": "npm run lint --fix",

npm can execute a script that pnpm defined, so the command works. What it advertises is that two package managers are recognised in one manifest, and if somebody types npm install here, no lock file constrains the result. The final script has a second wrinkle. It does not invoke a binary, it sources a shell file with a leading dot, so the script runs inside your current shell rather than a child process and anything it exports stays exported afterwards. That detail matters the moment you push tags from a laptop instead of a CI runner. Neither issue blocks a build; both show up only when somebody runs the whole script list rather than the four documented commands.

One tag hands the finished build to a server script

Deployment is a three-step chain, and the triggers are declared in the repository rather than left to a hosting panel.

yaml
- Any PR opened → [`CI:Build test`](.github/workflows/test.yml)
- New tag `v*` → [`CI:Create Release`](.github/workflows/release.yml)
- New Release created → [`CI:Deploy`](.github/workflows/deploy.yml) → Execute server [deploy script](./scripts)

The first workflow catches a broken build in a pull request. The second fires on a tag matching v*. The third fires once a version is published on GitHub, and what it runs is a script in scripts/ on the server rather than a copy to a static host. Two details matter when you copy the pattern. That last trigger watches the published version and not the tag, so a tag pushed without the matching publication leaves the server untouched. And the destination lives entirely inside that server-side script, which means a fork gets the trigger and the workflow file but not the host, the credentials or the script body.

Without NodePress, a clone renders an empty blog

Here is the honest boundary. Out of the box you have a rendering layer and no content, no users, no media library, no admin and no way to publish a post, because all of that is NodePress plus surmon.me.admin. The top-level entries hold no compose file, no migration directory and no seed script, and the setup section runs straight from install to dev server. Comments are somebody else's service too, with the vue-disqus topic marking the integration, so availability and moderation sit outside this codebase. The alternative shape is worth stating plainly, because the choice is structural rather than cosmetic. Most self-hosted blogs keep article sources inside the same repository and compile them into static HTML, which leaves you with a folder of files, a CDN and no long-running process. This project takes the opposite trade and pays a per-request cost for it: dynamic server rendering, content behind an HTTP API, live data and client-side navigation. In exchange you get a rendering layer that a static build would have needed a second service for anyway. Pick it when you want a complete, readable Vue 3 server-rendered site and are ready to run NodePress beside it.

Editorial conclusion

Adopt surmon.me when you want a finished Vue 3 server-rendered site and accept that content lives elsewhere; it is a public rendering layer with a complete sibling set, not a blog in a box. Leave it when you want one repository holding Markdown and a deploy that ends at a file host. Verify three things first: that you can run NodePress yourself, which environment keys the three .env files expect, and what the script under scripts/ actually pushes, because none of that is answered inside this repository.

Frequently asked questions

How do I run surmon.me on my own machine?

Install dependencies with pnpm, then use pnpm run dev for the watched development process or pnpm run build followed by pnpm run start for a production build. The lint check is pnpm run lint, and the project tracks pnpm-lock.yaml as its only lock file.

Does surmon.me include the blog backend?

No. This repository holds the public site only; the RESTful API is NodePress, a separate NestJS service, and the writing interface is surmon.me.admin built with React and Veact. A clone renders an empty site until the API is reachable.

What does surmon.me use to render article content?

Markdown handling runs through marked plus marked-highlight, marked-mangle and marked-xhtml, with highlight.js for code colouring and pangu for spacing between CJK and Latin text. Image loading in view is handled by lozad.

How does deployment work for surmon.me?

Three workflows are declared: a build test on every pull request, a second one on a tag matching v*, and a third that fires when a version is published on GitHub and runs a deploy script under scripts/ on the server.

What licence is surmon.me released under?

MIT. Both the licence field in package.json and the LICENSE file record it, which is a permissive licence with no copyleft obligation on derived work.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. surmon-china/surmon.me on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/surmon-china-surmon-me.svg)](https://hysenlabs.com/projects/surmon-china-surmon-me)

Was this useful?