DEV Community

Cover image for 1.5KB Single-File Wiki
Fedor
Fedor

Posted on • Edited on

1.5KB Single-File Wiki

Imagine having a personal wiki that fits in a single HTML file — no databases, no servers, just a self-contained knowledge base you can store in Dropbox, email to yourself, or even host on a static file server. Sounds familiar? Inspired by the legendary TiddlyWiki, I set out to create a minimalist wiki that’s lightweight and works even without JavaScript.

TiddlyWiki is a robust and feature-rich tool, but it comes with a cost: it’s a JavaScript-heavy application, often weighing several megabytes. What if we could peel away the complexity and distill it down to its purest form? The result is a lean, fast, and no-frills — a wiki that:

  • Works without JavaScript: Thanks to pure CSS routing, you can view pages even if JS is disabled.
  • Edits with ease: Markdown lets you write and format content effortlessly.
  • Saves instantly: Changes are saved by downloading a new HTML file, making it perfect for offline use.
  • Fits in 1.5 KB: Minified and gzipped, it’s smaller than most favicons.

In this article, I’ll walk you through the key ideas and hacks that made this project possible. Whether you’re a fan of TiddlyWiki, a minimalist at heart, or just curious about building lightweight web apps, there’s something here for you. Let’s dive in!


Pure CSS Routing: Navigation Without JavaScript

The secret sauce lies in the :target pseudo-class, which applies styles to an element whose ID matches the part of the URL after the # (hash). For example, if the URL is #my-page, the element with id="my-page" becomes the "target", and you can style it accordingly.

In our wiki, each page is represented by an <article> element with a unique ID. The CSS rule below ensures that only the target article (or the index page, if no hash is present) is displayed:

article:not(.index, :target, :has(:target)),
:root:has(:target) article.index:not(:has(:target)) {
  display: none;
}
Enter fullscreen mode Exit fullscreen mode

If the URL hash is #my-page, the <article id="my-page"> becomes visible. If the URL has no hash, the .index article is shown by default. If the hash is #my-photo and some article contains <img id="my-photo">, this article will be displayed.

Hash-based routing works purely with CSS, making it incredibly lightweight and reliable.


Markdown Editing and Prerendering

If JavaScript is enabled, the wiki can progressively enhance the experience with features like Markdown editing. The editor is toggled by double-clicking anywhere on the page. Pages are created on the fly when a new hash is encountered.

At first I expected to find a plethora of minimalist Markdown libraries that could handle basic formatting without external dependencies. To my surprise, a relativley popular option — Snarkdown — doesn't support paragraphs. That’s right — no <p> tags!

That led to creating a compact, self-contained Markdown-to-HTML converter. Here’s the core of it:

