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.
- Overview & Goals
- Architecture & Files
- UI Composition
- Internationalization (i18n)
- State & Persistence
- Linting Engine
- Media Shortcode Conversion
- Export Pipeline
- Keyboard Shortcuts
- Accessibility
- Performance Considerations
- Privacy & Security Model
- Browser Compatibility
- Production/Build Notes
- Extension Points & Roadmap
- Troubleshooting
- License
1) Overview & Goals
- Zero backend: Everything runs in the browser. No network calls, no servers.
- Consistency: Guide authors to Hugo-friendly Markdown (Front Matter first; avoid body
# H1, etc.). - Quality: Live linting for front matter and body text with actionable hints.
- Speed & UX: Toolbar + shortcuts, autosave, mobile settings drawer, dark mode.
- Safety: Escape shortcodes inside code fences; validate figure/video attributes.
2) Architecture & Files
The editor is a single HTML file with embedded CSS/JS. No external JS frameworks are required.
| Unit | Purpose |
|---|---|
head (Tailwind via CDN in prototype) | Styling (dark mode via darkMode:'class'). |
| UI Sections | Header/Toolbar, Lint panel, Meta form, Body textarea, Help dialog, Settings off-canvas. |
| Constants | PRESET_CATEGORIES, PRESET_TAGS, I18N (DE/EN). |
| State | Single object serialized to localStorage (STORAGE_KEY = "mk_hugo_editor_state_v1"). |
| Core Modules | i18n, 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
- Header: Title, language selector, theme toggle, clear/export buttons, lint summary badge.
- Lint panel: Counts for Errors/Warnings/Infos + list with line numbers.
- Meta form: Title, slug, date (+“Today”), description, cover filename & alt, category, tags, draft and ShowToc, default video preload.
- Toolbar: H1–H3, bold/italic, quote, hr, ul/ol, link, inline/code blocks, figure, video.
- Body: Main Markdown textarea, input → triggers autosave + lint.
- Dialogs: Help (Markdown & shortcuts), Settings (mobile off-canvas for theme/lang).
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
}
- Debounced autosave (400ms) on input/change.
- Restore on load (with defensive try/catch).
6) Linting Engine
Linting runs on each input/change and combines three passes:
- Front-Matter checks (
lintFrontMatterMeta()) - Body checks (
lintMarkdown(md)) - Shortcode attribute validation (
validateShortcodes(md, issues))
Front-Matter Rules
- Title length (warn if very short / info if > 90).
- Date required, format
YYYY-MM-DD, valid range (warn if far in future). - Category present (info if not in presets).
- Tags present (info if empty).
- Description length recommendation (warn if > 160; info if < 30).
- Cover alt text recommended for accessibility/SEO.
- Slug character whitelist
[a-z0-9-].
Body Rules
- Warn if body contains an H1 (Hugo takes
titlefrom Front Matter). - Warn on heading level skips (e.g., H2 → H4).
- Info on long lines (> 120 chars), trailing whitespace, tabs, NBSP, curly quotes, ellipsis char.
- Warn on suspicious links (missing/empty URLs).
- Warn on raw shortcode pattern in prose (
{{< … >}}) outside code fences. - Error on unclosed code fences.
Shortcode Validation
figure: requiressrc, must start withimages/.video: requiressrc, must start withimages/,preloadin{auto|metadata|none}.
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:
- Image extensions:
.jpg .jpeg .png .webp .gif→{{< figure src="images/NAME" alt="" caption="" >}} - Video extensions:
.mp4 .webm .mov→{{< video src="images/BASENAME" preload="<setting>" >}}
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
- Run
lintAll(). If errors → confirm continue. - Assemble YAML Front Matter (with escaping quotes/backslashes).
- Transform body:
escapeShortcodesEverywhere(body)convertInlineMedia(body, videoPreload)
- Create
Bloband trigger a user-initiateddownloadofindex.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
| Shortcut | Action |
|---|---|
| Ctrl/⌘ + B | Bold |
| Ctrl/⌘ + I | Italic |
| Ctrl/⌘ + K | Insert link |
| Ctrl/⌘ + ` | Inline code |
| Ctrl/⌘ + Shift + ` | Code block |
| Ctrl/⌘ + Alt + 1..3 | H1–H3 |
| Ctrl/⌘ + Shift + 8 | Bulleted list |
| Ctrl/⌘ + Shift + 7 | Numbered list |
| Ctrl/⌘ + Shift + 9 | Quote |
| Ctrl/⌘ + Shift + - | Horizontal rule |
| Alt + Shift + I | Figure shortcode |
| Alt + Shift + V | Video shortcode |
10) Accessibility
- Theme respects prefers-color-scheme; explicit toggle persists in
localStorage("mk_theme"). - Toolbar has
role="toolbar"& labels; buttons expose informativetitleattributes. - Keyboard navigation is supported; focus styles on interactive elements.
- Lint hints encourage alt text for images used as cover.
- Reduced motion: critical UI animations disabled when prefers-reduced-motion is set.
11) Performance Considerations
- All linting/updates are linear in document length; suitable for typical blog posts.
- Debounced autosave (400ms) avoids excessive
localStoragewrites. - Export is synchronous in the click handler to satisfy pop-up blockers (user gesture).
12) Privacy & Security Model
- No network requests: The app does not send content anywhere.
- All data lives in the browser (RAM +
localStorage). - Shortcode escaping prevents accidental execution/rendering by Hugo when code is copied.
13) Browser Compatibility
- Modern Chromium, Firefox, and Safari are supported (uses
Blob,URL.createObjectURL). - Legacy browsers without these APIs are not supported.
14) Production / Build Notes
- Tailwind: For production, build Tailwind locally (PostCSS/CLI) instead of the CDN comment. Purge unused styles to keep payload small.
- Hosting: Serve as a static file (e.g., in your project docs or a
/toolsdirectory). No headers or special server config required. - Localization: To add languages, extend
I18Nand ensure alldata-i18n*attributes are covered.
15) Extension Points & Roadmap
- Custom lint rules: Add to
lintMarkdown/lintFrontMatterMeta. - Per-section presets: Templates for different content types (e.g., Travel, DIY, Review).
- Issue export: JSON report for CI tooling.
- Media validation: Optional drag-and-drop with local
FileAPI (still offline).
16) Troubleshooting
“Download doesn’t start” in some browsers
- Ensure export happens within the same synchronous click handler (already implemented).
- Disable aggressive pop-up blockers for the page if necessary.
“Lint says code block unclosed”
- Check for matching
```. The linter toggles state per fence.
“Figure/Video invalid path”
- Shortcodes require
srcto start withimages/(Hugo page bundle convention).
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)
getLang(),dict(),applyI18n(lang),setLang(lang)
toggleTheme()
saveState(),saveStateDebounced(),restoreState()
wrapSelection(),wrapBlock(),toggleLinePrefix(),makeOrderedList(),insertLink(),applyHeading(),insertFigure(),insertVideo()
lintFrontMatterMeta(),lintMarkdown(),validateShortcodes(),lintAll()
downloadMarkdown(),exportDoc()
convertInlineMedia(),escapeShortcodesEverywhere()
Questions or suggestions? Open an issue in the repository.