NW' Blog
July 10, 2026 · Content Page

markdown2article Article Editor

A full-featured Markdown-to-Astro article editor with live preview, TOC generation, and one-click export.

markdown2article Article Editor

Purpose

Gives blog authors a visual editing environment for completing the following on a single page:

  1. Fill in the article's metadata (title, meta, activeNav, comment toggle, etc.)
  2. Edit sidebar-left (tagline, badge icon/text), sub-sidebar (tag list), and sidebar-right (promo)
  3. Write the Markdown body with a live rendered preview and automatic TOC generation
  4. Insert HashedImage, XMind mind maps, SPZ 3D Gaussian splat models, and terminal animation code snippets via dialogs
  5. One-click export to a .astro file ready to use in your project
  6. Load external Markdown files from the ?load= URL parameter

Feature Overview

Feature Location Description
Page title / browser titlefirst row of editor-settingsTwo separate inputs; the browser title automatically appends · NW' Blog
meta date, excerptsecond row of editor-settingsOptional excerpt, hidden when empty
activeNav selectionsecond row of editor-settingsControls which nav item is highlighted
Comment section togglesecond row of editor-settingsWhen checked, the comment section is visible in the preview
Tag editingsub-sidebar textareaOne name|link entry per line
Tagline editingsidebar-left inputControls the description text below the avatar
Badge editingsidebar-leftIcon picker + text input → badge updates live
Icon pickersidebar-left → pick iconPops up a fixed-position panel; search + click to select
Import MD filetoolbar → Import MDReads a local .md file into the editor
Insert HashedImagetoolbar → Insert HashedImageDialog: path, alt, figcaption, actual-size checkbox
Insert XMindtoolbar → Insert XMindDialog: JSON path, dark mode, layout, line style
Insert SPZtoolbar → Insert SPZDialog: .spz path, auto-load checkbox
Insert terminal codetoolbar → Insert TerminalDialog: title, code content; HTML is escaped automatically
MD editorarticle-panelMonospace textarea; the size counter shows a KB value
Live previewarticle-panelGFM Markdown → sanitized HTML
TOCsidebar-rightAutomatically extracts h1–h4; click for smooth scrolling; hierarchical indentation
Promo editingsidebar-rightEditable title + content, synced with the preview area
Mode switchfixed mode-barEdit/preview modes; preview mode hides all editing controls
Export buttonfixed float-btnDownloads the complete .astro file
URL loadingquery parameter?load=/path/to/file.md automatically loads remote Markdown

Page Layout

┌─ editor-mode-bar (fixed top:70, z-index:1000) ──────────┐
│  [Edit]  [Preview]                                       │
└──────────────────────────────────────────────────────────┘

┌─ main.main-layout[data-sidebar-mode="both"] ─────────────┐
│ ┌─ left-column ──┐ ┌─ article-panel ─────┐ ┌─ sidebar-right ┐ │
│ │ sidebar-left   │ │ editor-settings     │ │ TOC            │ │
│ │   logo/avatar  │ │ toolbar + dialogs   │ │ promo editor   │ │
│ │   tagline      │ │ md-editor textarea  │ │ promo display  │ │
│ │   badge        │ │ rendered-preview    │ └────────────────┘ │
│ │ edit controls  │ └──────────────────────┘                   │
│ │ sub-sidebar    │                        CommentWidget       │
│ │   tag list     │                         (4th grid child)   │
│ └────────────────┘                                            │
└──────────────────────────────────────────────────────────────┘

┌─ editor-float-btns (fixed bottom:76 left:24) ───────────┐
│  [export]                                                │
└──────────────────────────────────────────────────────────┘

The three column widths are defined by .main-layout in global.css280px minmax(0,1fr) 260px.


Core Workflows

Edit fields → live preview

All text/select inputs are bound to the input event → updatePreview():

  • Updates the h1 (from pageTitle), meta, and excerpt
  • Updates the tagline and the badge icon and text
  • Parses the tags textarea → renders <ul class="tag-list">
  • Updates the promo title and body
  • Assembles and displays the browser title
  • Calls renderMarkdown() to refresh the Markdown preview and TOC
  • Shows/hides the comment section based on the comment toggle