function md2html(str) {
  const enchtml = (str) => str.replaceAll("<", "&lt;");
  const inlines = [
    [
      /(\*{1,2}|~~|`)(.+?)\1/g,
      (_, c, txt) =>
        c == "*"
          ? `<i>${txt}</i>`
          : c == "**"
          ? `<b>${txt}</b>`
          : c == "~~"
          ? `<s>${txt}</s>`
          : `<code>${enchtml(txt)}</code>`,
    ],
    // ... (other inline patterns)
  ];
  const blocks = [
    [
      /\n(#+)([^\n]+)/g,
      (_, h, txt) => `\n<h${h.length}>${txt.trim()}</h${h.length}>`,
    ],
    [
      /\n(\n *\-[^\n]+)+/g,
      (txt) =>
        `\n<ul><li>${replaceInlines(txt)
          .split(/\n+ *\- */)
          .filter(Boolean)
          .join("</li><li>")}</li></ul>`,
    ],
    // ... (other block patterns)
  ];
  return blocks.reduce((md, rule) => md.replace(...rule), `\n${str}\n`);
}
Enter fullscreen mode Exit fullscreen mode

The md2html function follows a specific sequence to ensure Markdown is parsed correctly:

  1. Process Block-Level Elements First: The function starts by identifying and transforming block-level Markdown syntax (like code blocks, headings, lists, and paragraphs) into their corresponding HTML structures.
  2. Process Inline Elements Within Blocks: Inline formatting rules (like bold, italics, and links) are applied only where appropriate (e.g., inside paragraphs or list items).
  3. Escape HTML Only Where Necessary: HTML escaping is applied selectively, primarily within code blocks and inline code snippets. It's not a security feature here.

The rules are easy to modify or extend, allowing you to add support for additional formatting features. But remember that Markdown is not a replacement for HTML!

When you save a wiki page, the content is prerendered into HTML and dynamically appended to the DOM. It’s essentially like a static site generator, but it operates entirely within the browser resulting in a single, self-contained HTML file.


Saving Wiki: File Download

Since this is a single-file wiki, saving works by downloading the entire HTML file with the updated content. This is achieved using the Blob API:

function download() {
  const doc = document.documentElement.cloneNode(true);
  const html = "<!DOCTYPE html>\n" + doc.outerHTML;
  const blob = new Blob([html], { type: "text/html" });
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = "wiki" + Date.now() + ".html";
  link.click();
}
Enter fullscreen mode Exit fullscreen mode

This function creates a new HTML file containing the current state of the wiki and triggers a download. It’s a simple yet effective way to persist changes without a backend.


Conclusion: 1.5KB Single-File Wiki

By combining pure CSS routing, a custom Markdown parser, and a simple offline-first saving method, you can create a functional and lightweight wiki that works even without JavaScript. Whether you're building a personal knowledge base or a portable documentation tool, this approach strikes a nice balance between simplicity and practicality. It’s not perfect, but it gets the job done in a way that’s both elegant and efficient.

Feel free to take the code, tweak it, and make it your own. After all, the best tools are the ones you build yourself.

Top comments (15)

Collapse
 
justov_pinkton_841202b528 profile image
Justov Pinkton

This is great! I LOVE TW but this could be useful on my phone, where I want a VERY minimal interface etc. and don't need anything complex, just quick note taking.

I'm no JS guru - I'm trying to add


to the md2html and it's driving me crazy. It APPEARS I'm following the pattern of the other inline rules, but it just isn't working. Here's my update to the inlines:
const inlines = [
[
  /(\*{1,2}|~~|`)(.+?)\1/g,
  (_, c, txt) =>
    c == "*"
      ? `<i>${txt}</i>`
      : c == "**"
      ? `<b>${txt}</b>`
      : c == "__"
      ? `<hr/>`
      : c == "~~"
      ? `<s>${txt}</s>`
      : `<code>${enchtml(txt)}</code>`,
],
Enter fullscreen mode Exit fullscreen mode

Anyone see what I'm missing?

Great idea, and thanks in advance!

Collapse
 
fedia profile image
Fedor

Here's a casual way you could do that: chatgpt.com/share/67d495a0-3228-80...

Collapse
 
justov_pinkton_841202b528 profile image
Justov Pinkton

Ahh within the blocks section... that makes sense. eesh.
And thanks for expanding to ---, ***, ___.
I guess I could have hit my AI friend up on this hehe

I was just going with ___ testing to see if there was a conflict with ---

That update works!

Thanks!

Collapse
 
deuxlames profile image
deuxlames

That's incredible. I tried tiddlywiki may be 20 years ago :-) and a few days ago i was looking for a markdown wiki that doesn't need a server. Something simple and bingo in my mailbox today !

I'm going to try right now !

Collapse
 
artydev profile image
artydev

Great, thank you :)

Collapse
 
destynova profile image
Oisín

Very neat! I'm not sure how saving changes works if you don't have Javascript enabled, though.

Collapse
 
urbanisierung profile image
Adam

I love such simple no-dependency and simple ideas! Thanks!

Added it to the next issue of weeklyfoo.com.

Collapse
 
joebordes profile image
Joe Bordes

wonderful piece of software! recommendable

Collapse
 
detlef_meyer_99a6d7a7deed profile image
Detlef Meyer

Thanks for sharing. How am I supposed to understand this: "Feel free to take the code, tweak it, and make it your own. After all, the best tools are the ones you build yourself."
Is this an Unlicense (unlicense.org/)?

Collapse
 
katafrakt profile image
Paweł Świątkowski

It's licensed under MIT according to package.json contents.

Collapse
 
fedia profile image
Fedor

Right, it's MIT. Thank you!

Collapse
 
best_codes profile image
Best Codes

This is very cool! I don't think back / forward navigation works, though.

Collapse
 
fedia profile image
Fedor

Seems to be a Codesandbox issue with hash-based nav...

Collapse
 
best_codes profile image
Best Codes • Edited

Oh, so it does work, CodeSandbox is the issue! 👍

Collapse
 
mhvelplund profile image
Mads Hvelplund

This is pretty neat. It's a pity that it stores all the text twice, once in HTML and once in Markdown, but I'm guessing that is a performance thing?