DEV Community

Cover image for Wrighter (β) - A Powerful Markdown Blogger & A Writing Companion ⚡
TK Vishal
TK Vishal

Posted on • Originally published at wrighter.vercel.app

Wrighter (β) - A Powerful Markdown Blogger & A Writing Companion ⚡

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 anytime, be sure to capture them

  • 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!

the wrighter editor

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!

product banner - apple style

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!

blazingly fast command bar

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!

Offline and Online Mode

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.

difference between WYSIWYG vs WYSIWYM

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

how wrighter editor manages autosaves

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.

wrights homepage

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.

wright settings

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

ideas can come anywhere, at any time

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.

your bites page

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.

Quickly add your ideas into wrights

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.

create bite modal

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!

using bites to collect code snippets

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.

the tagtree

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. copy from StackOverflow with markdown syntax
  • 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

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.

react query as global state manager

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!

prisma data model for MySQL DB

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",
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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.

if it works, don't touch it

Some Hiccups...

solving imaginary problems

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 ⭐

tweet about how wrighter was born

Important Links

Please try out wrighter or contribute to wrighter, I would love to get your feedback ❤️

Top comments (2)

Collapse
 
jnv profile image
Jan Vlnas

I saw Wrighter on Product Hunt and thought that it's pretty neat. Especially the Bites feature is cool idea. Thanks for the write up and congratulations on the launch!

Collapse
 
tkvishal profile image
TK Vishal

thank you :)