Markdown editing → rendered preview + TOC

  • updatePreview() fires after a 300ms input debounce
  • renderMarkdown() uses marked.parse() + the GFM extension → HTML
  • Filtered through sanitizeHtml(): removes <script>, <style>, <iframe> (which are preserved), on* attributes, javascript: links, etc.
  • generateTOC() scans h1–h4 in the preview area, generating an <li> with data-level for each heading; clicking smooth-scrolls

Inserting the custom components

All four dialogs toggle via display:none/block. On insert, insertToEditor(tag) is called:

  • Inserts the tag at the textarea's cursor position
  • Automatically updates the character counter and the preview
  • HashedImage: <HashedImage src="..." alt="..." /> with optional class="content-image-actual"
  • XMind: generates an <iframe> pointing to /ToolBase/Xmind/XmindViewer_embed.html?dark=...&layout=...&data=...
  • SPZ: generates an <iframe> pointing to /ToolBase/3DViewer/3DViewer.html?url=...; in non-auto-load mode it additionally renders a click-to-play placeholder
  • Terminal code: generates an HTML block with the .terminal-code structure (title bar + red/yellow/green control dots + <pre><code>), automatically HTML-escapes the code content, and preserves the user's original indentation

Edit/preview switching

Toggles editor-mode-preview-active via document.body.classList:

  • In CSS, :global(body.editor-mode-preview-active) .editor-settings and the like are all display: none !important
  • Preview mode hides all editing controls and the floating export button
  • In preview mode, the comment section is shown/hidden by updatePreview() according to the checkbox

Exporting the .astro file

Clicking the floating export button → runs the click handler of DOM.exportBtn:

  1. Collects the current value of every field
  2. Builds the complete Astro frontmatter + HTML template
  3. Splits the Markdown content into blocks (see the export logic in detail below)
  4. Generates a Blob → automatically downloads {slug}.astro
  5. The button icon temporarily switches to ✅ as feedback (reverts after 2 seconds)

Loading via URL parameter

When visiting ?load=/path/to/file.md:

  • Automatically fetches that path
  • On success, fills the MD editor and triggers a preview update

HTML Structure

html

<BaseLayout title="...">
  <main class="main-layout" data-sidebar-mode="both">

    <!-- column 1 -->
    <div class="left-column">
      <aside class="sidebar-left">    ← logo, avatar, tagline, badge, editor-field controls
      <aside class="sub-sidebar">      ← h2 + editor-field(textarea) + dynamic tag-list
    </div>

    <!-- column 2 -->
    <article class="article-panel">    ← editor-settings, toolbar, dialogs, md-editor, preview

    <!-- column 3 -->
    <aside class="sidebar-right">      ← TOC + promo-editor + promo-display

    <!-- 4th child (still inside main) -->
    <CommentWidget />                   ← comment section
  </main>

  <!-- the following sit outside </main>, independently positioned -->
  <div id="editor-mode-bar">            ← fixed top:70
  <div class="editor-float-btns">      ← fixed bottom:76 left:24 (export button)
</BaseLayout>
            

Note: CommentWidget is placed inside <main> as the 4th grid child, and the .comment-panel { grid-column: 2; } rule in global.css positions it below the middle column.


CSS Architecture

Design system

Token Value
Base background colors#09090f / #1a1c2a
Text color#dcd7cc
Accent color#ff6925
Glassmorphismbackdrop-filter: blur(18px)
Bordersrgba(255,255,255,0.08~0.20)
Border radius8px / 12px / 14px

Key classes

