Extended Markdown Editor
The Journey
Version 1: PostgreSQL + Docker + Django + Vue + Auth0 + GraphQL
View SourceI actually built this project 3 times! My first version was overcomplicated both in the features and the tech stack. My goal was to build a Notion-like (also similar to Word and Google Docs) experience for users: A WYSIWYG (What You See Is What You Get) rich text editor that autosaves pages which can also be published to a public URL. I used Docker, Python, Django, and PostgreSQL for the backend, Auth0 for authorization, GraphQL for the API and full stack type safety, and Vue 3 for the frontend. This might be a great stack for a large team, helping them stay on the same page, but for a solo project that I started 6 months into my first software engineering job, it was too much. I spent more time wrestling with the tech stack than building features.
I also realized that it would be a nightmare if my app ever needed a major refactor. As a fresh grad from MIT, I was so eager to add complexity wherever I could to flex my skills. But as a solo developer, I needed to be pragmatic. I decided to refactor the entire project with a simpler tech stack and a more focused feature set.
Version 2: Firebase + Vue
View SourceSo I built a second version with the very simplest stack I knew: Firebase for the API, database, blob storage, and authentication, and Vue 3 for the frontend. This enabled very rapid development, but there were was one issue: Firebase's free tier began excluding blob storage and this was just a personal project I was using so I could write articles and learn new skills, not something I was willing to pay for.
 Additionally, building a WYSIWYG editor is unexpectedly difficult. None of them are very accessible, nor do they work on all browsers. See this thread to see what other devs say about building WYSIWYG rich text editors from scratch. Most of these editors that exist use contenteditable, which is a complex and often frustrating API to work with, allowing you to directly edit HTML. I had to manually decide where the cursor should go and which element it should be within (your cursor is right between bold and normal text right now, when you type are the new characters bold or normal). I also had to handle pasting from Word, Google Docs, and other sources. Every time you type in a contenteditable div, the cursor would jump to the beginning since a state update was necessary, and the cursor position would need to be manually set back. Here is an example of the buggy behavior I had to fix: 
Version 3: Markdown + Nuxt
View SourceIn the third and final version, I decided to scrap the WYSIWYG editor and switch to a markdown editor. This was a much easier feature to build, since plain markdown is just text which can be typed in a text area which has so many features built in.
I also switched to Nuxt 3 which is a full stack framework (similar to Next.js for React) so that I could use server-side rendering to optimize SEO and performance for the public rendered page.
I also got back the end to end type safety I had with GraphQL by using Drizzle ORM to define my database schema in TypeScript and Nuxt automatically gives me type safety between the backend endpoints and Vue components.
 Auth should also be simple to remove as many opportunities for me to shoot myself in the foot as possible. I used nuxt-auth-utils with Google OAuth to easily handle authentication on both the server and client. 
 I used an extended version of markdown that includes several extra components. I used the syntax used by Nuxt UI Content Typography. If you use Nuxt UI Content, you can create pages by adding .md files in your project, then you deploy your site somewhere using CI/CD. I wanted to build a similar experience, but with a live-updating editor that autosaves and publishes to a public URL, all in the browser, and without having to deal with deployments. 
Features
Syntax Highlighting
 The editor uses a simple textarea for input, with syntax highlighting provided by shiki. It works on all browsers and is accessible. Since you cannot change the color of individual characters in a textarea, I make the textarea transparent and place a syntax highlighted pre behind it. This allows you to see the syntax highlighting while you type. The alternative would be to use a contenteditable div, but that is a mess as we talked about earlier. This is how it works on the shiki docs. Here is an example of an editable TypeScript code block using this technique: 
Using Go compiled into WebAssembly to improve performance
At first, the syntax highlighting for markdown in the editor was done as simply as possible: Whenever the content changed, the entire content was re-parsed and re-highlighted. This worked fine for small documents, but as the document grew larger, the performance became unacceptable. I needed a way to improve the performance of the syntax highlighting without sacrificing accuracy.
 I decided to re-highlight only the lines that were changed so that users could get similar performance regardless of the size of their page. The JavaScript Event fired by textarea input does not provide any information about what changed, so I had to manually compare the old and new content to determine which lines changed. 
