Full-featured, open-source Markdown editor descended from PageDown, the Markdown library originally written for Stack Overflow and the other Stack Exchange sites. Modernized for Vercel deployment, hardened, and actively maintained.
Note
Independent successor to benweet/stackedit by Benoit Schweblin.
The original project's last commit was 2023-05-27 and it is dormant. This codebase started as a fork, preserves Apache-2.0 attribution to Benoit (LICENSE, NOTICE, About modal), and is now developed independently — Vite + Vercel build, security hardening, modern test suite, and a roadmap that is no longer constrained by upstream-sync ergonomics.
Original project's online deployment: stackedit.io (still hosted by upstream; this codebase is not deployed there).
Warning
StackEdit stores OAuth access tokens for connected sync/publish providers (Google Drive, GitHub, Dropbox, GitLab, WordPress, Zendesk, CouchDB) in the browser's IndexedDB + localStorage. Anything with JavaScript access to this origin can read them. Review the source before connecting provider accounts, and consider using dedicated OAuth apps with minimal scopes. The authors are not responsible for provider-account compromise resulting from XSS or malicious browser extensions.
- WYSIWYG split-pane Markdown editor with live preview
- KaTeX math, Mermaid diagrams, ABC music notation, Prism syntax highlighting
- Sync + publish to Google Drive, GitHub, Dropbox, GitLab, WordPress, Blogger, Zendesk, CouchDB
- PWA with offline-first editing (Workbox service worker)
- Custom Handlebars export templates (HTML, PDF, EPUB, DOCX, RST, LaTeX, …)
- Multiple workspaces, file + folder management
- Collaborative comments on discussions
A modernization + hardening pass on top of upstream's last shipped version (5.15.4, May 2023). The full, version-by-version trail lives in CHANGELOG.md — the summary below is what's shipped cumulatively so far.
| Area | Upstream | Fork |
|---|---|---|
| Framework | Vue 2.7 | Vue 3.5 |
| State management | Vuex 3 | Pinia 3 |
| Editor core | forked PageDown | CodeMirror 6 |
| Bundler | Webpack 2 | Vite 8.0 (Rolldown + Oxc) |
| Dev server | webpack-dev-server | Vite with HMR |
| Test runner | Jest (broken on modern Node) | Vitest 4 + happy-dom 20 (+ jsdom for sanitizer specs) |
| Linter | ESLint 4 | ESLint 10 flat config + eslint-plugin-vue 10 + @typescript-eslint |
| Node target | 10.x | 22.x |
| Sass | node-sass (deprecated) + @import |
Dart Sass with @use / math.div / color.adjust |
| CSS baseline | normalize-scss |
modern-normalize |
| PWA | workbox via webpack plugin | vite-plugin-pwa 1.x |
| Source language | JavaScript | TypeScript — all .vue components, api/, and src/services/ (templates type-checked by vue-tsc) |
Bundle: main chunk went from 2.8 MB → ~545 KB (gzipped 828 KB → ~152 KB), with @vue/* + Pinia split into a dedicated vue chunk. Mermaid, KaTeX, and the CodeMirror 6 editor are lazy-loaded; the template worker is a separate chunk.
- Express + PM2 (single Node VM) → Vercel: 4 Edge-runtime API routes (
/api/conf,/api/userInfo,/api/cspReport,/api/googleDriveAction), 1 Node route (/api/githubToken), 2 stubs (/api/pdfExport,/api/pandocExport). /api/healthendpoint for uptime monitors.- Upstash Redis distributed rate limiting with a per-instance in-memory fallback for local dev.
@vercel/analytics+@vercel/speed-insights(anonymized, DNT-honoring — see PRIVACY.md).- Custom
/404.html,/robots.txt,/favicon.ico,/privacy.html. vite:preloadErrorhandler — skew protection for long-lived browser tabs.- GitHub Actions CI (build + unit on every PR) and Dependabot (weekly npm + github-actions).
- DOMPurify 3.4 replaces the forked Angular
$sanitize; allv-htmlpaths route through it (includingCurrentDiscussion, which was unsanitized upstream). - Trusted Types enforced (
require-trusted-types-for 'script'+ default policy).'unsafe-eval'scoped to the template-worker chunk only, via per-file CSP headers invercel.json. - CSP: enumerated
connect-src,frame-srclimited to'self' https://accounts.google.com, HSTS preload, COOP / CORP, expanded Permissions-Policy. - OAuth: GitHub flow upgraded to PKCE S256. Dropbox / Google Drive scopes kept minimal.
- API routes:
sameOrigincheck, body-size caps, IP-keyed rate limits, masked error bodies. target="_blank"anchors auto-getrel="noopener noreferrer"(36 anchors across 16 files).- Source maps disabled in production builds.
- Removed third-party script loaders (Google
apis.google.com/js/api.jsoffline check, landing-pagestackedit.io/style.css). - Removed the deprecated AppCache manifest from the landing page.
- Removed ~50 webpack-era devDeps (loaders, gulp, node-sass, stylelint, etc.) — Vite handles those natively.
- Removed unused runtime deps:
aws-sdk,request,body-parser,compression,serve-static,tmp,google-id-token-verifier,indexeddbshim,babel-runtime. - Replaced:
mousetrap@1.6.5(unmaintained, last release Jan 2020) → tinykeys 4;file-saver1 → 2;bezier-easing2 → 3; legacymarkdown-it-imsizefork → in-repo shim; custom clipboard plugins → native async Clipboard API. - Bumped: Mermaid 11, KaTeX 0.17, Prism 1.30, markdown-it 14, happy-dom 20, DOMPurify 3.4,
vite-plugin-pwa1.3, Vite 8, Vitest 4.1.
- Import from clipboard (
Import/Exportmenu) — reads bothtext/htmlandtext/plainflavors, prefers plain text when it already looks like Markdown (avoids Turndown's defensive escaping of#/*/-/`), otherwise runs HTML through sanitize + Turndown. Auto-derives the filename from the first heading. - Clipboard image paste directly into the editor.
- Google Drive image picker replacing the dead Google Photos picker.
- Open Graph + Twitter-card meta tags for link previews.
- About modal rebranded for independence:
Independent Successorpill, one-line tagline, dual copyright line (Dock5 + soreavis),View on GitHub/Changeloglink set, dead upstream-only links pulled. - Privacy Policy: canonical
PRIVACY.md+ static/privacy.htmlmirror (hand-synced, no build step).
- Scroll-sync regression (bezier-easing v3 dropped the v2
.get()/.toCSS()methods).animationSvcre-attaches them as shims on the returned function so the animation loop doesn't throw on every frame. - Shortcuts init TDZ ReferenceError at module load — hoisted the
expansionsconst above itsimmediate: truewatcher inshortcuts.ts.
- 480 specs under
test/unit/. Hardening specs (test/unit/hardening/) cover: sanitizer XSS vectors, rate limiter, API handlers, GitHub OAuth PKCE, template worker sandbox,vercel.jsoncontract, markdown-it plugins (including the rule-disable regression foroptions.{fence,table,del}), drag-and-drop markdown import, mermaid lightbox pan/zoom/copy. Component specs (test/unit/components/) cover:defaultLocalSettingsinvariants, the icon registry, thesimpleModalsregistry shape, thecustomToolbarButtons+pagedownButtonsshape, the modal Pinia store's open/reject/stack/hideUntil flows, and a Modal focus-trap regression guard. - Paste-ready manual fixtures under
test/fixtures/for browser smoke-testing (KaTeX, Mermaid, YAML front-matter, API curl recipes, sanitizer vectors).
- Semver prerelease suffix:
5.15.5-fork.1,5.15.5-fork.2, … inherited from the fork era. Now that upstream is dormant the suffix can be retired at the next significant release; until then it stays for continuity with shipped artifacts. Tracked inCHANGELOG.md. - Three-branch model — see Branching model below.
| Branch | Purpose | Protection |
|---|---|---|
main |
Stable. Deployed to production. | PR required, 0 approvals, convo resolution required, no force-push, no deletion |
develop |
Integration. Features land here first. | PR required, 0 approvals, no force-push, no deletion |
next |
Experimental / preview. Useful for sharing work-in-progress. | No PR required, force-push allowed, no deletion |
Flow for a typical change:
- Branch off
develop:git checkout -b fix/<slug> develop(orfeat/<slug>,chore/<slug>,docs/<slug>,build/<slug>). - Open a PR into
develop. - When
developis ready to cut, open a PR fromdevelop→main. After merge, fast-forwardnextto matchmain.
Commit prefixes follow Conventional Commits: feat, fix, docs, chore, build, refactor, test, style.
Upstream benweet/stackedit is dormant (last commit 2023-05-27), so we don't run a routine sync. If upstream ever ships a fix worth pulling, the one-shot recipe is:
git remote add upstream git@github.com:benweet/stackedit.git # one-time
git fetch upstream
git checkout -b chore/upstream-pick-<sha> develop
git cherry-pick <upstream-sha>
git push -u origin chore/upstream-pick-<sha>
gh pr create --base developThis codebase is no longer source-compatible with upstream's file layout in general — cherry-picks may need manual conflict resolution.
- Node.js 22.x
- npm ≥9
git clone git@github.com:soreavis/stackedit.git
cd stackedit
npm install --legacy-peer-deps # required: a few transitive peers still lag the toolchain
npm run dev # http://localhost:8080| Script | What it does |
|---|---|
npm run dev |
Vite dev server with HMR (port 8080) |
npm run build |
Production build into dist/ |
npm run preview |
Serve the production build |
npm run unit |
Run the Vitest suite |
npm run unit-with-coverage |
Coverage → coverage/ |
ANALYZE=1 npm run build |
Emit dist/bundle-analysis.html |
-
Import the repo in Vercel (or
vercel link). -
Set these Environment Variables (Production + Preview) before the first deploy:
Variable Purpose When GOOGLE_CLIENT_IDGoogle Drive OAuth Build-time (baked into bundle) GITHUB_CLIENT_IDGitHub OAuth Build-time GITHUB_CLIENT_SECRET/api/githubTokentoken exchangeRuntime DROPBOX_APP_KEYDropbox OAuth (app folder scope) Runtime (from /api/conf)DROPBOX_APP_KEY_FULLDropbox OAuth (full dropbox scope) Runtime (from /api/conf)WORDPRESS_CLIENT_IDWordPress OAuth Runtime GOOGLE_API_KEYGoogle APIs Runtime (restrict by HTTP referrer) UPSTASH_REDIS_REST_URL+UPSTASH_REDIS_REST_TOKENOptional — enables distributed rate limiting Runtime -
The
vercel.jsonpinsnpm install --legacy-peer-depsas the install command — Vercel's defaultnpm installfails on the peer-dependency graph without it. -
Vercel auto-deploys on push to
main.
npm run unit480 tests under test/unit/ (hardening + component specs). Paste-ready manual fixtures under test/fixtures/ for browser smoke-testing (sanitizer XSS vectors, KaTeX, Mermaid, YAML front-matter).
api/ # Vercel serverless/Edge functions
dev-server/ # Vite dev middleware for PDF/Pandoc export
src/
components/ # Vue 3 SFCs in TypeScript (app shell, modals, menus)
extensions/ # markdown-it plugins (KaTeX, Mermaid, emoji, …)
icons/ # SVG icon components
libs/ # htmlSanitizer (DOMPurify) + in-tree markdown-it shims
services/ # editor (CodeMirror 6), sync, network, templateWorker — all TypeScript
stores/ # Pinia stores
styles/ # SCSS
static/ # Landing page, favicon, robots, 404, privacy.html, fonts
test/
fixtures/ # Paste-ready markdown smoke tests
unit/hardening/ # Vitest specs for security + API surface
unit/components/ # Vitest specs for component / store / data registries
vercel.json # Install + headers + rewrites + Edge/Node function config
CHANGELOG.md # Keep-a-Changelog, `x.y.z-fork.N` versioning
PRIVACY.md # Canonical privacy policy (mirrored to static/privacy.html)
These are all maintained by the upstream project, not this fork, and still work with or against the upstream stackedit.io deployment. Linked here so fork users know they exist:
- Chrome app
stackedit.js— embed StackEdit in any website- Chrome extension (uses
stackedit.js) - Community forum
- Original author: Benoit Schweblin — the entire StackEdit editor + ecosystem. Fork maintained in parallel with, not in replacement of, the upstream repo.
- Fork maintained by: Julian Soreavis
- Community: community.stackedit.io (upstream's)
Apache License 2.0 — see LICENSE and NOTICE.
This fork preserves the upstream license, copyright, and NOTICE file verbatim. All original StackEdit code remains © 2014–2023 Benoit Schweblin. Modifications by fork maintainers are also licensed under Apache-2.0.