Class Purpose
.editor-mode-barFixed top bar with the edit/preview toggle buttons
.editor-mode-btn--activeActive mode state (orange background)
.editor-settings / .editor-settings-rowPage settings area, flex layout
.editor-labelField labels: orange left border + uppercase + hover highlight
.editor-inputGeneric input, dark theme style
.editor-selectCustom select, appearance: none + SVG arrow + focus glow
.editor-checkboxCustom checkbox, appearance: none + orange check + SVG checkmark
.editor-textareaGeneric multiline input, resizable height
.editor-md-editorMarkdown editing area, monospace font, min-height: 323px
.editor-icon-dropdownIcon picker popup panel, position: fixed; z-index: 999999
.editor-icon-gridIcon grid with 8 columns, with search filtering
.editor-dialogInsert dialogs, glassmorphism background
.editor-toolbar / .editor-toolbar-btnToolbar button group
.editor-float-btnFloating button 44×44px, with -webkit-backdrop-filter fallback
.editor-md-sizeMarkdown size counter (KB)

Preview mode CSS

Uses the :global(body.editor-mode-preview-active) selector to hide all editing controls (display: none !important). Each control has an animation: editorFadeIn entrance animation.

Scrollbars

.editor-textarea, .editor-md-editor, .editor-icon-dropdown, and .editor-icon-grid are all customized as thin orange scrollbars.

Notes

  • backdrop-filter creates a new stacking context → the icon dropdown works around it with position: fixed + JS appendChild(body)
  • The -webkit-backdrop-filter prefix ensures Safari compatibility

JavaScript Modules

Core functions

