I always thought that using our own custom solutions over the ready ones is not necessary but genuine idea because we face with an unknown in other words real challenge. I used to write my articles using Obsidian and manually update the DB so far.
It turns out actively using this app to write and manage my technical articles. Just like this one π
The open-source Ultimate Blog Editor App brings together the Markdown editing experience of Obsidian and the publishing flexibility of Substack in a single workspace.
The stack doesn't cost a penny. Supabase and Netlify free plans are suitable. It won't let you down halfway through even at scale.
That said, it comes with amazing features listed down below:
- Rich-Text Editing
- Blazing Fast Editing
- Markdown Editing Mode
- MDX Content Viewer
- Dynamic Error Display for Compiled MDX Source
- Media Library
- Image Optimization (converting primary formats to
.webp) - Extensive Autocomplete In English
- Custom Content Components (including charts from
recharts) - Command Center
- 50+ Predefined Eye Soothing Backgrounds
- Draft & Post Management
- Document Serialization and Compression
- URL-Based Document State
- Comprehensive Keyboard Shortcuts With Full Accessibility Support
- Dialogs, Sheets, Shared Controls
- Feedback and Cloud Syncing
- Backend Integration
This project is primarily tailored for my personal use, but it is also intended to serve as a reference for others interested in the editor architecture and development of editor features. It's so easy to change the serialized custom components and allowing you to curate them as you wish.
A snippet from the collection of app features:
It serve as a starting point for building custom editor apps with its highly modular architecture and extensibility. Just like a puzzle box, it can be customized to fit your needs.
| tech | version |
|---|---|
| Next.js | β₯16 |
| React / React DOM | >19 |
| TypeScript | β₯5.0 |
| Lexical | β₯0.49.0 |
| Supabase JS | β₯2.112.3 |
| Tailwind CSS | β₯4.0.0 |
| shadcn | β₯4.19.0 |
| Base UI | β₯1.7.0 |
| next-mdx-remote-client | β₯2.1.12 |
| React Hook Form | β₯7.87.0 |
| Zod | β₯4.5.4 |
My experience on very first blog using this editor was pretty good actually you can write your thoughts abot the repo maybe some improvements down the blog using this link
For inspiration purposes you can always check out my Github portfolio repo slug to see that how I made the integration for my SSG rendered Next.js personal blog.
Of course, those who rely on others' solutions rather than coming up with their own in 2026 are not fools or timid. What I mean is that whatever Substack or Obsidian provides is maybe convenient enough for you.
Therefore you might not end up with going further, it's totally up to you.
As far as I am concerned, these techs don't meet my expectations on their own, but would meet them if they were combined, if you're the one looking from the same window like me then you've come to the right place.
The app follows a modular architecture that separates editing, document previews, and content management into dedicated feature areas. Shared UI components, an extensible Lexical editor, and centralized server actions keep the code organized and make it easier to maintain and expand. Respected to DRY principles heavily.
- Feature modules:
features/separates the editor, document viewer, and management tools for drafts, posts, and the media library. - Extensible editor:
components/editor/organizes Lexical nodes, plugins, extensions, toolbars, and Markdown transformers. Charts and Contents have dedicated modules for editing, rendering, and conversion. - MDX rendering:
components/mdx/defines content components and processing plugins, whilelib/mdx/handles serialization and related error handling. - Server and DB layer:
app/actions/groups content operations, supported by the server database client inlib/db/and generated database types intypes/. - State and validation:
store/document.store.tsholds document state,providers/tools-provider.tsxsupplies shared tool context, andschema/defines draft validation. - Shared interface:
components/ui/, reusable modals, andhooks/provide consistent controls, dialogs, keyboard shortcuts, and URL parameter handling.
The working tree is pretty straightforward and scalable with it's modular design:
src/
βββ app/
β βββ page.tsx # Entry point
β βββ actions/ # Server-side content operations
βββ features/
β βββ editor/ # Editor workspace
β βββ document-viewer/ # Content preview
β βββ tools/ # Drafts, posts, and media management
βββ components/
β βββ editor/ # Lexical nodes, plugins, and extensions
β βββ mdx/ # MDX rendering components
β βββ ui/ # Shared interface components
βββ lib/ # Auth, database, and serialization
βββ store/ # Document state
βββ providers/ # Shared tool context
βββ hooks/ # Reusable React hooks
βββ hoc/ # Content and metadata wrappers
βββ schema/ # Validation schemas
βββ types/ # Application and database types
βββ constants/ # Tool and editor configuration
βββ utils/ # Supporting utilities
βββ proxy.ts # Request interception
You will need:
- Node.js >= 22
- npm
Clone the repository:
git clone https://github.com/Masculinn/blog-editor.git
cd blog-editorInstall dependencies:
npm install
β οΈ CAUTION: Before continuing further you must configure the DB layer. Do not attempt running the dev server unless all the database environments and tables are configured properly. See instructions below to be able to start the dev server.
Visit Supabase auth page to signup or if you have already an account simply login. Create a new organization and choose your own namespace. After that, create a table called
blog_posts and paste the given PostgreSQL snippet below into the SQL Editor section placed in the sidebar of your dashboard.
This will create our very first table for our articles.
create table public.blog_posts (
Β id serial not null,
Β title text not null default 'title'::text,
Β content text not null,
Β tags text[] not null,
Β published_at timestamp with time zone null default now(),
Β description text not null default 'I AM JOHN DOE BUT CANT PROVE IT'::text,
Β banner_image text not null default 'MY_DEFAULT_IMAGE_URL'::text,
Β level numeric not null default '1'::numeric,
Β constraint blog_posts_pkey primary key (id),
Β constraint blog_posts_level_check check ((level > (0)::numeric))
) TABLESPACE pg_default;After configuring the DB and ensuring everything is stable, you can curate the predefined tables however you want.
Next, we need the secondary table for our article candidates that isn't actually ready to be considered under the category of posts. I call it draft.
Paste the given snippet below to create draft table into the SQL Editor as well.
create table public.drafts (
Β id serial not null,
Β title text not null default 'title'::text,
Β content text null default 'The quick brown fox jumps over the lazy dog.'::text,
Β tags text[] null,
Β published_at timestamp with time zone null default now(),
Β description text null,
Β banner_image text null,
Β level numeric not null default '1'::numeric,
Β constraint drafts_pkey primary key (id),
Β constraint drafts_level_check check ((level > (0)::numeric))
) TABLESPACE pg_default;These tables are almost identical. The only difference is the partially defined columns, which are in response to the separation of concerns principle.
Finally, we're one step away to be done on DB configuration which is creating a bucket called banner. To do that, move into the storage page via dashboard > sidebar and create a new bucket called banner.
I'm currently using my bucket publicly because I treat my bucket as an external CDN point for my media elements in my site β idc how unsafe it is so if you think that things in it must remain private then you can leave the toggle state as-is like the one showed above.
Create a brand new env file called .env at the root of your project and get the corresponding values of your env keys from project settings of your Supabase organization.
SUPABASE_URL = YOUR_PROJECT_URI;
SUPABASE_SERVICE_ROLE_KEY = YOUR_SERVICE_ROLE_KEY;
BUCKET_NAME = banner;
APP_TOKEN = YOUR_APP_BASE64_PSW;
APP_USER_AGENT = YOUR_APP_USER_AGENT_WITHOUT_WHITESPACE;The app uses the typegen for DB types for in particular for React actions. Everything in the package.json file is configured for you to invoke codegen but before doing so, you must login to your Supabase account via CLI
Run the CLI command to login your account in your IDE.
npx supabase loginThis is going to open up a new window inside of your browser to SSO. Follow the instructions given in the CLI to login then run the following command to fetch the DB types
npm run db:typesStart the development server:
npm run devThe current development script starts Next.js on portΒ 36805:
http://localhost:36805That's pretty much everything so far to run the app on locally, it takes max 5 minutes from scratch, I think it's fair.
It's worth to mention that the Unix-based users like mac or linux OS, a bit differs in particular scenarios such as key bindings. If you're the one, you may want to configure the app shortcuts in a way that the modifier key meta handles the shortcut over Control because initially it's defined to Control
Simply find the components in your IDE search with a match for the hook imported as:
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut";And configure like:
useKeyboardShortcut(["Meta", "/"], () => {
/* INTERNAL CALLBACK */
});Consider
CtrlasMetaplease mac & linux users.
Press Ctrl + K to toggle the Command Center, even while editing. Search for a tool, navigate with β / β, and press Enter to open its modal. Selecting a tool automatically closes the Command Center.
| Shortcut | Action |
|---|---|
Ctrl + K |
Toggle the Command Center |
β / β |
Navigate tools |
Enter |
Open the selected tool |
Esc |
Dismiss the active dialog |
Available tools: View Posts, View Drafts, Create Draft, Media Library, Manage Drafts, and Manage Posts.
| Shortcut | Purpose |
|---|---|
Ctrl + M |
Toggle between rich-text editing and Markdown mode. Source |
Ctrl + / |
Show or hide the document viewer. Requires a selected post or draft. Source |
Ctrl + S |
Save content changes to the selected draft or post while the document viewer is mounted. Source |
If you'd like to publish the posts powered by SSG render power in Next.js Pages router that maximizes your SEO by god knows how many times, you can take a look at my blog app's Github Repo to inspire by.
Please ensure that the DB configuration has done properly before moving on this part
The app may fail during the build when next.js is used in your stack and to prevent this add 2 more env key-pair to the env file alongside the existing keys to prevent this behaviour in your Netlify dashboard. Although I don't know the reason, I found this patch due to an error caused by its external systems.
SECRETS_SCAN_ENABLED=false
SECRETS_SCAN_OMIT_PATHS=.netlify/.next/cacheContributions are welcome! Open an issue to report a bug, suggest a feature, or discuss a significant change before starting.
- Fork the repository and create a branch for your changes.
- Install dependencies with
npm install. - Start the development server with
npm run dev. - Make your changes, keeping them focused and consistent with the existing architecture.
- Run
npm run checkandnpm run buildbefore submitting. - Open a pull request describing the changes, any related issues, and how you tested
- them. Include screenshots for UI updates.
Keep credentials and environment files out of your commits.
Huge thanks to this repo and its hard-working contributors for allowing me to curate their Shadcn Editor! Lovely set of backgrounds are also scraped from patterns craft definitely worth to add in your checklist.