There are complex algorithms for doing this, like what GitHub uses to show diffs, but my use case is much simpler: I can assume that changed lines are contiguous since I am getting the diff on every change (key press, delete, delete several lines, paste several lines), unlike GitHub where many changes may be made before a diff is requested. I also only need to know which lines changed, not the exact character-level changes within those lines. This allows me to use a much simpler algorithm that runs in linear time and does not rely on computing the similarity of lines, making it much more predictable.
My algorithm finds the first changed line, starting the search at the beginning of the document and working its way down until it finds a line that has changed. It similarly finds the last changed line by going from the bottom. From there it can figure out whether a line has been added, removed, or changed. Then it can only re-highlight those lines for much better performance.
To improve performance even further, I implemented this algorithm in Go and compiled it to WebAssembly. This allows the algorithm to run at near-native speed in the browser, making it much faster than if it were implemented in JavaScript. This was my first time using Go and WebAssembly, and it was a fun experience. I learned a lot about both technologies, and I was able to improve the performance of the editor significantly.
 However, it is a myth that WebAssembly is always faster than JavaScript. So I ran an experiment to compare the performance of my Go WebAssembly implementation to a TypeScript implementation of the same algorithm. I tested in the browser where the code would actually be running and added the entire Bee Movie script to the editor and started editing. While both implementations were performant, the Go WebAssembly implementation averaged 1.27 ms per edit, while the TypeScript implementation averaged 0.22 ms per edit. So in this case, the TypeScript implementation was actually more than 5 times faster than the Go WebAssembly implementation! I did this in Chrome using the performance.now() method to most accurately measure the time taken by each implementation. I got similar results in Safari. 
const startTime = performance.now();
const result = diffLines(previous, newText);
const parsed = JSON.parse(result) as DiffResult;
const endTime = performance.now();
diffCount++;
totalDiffTime += endTime - startTime;
console.log(
  `Current averageDiffTime is ${(totalDiffTime / diffCount).toFixed(2)}ms`,
);
const startTimeTS = performance.now();
const resultTS = tsDiffLines(previous, newText);
const endTimeTS = performance.now();
tsDiffCount++;
tsTotalDiffTime += endTimeTS - startTimeTS;
console.log(
  `Current tsAverageDiffTime is ${(tsTotalDiffTime / tsDiffCount).toFixed(
    2,
  )}ms`,
);Ultimately, the Go WebAssembly code should have been faster, but this is why I always validate my assumptions with data.
Shortcuts
 I also brought some features over from the WYSIWYG editor, like  B automatically adds **bold** text where your cursor is or around highlighted text, and  I automatically adds *italic* text. This involves manually manipulating the text in the textarea and setting the cursor position, which is a bit tricky but not too bad. But this functionality ends up breaking the browser's built-in undo/redo stack (because the textarea value change is not tracked), so I had to manually implement undo/redo functionality. See the editor in the section above to try out the shortcuts and how they break undo/redo. 
Beyond this, I also added a command palette that opens when you press / while focused in the editor. This allows you to search for any markdown syntax or component syntax without needing to memorize it. You can also search for any icon () or special key binding (⇧). You can also search "Upload Image" to download an image and have the markdown automatically generated to point to your freshly uploaded image!
When you copy rich text, and paste it into the editor, it will translate to markdown. For example, if you copy some text from Google Docs or a webpage, and paste it into the editor, it will automatically convert it to markdown. This works for headings, links, bold, italic, lists, and more.
Autosave
The editor automatically saves your changes to a SQLite database every few seconds, so you never have to worry about losing your work. If you close the tab or browser, your changes will be there when you come back. You can also manually save your changes by pressing S.
 I implemented the debounce logic poorly the first time, storing the last update time in a ref and using several setTimeout calls which decide whether to execute based on the time of the last update. This was overly complex and buggy. 
 I simplified this greatly by storing the timeout in a vue ref and clearing it and setting a new timeout every time the content changes. When the timeout completes, the content is saved. Much simpler and more reliable. 
Markdown Parser
I built a custom markdown parser since I wanted to support extra components like Tabs, Accordions, and Callouts. Because one of my goals was to prevent XXS attacks, I couldn't use a library that simply converted markdown to HTML since that would allow arbitrary HTML to be injected. Even with sanitization libraries, I couldn't be sure that no vulnerabilities existed, and these libraries often have to be updated to patch new vulnerabilities. So I built a custom parser that transforms the markdown in to tokens.
I also used shiki to provide syntax highlighting for code blocks and inline code, which also returns in the form of tokens. The main problem I faced here was that shiki uses web assembly, which is not supported in Cloudflare Workers for security reasons (the platform I used to host the backend). Since I was trying to optimize for simplicity, cost, and the viewing of articles, I decided to compute the syntax highlighting on the client while editing the articles. This was a tradeoff, but I would rather things be a bit slower while editing, than have rendered and public articles take longer to load. This does improve performance for live previews of rendered markdown while editing.
Server-Side Rendering (SSR)
Once the browser requests a public article, the server fetches the article from the database and renders it as HTML. This HTML is then sent back to the browser, where it is displayed to the user. This process is known as server-side rendering (SSR), and it has several benefits, including improved performance and SEO.
Keeping with maximizing simplicity, I used Nuxt 4 to handle SSR for me automatically.
Static Site Generation (SSG)
SSG is similar to SSR, but instead of rendering the HTML on the server for each request, the HTML is generated at build time and served as static files. This can improve performance even further, since the server does not have to render the HTML for each request. However, it does require a build step, which can be time-consuming for large sites, which is why I only use SSG for the homepage currently.