Function Location Responsibility
sanitizeHtml(html)utilityDOMParser filters XSS: removes script/style/iframe/on*/javascript:
updateMdSize()utilityComputes the MD byte size with TextEncoder, displayed in KB
renderMarkdown()renderingmarked.parse()sanitizeHtml() → writes the preview + TOC
generateTOC()renderingScans h1–h4 → data-level list, id handling, smooth scrolling
updatePreview()coreCollects all fields → syncs them to the preview DOM + renders Markdown + toggles the comment section
positionIconDropdown()iconsComputes the button position, prevents overflow on the right, fixed positioning
insertToEditor(tag)insertionInserts text at the cursor, updates the counter + preview
escapeAttr(s)exportEncodes HTML attributes (& " < >)
export logicexportSee the export logic in detail below

DOM references

The DOM object centrally manages all getElementById references (~50 elements), defined together at the top.

Event bindings

  • All editing fields: inputupdatePreview()
  • MD editor: inputupdateMdSize() + 300ms debounce → updatePreview()
  • Icon search: input → filters the grid
  • Icon grid click → select + close + update preview
  • Export button click → build → download
  • Mode buttons click → toggle the body class + update preview
  • TOC click → highlight + smooth scroll
  • Import button click → triggers the hidden <input type="file">
  • Dialog buttons click → toggle visibility + insert

Initial load

On DOMContentLoaded:

  1. Calls updatePreview() to populate the initial state
  2. Checks the ?load= URL parameter → asynchronously fetches and fills the editor

Export Logic in Detail

The export button is the editor's core output feature. Here is the flow it executes:

1. Collect the fields

Reads the current values from all editing controls: title, meta, activeNav, excerpt, tagline, badge, tags, promo, comment toggle, MD content.

2. Extract the h1 from the Markdown

Uses the regex /^#\s+(.+)/m to extract the first level-1 heading from the MD content and use it as the <h1>, falling back to pageTitle if none exists.

3. Build the Astro template

Generates the complete Astro file string, including:

  • the frontmatter --- block (BaseLayout, HashedImage, and conditional CommentWidget imports)
  • the <BaseLayout> wrapper structure
  • the full sidebar-left / sub-sidebar / article-panel / sidebar-right skeleton

4. Markdown block splitting

The exporter analyzes the Markdown content line by line and splits it into the following types:

Block typeRuleExport result
Plain MarkdownNon-empty line + not a raw HTML tagWrapped in <div class="section-block">
Raw HTML block (self-closing)Starts with <HashedImage and ends with />Kept as-is, not wrapped in section-block
Raw HTML block (multi-line <div>)Starts with <div class="..."> and matches RAW_TAGSEnters a multi-line accumulation state machine that tracks <div>/</div> nesting depth; once closed, the whole block is kept and not wrapped in section-block
Code block``` at the start of a line toggles the stateThe whole code block is treated as one block
Empty lineSeparator between consecutive blocksTriggers flushBlock()
Multi-line accumulation: when <div class="terminal-code", <div class="xmind-embed-wrapper", or <div class="splat-viewer-wrapper" is encountered, the exporter enters the inRawHtml state and accumulates lines until the matching </div> closes. It also cleans up the extra leading spaces inside <code> caused by the uniform indentation (only removing \n 6, preserving the user's original indentation).

Once splitting is done, all blocks are concatenated in order to form the article's complete body.

5. Conditional output

  • If the comment toggle is on → import CommentWidget + render the comment section, then close </main> and </BaseLayout>
  • If the comment toggle is off → omit the import and rendering

6. Download

Generates a slug from the title → creates a Blob → automatically triggers an <a> download of {slug}.astro.


Icon Picker

The FA_ICONS array contains ~180 Font Awesome 6.5 icons (fa- prefix, all in the fas style).

Interaction flow

  1. Click the button → JS appendChilds the dropdown to the body (taking it out of the sidebar-left stacking context)
  2. positionIconDropdown() computes the button position, preventing overflow on the right
  3. The search box filters live (datase.icon.includes())
  4. Click an icon → _selectedIcon is updated + the menu closes + the preview refreshes
  5. Click outside → the menu closes
  6. On scroll → repositioned

Technical details

  • position: fixed; z-index: 999999 + being removed from the original DOM tree ensures it is not covered by the background filter layers
  • Width 323px, max height 240px, 8-column grid, overflow-x: hidden

Dependencies

PackagePurpose
markedMarkdown → HTML parsing (GFM + breaks)
Astro built-insBaseLayout, HashedImage, CommentWidget components
Font Awesome 6.5Icon library (CDN, loaded by BaseLayout)

No additional runtime dependencies.


Maintenance Notes

Common pitfalls

  1. backdrop-filter and stacking contexts — the backdrop-filter on .sidebar-left creates a new stacking context, breaking inner z-index values. The icon dropdown's workaround: JS moves the element to <body> + position: fixed.
  2. :global() scoping — Astro's scoped CSS cannot reach body. That is why the preview-mode toggle uses the :global(body.editor-mode-preview-active) selector.
  3. Comment section positioning — the .comment-panel rendered by CommentWidget is the 4th child of <main> and relies on grid-column: 2 from global.css to land below the middle column. Keep this in sync when changing the main-layout grid definition.
  4. data-sidebar-mode="both" — must be set on <main> so that initSidebarToggle() in site.js activates the state machine correctly.
  5. Raw HTML preservation on export — self-closing tags (<HashedImage ... />) are matched on a single line and flushed immediately; <div> block tags (terminal-code, xmind-embed-wrapper, splat-viewer-wrapper) enter the multi-line accumulation state machine, which tracks the <div> nesting depth. Inside <code>, the extra \n 6 prefix indentation is additionally cleaned to protect code rendering. If new Astro component tags are added in the future, the RAW_TAGS array must be updated accordingly.
  6. -webkit-backdrop-filter — Safari requires this prefix; both the floating button and the mode-bar must provide the standard property alongside it.
  7. Comment toggle control — controlled by DOM.commentPanel.style.display inside updatePreview(); when switching preview modes, updatePreview() must also be called manually to keep everything in sync.

Extension guide

  • Adding a new editing field: add the input/textarea/select in the HTML → add a reference to the DOM object → add the key to the LIVE_FIELDS array → add sync logic in updatePreview() → add collection in the export logic.
  • Adding a new insertable component type: add a button + dialog to the toolbar → call insertToEditor(tag) when the dialog is confirmed → if the inserted content is an Astro component or a <div> block, add its prefix to RAW_TAGS so it is not wrapped in section-block on export. For <div> blocks, also make sure they carry the .terminal-code class to trigger the multi-line accumulation logic.
  • Adding a new FA icon: append the icon name to the FA_ICONS array; no extra configuration needed.
Comments Leave your thoughts
Guide