Hugo Post Editor — Technical Documentation

A local, zero-server Hugo post editor with live linting, i18n, and smart media shortcodes. This document explains the architecture, code structure, data flow, rules, and implementation details.

Contents
  1. Overview & Goals
  2. Architecture & Files
  3. UI Composition
  4. Internationalization (i18n)
  5. State & Persistence
  6. Linting Engine
  7. Media Shortcode Conversion
  8. Export Pipeline
  9. Keyboard Shortcuts
  10. Accessibility
  11. Performance Considerations
  12. Privacy & Security Model
  13. Browser Compatibility
  14. Production/Build Notes
  15. Extension Points & Roadmap
  16. Troubleshooting
  17. License

1) Overview & Goals

Key idea: Keep the tool portable and inspectable. Use HTML + vanilla JS + Tailwind; avoid complex bundlers for the editor itself.

2) Architecture & Files

The editor is a single HTML file with embedded CSS/JS. No external JS frameworks are required.

UnitPurpose
head (Tailwind via CDN in prototype)Styling (dark mode via darkMode:'class').
UI SectionsHeader/Toolbar, Lint panel, Meta form, Body textarea, Help dialog, Settings off-canvas.
ConstantsPRESET_CATEGORIES, PRESET_TAGS, I18N (DE/EN).
StateSingle object serialized to localStorage (STORAGE_KEY = "mk_hugo_editor_state_v1").
Core Modulesi18n, Toolbar actions, Linting (meta + body + shortcode validation), Export, Autosave.

High-level data flow

┌────────────┐   input/change   ┌───────────────┐   render   ┌───────────────┐
│   UI Form  │ ───────────────▶ │  State (in-mem)│ ─────────▶ │  Lint Results │
└─────┬──────┘                   └───────┬───────┘            └───────────────┘
      │ save (debounced)                │
      ▼                                 ▼
  localStorage  ◀──────── restore ──────┘

Export click:
  State + Body MD → escapeShortcodesEverywhere → convertInlineMedia → YAML Front Matter → Blob(index.md)

3) UI Composition

Key DOM IDs

#title #slug #dateInput #description #cover #coverAlt
#category #tagPicker #tags #categoryPreview
#draft #showToc #videoPreload #body
#lintList #lintSummary #lintSummaryMobile #badgeErrors #badgeWarns #badgeInfos
#btnExport #btnClear #btnRecheck #helpDlg #settingsDlg

4) Internationalization (i18n)

A plain object I18N holds DE/EN strings for labels, titles, hints, and lint messages. Language is persisted in localStorage("mk_lang").

function applyI18n(lang) {
  const d = I18N[lang] || I18N.de;
  // data-i18n: innerHTML, data-i18n-title: title attribute, data-i18n-ph: placeholder
}

5) State & Persistence

The editor serializes form values + UI prefs to localStorage.

STORAGE_KEY = "mk_hugo_editor_state_v1"

state = {
  title, slug, date, description,
  cover, coverAlt, category,
  tags: [...chips], draft, showToc,
  videoPreload, body,
  themeDark: documentElement.classList.contains('dark'),
  lang: currentLanguage
}

6) Linting Engine

Linting runs on each input/change and combines three passes:

  1. Front-Matter checks (lintFrontMatterMeta())
  2. Body checks (lintMarkdown(md))
  3. Shortcode attribute validation (validateShortcodes(md, issues))

Front-Matter Rules

Body Rules

Shortcode Validation

Escaping Shortcodes Inside Code

Shortcodes within inline/code fences are escaped and later restored to avoid accidental rendering by Hugo.

// Pseudocode
function escapeShortcodesEverywhere(md) {
  // 1) capture fenced blocks → replace with tokens
  // 2) escape shortcode markers inside `...` and fences
  // 3) restore tokens
}

7) Media Shortcode Conversion

Lines containing only a filename are auto-converted on export:

const justFile = /^(?:\.\/)?(?:images\/)?([A-Za-z0-9._\-()+\s%]+)$/;
const imageExt = /\.(jpg|jpeg|png|webp|gif)$/i;
const videoExt = /\.(mp4|webm|mov)$/i;

8) Export Pipeline

  1. Run lintAll(). If errors → confirm continue.
  2. Assemble YAML Front Matter (with escaping quotes/backslashes).
  3. Transform body:
    1. escapeShortcodesEverywhere(body)
    2. convertInlineMedia(body, videoPreload)
  4. Create Blob and trigger a user-initiated download of index.md.
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = "index.md"; a.click();
setTimeout(() => URL.revokeObjectURL(url), 500);

9) Keyboard Shortcuts

ShortcutAction
Ctrl/⌘ + BBold
Ctrl/⌘ + IItalic
Ctrl/⌘ + KInsert link
Ctrl/⌘ + `Inline code
Ctrl/⌘ + Shift + `Code block
Ctrl/⌘ + Alt + 1..3H1–H3
Ctrl/⌘ + Shift + 8Bulleted list
Ctrl/⌘ + Shift + 7Numbered list
Ctrl/⌘ + Shift + 9Quote
Ctrl/⌘ + Shift + -Horizontal rule
Alt + Shift + IFigure shortcode
Alt + Shift + VVideo shortcode

10) Accessibility

11) Performance Considerations

12) Privacy & Security Model

13) Browser Compatibility

14) Production / Build Notes

15) Extension Points & Roadmap

16) Troubleshooting

“Download doesn’t start” in some browsers

“Lint says code block unclosed”

“Figure/Video invalid path”

17) License

Copyright © mitkaracho.de. Choose a license that fits your repo (e.g., MIT/Apache-2.0). Update this section accordingly.

Appendix: Key Functions (Index)

i18n
  • getLang(), dict(), applyI18n(lang), setLang(lang)
Theme
  • toggleTheme()
State
  • saveState(), saveStateDebounced(), restoreState()
Toolbar
  • wrapSelection(), wrapBlock(), toggleLinePrefix(), makeOrderedList(), insertLink(), applyHeading(), insertFigure(), insertVideo()
Lint
  • lintFrontMatterMeta(), lintMarkdown(), validateShortcodes(), lintAll()
Export
  • downloadMarkdown(), exportDoc()
Media
  • convertInlineMedia(), escapeShortcodesEverywhere()

Questions or suggestions? Open an issue in the repository.