Introducing Wrighter - A Powerful Markdown Blogger & A Writing Companion ⚡
...with an amazing WYSISWM markdown editor
Open Wrighter 🚀
The Problem
I have used almost all of the markdown editors out there, they were lacking the most important features that I needed the most.
I wanted a distraction-free markdown editor on the web that is usable in any form factor. Most markdown editors are either too cluttered or too minimal(hard to find things). There are only two extremes. I wanted an app that sits in the middle, it should be distraction-free while also having the ability to easily provide all of the application's functionality with a few key taps.
A markdown editor that isn't just a textbox, it should be reactive to the content that you are typing and context-sensitive. The markdown syntax can disturb your flow while you are reading in the editor.
- Ideas can come up anywhere at any time! You should be able to capture and organize the chaos. There should be very less friction between turning your ideas into articles that you are writing seamlessly. The process of
gathering/organizing ideas -> write -> share it to the world
should all be on the same platform, so there is less friction between the ideation phase and writing. These ideas and blogs would be organized in a way that is easily accessible and searchable, which could essentially act as a second brain.
There is still a lot of friction around ideation & writing blogs on the web & publishing them. Wrighter has the greater aim to solve them!
About Wrighter
Wrighter is THE full package! It consists of a powerful WYSIWYM (what you see is what you mean) markdown editor supporting GFM and KaTeX. It has proven to be the best way to edit markdown!
Wrighter allows you to write the best way and publish the fastest way with configurable SEO settings. Wrighter also has the ability to gather and organize ideas in any form factor quickly(they are called bites), which you can later use in your blogs while writing them.
Wrighter is built upon a tagging system that can be used in both your blogs and also your ideas. Never forget where you saved an idea or a blog, just open up the tag tree and search for keywords!
It also includes a super smart context-based Command Bar, Invoke it with ⌘/Ctrl + Shift + P. The command bar basically provides you with suggestions based on the current page/context. You can go mouse free with the command bar, with all your wrighter functionalities being just a few keystrokes away!
The best and my favorite feature of all? It is optional signup! You can get all of wrighter's features(except publishing) without even giving away your personal details. All your data will be there as long as you use the same browser. In fact, you can just open up this link and start wrighting right now!
Another amazing feature is that you can copy from anywhere on the internet. when you paste it in wrighter, it automagically converts it to markdown, which happened to be a huuuuuuge time saver.
...and even more features that I will explain in detail in each section ⤵️
Making of WYSIWYM Markdown Editor
The wrighter editor is built on top of codemirror and bytemd. codemirror is the go-to choice when it comes to flexible/hackable text editing and bytemd provides a nice wrapper for codemirror using react with some extra functionalities. I wanted to create a fork of bytemd that includes all the WYSIWYM features that I built for wrighter, but it was out of scope and takes too much time.
Wrighter uses some clever techniques to make sure that markdown semantics and syntax are muted while the content takes the most focus. It also pushes the heading syntax far left outside of the editor so that the headings look like actual headings, it also indents the lists to the right, giving some degree of separation between different content types while typing. It comes with a focus mode so that you can focus on the content alone, hiding all of the editor buttons and gizmos.
It supports almost all of the common text editor shortcuts that you use every day. If you forget any of it, just launch the command bar(⌘/Ctrl + Shift + P) and search for whatever you want. The editor autosaves the entire context to the browser's IndexedDB regularly so you don't have to worry about data loss. If you are logged in, it autosaves with the Planetscale DB
under the hood, the markdown is parsed by the unified remark and rehype processors, which in turn under the hood manipulate the markdown + HTML as an AST, which gives a lot of flexibility on parsing and rendering markdown. The editor uses them as plugins, which allows me to pick the features and inject them into the editor, one such injectable feature is the custom-made "copy from anywhere & paste as markdown" feature.
The editor and the markdown renderer are used in multiple parts of wrighter with selective features removed/added all with the help of the flexible plugin system.
The Wrights aka Your Blogs
I wanted to create consistent branding in wrighter, hence the name wrights/wrightups. wrights are just labeled synonyms for the writeups/blogs that you write. You can create a wright by just clicking the Create Wright button or by using the command bar or by visiting https://wrighter.vercel.app/new. I wanted the initial onboarding to be as seamless as possible.
No signups, no button clicks, no BS. Just visit the URL and you are ready to jot down your thoughts. This is definitely inspired by Google's way of onboarding/creating new docs using a quick link.
If you are a logged-in user, you would have the option to keep your wright private or public visibility. You can also modify SEO settings like slugs and meta image that would show up when you share your link on social media. Other SEO metadata is managed by wrighter to provide your wright the best search engine visibility.
The published wrights make use of vercel's server-side rendering capabilities. This means that after the initial load, your wright is SEO compatible and loads almost instantly!
The Bites aka Your Ideas
I'm not a "great" writer, but I have a lot of ideas. Some good ones and many dumb ones. I might have missed at least a dozen of good ideas because I didn't note them down/I lost the chain of thoughts. That's exactly why I made Bites. They are used to jot down ideas of any form factor.
Is it a cool link that you found on the internet? or is it an awesome reference image that might be useful later? or a code snippet? or just a miscellaneous blob of text? bites has got you covered! You can add tags to the bites to organize and filter them accordingly. The bites page has a calendar and range date pickers which you can use to go up/down memory lane.
To reduce the friction in creating bites, the command bar provides a way to create a bite at any place at any time within wrighter. You can also create one by using the shortcut c + b
. This means that you can create a bite while writing your blog.
What's the use of Bites?
The major use case is that it helps you to "bite" down on small ideas that might pop up. The other awesome use case is that you can use the bites in the blogs/wrights that you are writing. Launch the command bar and select the bite to attach and the contents of the bite just appends to wherever the cursor is preset.
Attaching bites in wrights is one of my favorite features because, When I think of ideas or see a cool website while on my mobile. I just save it as a bite on mobile. While writing the blog, I'll just search and attach the stuff I had saved on my mobile. This magically improved the
ideation -> writing
phase.
A more narrowed-down use case would be reference image gathering for artists. Digital artists usually have a huge local folder called ref
or inspiration
. This becomes very hard to manage. Bites would provide the ability to tag/filter/search any way they prefer.
There were some generic use cases too. The other day, I found some cool wallpapers for my new PC while on my mobile and found some on my personal laptop. I just made them as bites and tagged them with #wallpaper
. As soon as I got home, I filtered the bites page with the tags, and boom! all of my collections in my PC.
Since the bites can be of any form factor, you can also collect code snippets/KaTeX formulas as markdown with full syntax highlighting, which you can later use in your wrights while writing!
The Tags
All your content(wrights & bites) is tagged via a central tagging system over which you have full control. You can open up the tag tree to view/search all of the tags. You can also view all the wrights & bites under a specific tag.
You can bulk detach and delete a tag right from the tag tree itself! These tags also serve as SEO keywords for published wrights, so make sure you tag them with relevant keywords when publishing.
Other Features
There are still a lot of features all over wrighter, this blog would grow huge if I had to cover them in a single blog...
- The ability to export your wright in markdown, whenever you want. This might come in handy if you are going to use wrighter as your markdown editor and other platforms like hashnode/devto as your blogging platform.
- Full mobile compatibility!
- write in one tab and preview in another tab.
- Dark and Light themes all over wrighter.
- copy from anywhere and paste as markdown. Wrighter recognizes and parses almost all of the common semantic content from the web ranging from text styles and images to code and HTML tables.
- GFM and KaTeX(very similar to LaTeX) suppport on both the wright editor and bites.
- Instant switch from offline to online modes and vice-versa.
- SEO controls for wrights like editing URL slugs and the OG image for the wright.
- extremely clean and minimal UI with simplistic UX and maximum functionality ~
Technical Challenges
Working on this project has been a huge learning experience for sure. PlanetScale was surely the best of the best in terms of developer experience. I migrated my schema and data from local to the cloud within seconds, the bites and wrights features are tested in separate branches before promoting them to prod. It was a unique DX that I'd never seen before and Prisma was the cherry on top!
The real technical challenge was making the editor, markdown parser and renderer align to the global application context. This meant that I had to learn how markdown processors worked under the hood.
A month ago I had no idea how markdown parsers, rendering, and text editors work, now I have a slight idea of how they work.
There were a lot of technical difficulties I faced while making wrighter. The tech stack was new for me(except nextjs) and it's been a very looooong time since I deployed a monolithic server, so I am also out of touch. Here's the tech stack I've used
- Nextjs - Frontend
- Fastify Nodejs - Backend
- MySQL PlanetScale - Remote DB
- Prisma - ORM
- IndexedDB(dexiejs) - Local + Cache DB
- Vercel - Frontend builds and deployments
- Render - Backend builds and deployments
The biggest challenge was mostly handling IndexedDB queries and managing the intersection between the code for guest users and logged-in users.
Making Wrighter offline and React Query
If you haven't heard of react query, it's basically an async state management tool. Before this project, I used react query for fetching and caching API calls and that's about it. But soon I found out that, it can also be used with any kind of async calls. The best part is that it's a global state, So I can inspect and use a query from any component just by using a hook.
The whole of wrighter's frontend is built around this idea. React query manages the state for both guest users and logged-in users with a single query. This solves the problem of managing two different state updates on the component level. The ground-level components don't need to care about where the data is coming from(either IndexedDB or PlanetScale), it just needs to understand the data structure, and the rest of it works magically! react query saves the day(yet again) ✨
Reinventing the wheel with IndexedDB
IndexedDB is supposed to be a minimal database solution in the browser for persistent data storage. Though it does not have the conventional "tables" that you see in databases, it does have object stores(mongodb-like), which means you can store any kind of data in a collection(arrays, blobs, array of objects, etc..). But on the other hand, PlanetScale uses MySQL. It is a full-fledged relational database. You can join tables and perform all the fancy queries. As you can see, these two are both extremes of data modeling!
The problem is that I had to manage two different schemas for the data. In order to sync the data with both IndexedDB and MySQL DB in the future. Although this was a non-existent problem and for the sake of future-proofing, I had to replicate the same schema for IndexedDB too. I created relational collections that hold foreign keys of other collections in the IndexedDB schema. It's kinda funny to think about it, but it works. Here's the same data model as IndexedDB dexie schema.
export class IDB extends Dexie {
wrights!: Table<WrightIDB>;
editorContext!: Table<WrightIDB>;
tags!: Table<Tag>;
tagWright!: Table<TagWright>;
bites!: Table<Bite>;
tagBite!: Table<TagBite>;
constructor() {
super("wrighter");
this.version(2).stores({
wrights: "++id, title, head, createdAt, updatedAt, userId, content",
editorContext: "++id, title, head, createdAt, updatedAt, userId, content",
tags: "++id, name, color, userId",
tagWright: "++id, tagId, wrightId",
bites: "++id, title, content, type, createdAt, updatedAt, userId",
tagBite: "++id, tagId, biteId",
});
}
}
This essentially means that I have to do joins and cascade delete/updates on code, for IndexedDB operations, which was a unique experience. Here's the code to delete a bite, that also cascade deletes the tag relations for it. This also applies to getting queries too. To GET something, I have to perform a fake inner join between two collections using code and then return the data.
const deleteBite = async (biteId: string) => {
await indexedDB.bites.delete(biteId);
// faking cascade delete
await indexedDB.tagBite.where("biteId").equals(biteId).delete();
};
It works flawlessly every time, so it was a satisfying problem to solve. This approach makes me worry less if I have to sync the whole IndexedDB and PlanetScale in the future. If you are trying to do the same, do not reinvent the wheel as I did. use something like dexie-relationships library instead.
Some Hiccups...
I wanted to use something new for wrighter and that's why I used fastify. I was more of an express guy before. The ability to add validator schemas was new to me in node backends, combining this with zod and typescript became confusing. There were several situations where I was missing fields for responses because I forgot to add them to the schema. I blame myself for that lol
The react Bytemd editor was meant to be a hackable markdown editor. I think it probably means about the different configurations it provides. But I wanted it even more hackable, I had to literally hack bytemd's context to use it globally. I got the editor's context and put it inside window
object to manipulate the editor from different components. After the editor unmounts I just remove the editorContext
from window
by simply doing window["editorContext"] = null
on the effect cleanup.
return {
// provides editor's context
// assume this runs as useEffect() hook for the editor
editorEffect(context: ByteMdEditorContext) {
if (window && shouldInjectToWindow) {
console.log("injecting context into window");
window["editorContext"] = context;
}
return () => {
if (window) {
window["editorContext"] = null;
console.log("destroyed editor context");
}
};
},
};
// window.d.ts
// type declaration for editor context
declare global {
interface Window {
editorContext?: BytemdEditorContext | null;
}
}
This was hacky but it worked. This is how the command bar knows about the editor's current context. When a command is selected, it used the window object to apply different formatting styles and perform actions on the editor.
Closing Notes
This has got to be one of the most satisfying projects I've ever worked on. It taught me many things like markdown parsers, AST, rehype, faux relational IndexedDB, etc.. that I never would've learned elsewhere.
This blog that you are reading right now is fully written with wrighter and published with wrighter. I just love how good it feels to use your own tools to create something cool.
I might be biased, but I highly recommend wrighter for any kind of blogs/scientific writing/brainstorming. It has greatly improved my existing workflow!
Wrighter was the project that I started in order to improve the workflow of writing articles -> getting reviews -> publishing
. But while working on it for a week, I found out that the writing articles
stage in itself can be improved, so I went back to the drawing board and situated my USP around the ease of writing and brainstorming. That was the best decision that I took because it worked out so well! This tweet below by Matt Wensing captures the story of how wrighter was born ⭐
Important Links
Read this same blog on Wrighter - Here
Wrighter Homepage - Wrighter
Contribute to Wrighter - Wrighter GitHub
My GitHub
Follow me on twitter
Please try out wrighter, I would love to get your feedback ❤️