Getting Started
hugOS turns a Hugo site into a small “web desktop”. Every section of your
content becomes an app; individual pages, images and links open in
draggable, resizable windows. There is no build step beyond Hugo and no
JavaScript framework - the whole desktop is plain HTML, CSS and one vanilla
JS file.
This Handbook is itself an example: it is the docs/ content section rendered
as a wiki app. Everything you are reading lives in content/docs/*.md.
The mental model
There are only three things to understand:
data/desktop.yaml describes the desktop - which apps exist, their
icons, and what each one opens.content/ holds your actual material - markdown pages, sections of
pages, and image bundles.- The theme wires the two together. An app in the YAML points at a piece of
content (or at a built-in tool like the terminal), and the theme renders the
icon, the launcher entry and the window.
If you can edit a YAML file and write markdown, you can run the whole thing
without touching any code.
A two-minute tour
- Double-click a desktop icon (or single-click on touch) to open its window.
- Drag a window by its title bar. Drag it to the top to maximize, or to
a side to tile it to half the screen.
- Resize from any edge or corner.
- Use the panel at the bottom: the launcher on the left, running windows in
the middle, the clock on the right (click it for a calendar).
- Right-click the desktop for a context menu.
- Open the Terminal and type
help.
Where to go next
- Installation & Setup - add the theme to a Hugo site.
- Configuring the Desktop - the
data/desktop.yaml reference. - Adding Content - where files go and how they appear.
- App Types Reference - every kind of app you can declare.
Tip: use the search box at the top of this Handbook to jump to any page.
Source
hugOS is open source and released into the public domain (The Unlicense). Browse
the code, file an issue, or fork it on
GitHub.
Installation & Setup
hugOS is a standard Hugo theme. Hugo Extended (v0.146.0 or newer) is
recommended. It is strictly required only if your photo galleries contain
raster images (JPG/PNG) - those get converted to WebP thumbnails, and WebP
encoding needs Extended. SVG-only galleries and everything else run on standard
Hugo just fine. (Check your build with hugo version; Extended shows
+extended in the string.)
1. Add the theme
Pick whichever method matches how you manage your site.
Hugo Modules (recommended)
hugo mod init github.com/you/your-site
# hugo.toml
[module]
[[module.imports]]
path = "github.com/paradox-ng/hugos-theme"
Git submodule
git submodule add https://github.com/paradox-ng/hugos-theme themes/hugos-theme
# hugo.toml
theme = "hugos-theme"
Manual copy
Drop the theme into themes/hugos-theme/ and set theme = "hugos-theme".
2. Minimal configuration
# hugo.toml
baseURL = "https://example.com/"
title = "My Desktop"
theme = "hugos-theme"
[params]
accent = "#3daee9" # desktop accent colour
[markup.goldmark.renderer]
unsafe = true # allow raw HTML in markdown content
unsafe = true is recommended. The theme’s documents render author-written
markdown, and several niceties (badges, inline HTML) rely on it. It does not
affect visitor input - this is a static site.
3. Add the desktop file
Create data/desktop.yaml. The Configuring the Desktop page documents every
field and includes a complete example you can copy and edit.
4. Run it
Open http://localhost:1313. Edits to content, data and templates live-reload.
Building for production
The finished static site is written to public/. Host it anywhere that serves
files - GitHub Pages, Netlify, Cloudflare Pages, an S3 bucket, your own server.
Project layout
my-site/
โโโ hugo.toml
โโโ data/
โ โโโ desktop.yaml # the whole desktop is described here
โโโ content/
โ โโโ about.md # a single page -> "page" app
โ โโโ blog/ # a section -> "folder" app
โ โ โโโ first-post.md
โ โ โโโ second-post.md
โ โโโ docs/ # a section -> "wiki" app (this Handbook)
โ โโโ photos/ # an image bundle -> "gallery" app
โ โโโ index.md
โ โโโ 01-photo.jpg
โโโ themes/hugos-theme/
Configuring the Desktop
Everything on the desktop is declared in data/desktop.yaml. There are three
top-level keys: profile, bookmarks and apps.
profile
Identity shown in the launcher header, the About/terminal whoami, and meta
tags.
profile:
name: "Paradox"
role: "Fullstack Developer"
initials: "P" # shown in the launcher avatar
email: "paradox@example.com"
about: "One-line bio for the launcher and terminal."
bookmarks
Tiles shown on the browser app’s home page.
bookmarks:
- { label: "GitHub", icon: "๐", url: "https://github.com/octocat" }
- { label: "Hugo", icon: "๐", url: "https://gohugo.io" }
apps
A list. Each entry becomes a desktop icon, a launcher entry, and a window.
Order in the list is the order of the icons.
apps:
- { id: about, label: "About Me", icon: "๐",
title: "About Me - Notes", type: page, source: "about", open: true }
Fields
| field | required | meaning |
|---|
id | yes | Unique key. Used for the window, the URL hash, and open <id> in the terminal. |
label | yes | Text under the desktop icon and in the launcher. |
icon | yes | An emoji or short string (e.g. >_). |
title | usually | The window title-bar text. |
type | yes | What kind of app - see App Types Reference. |
source | some | Which content the app opens (a page, section or bundle). Depends on type. |
url | some | For web shortcuts. |
open | no | true opens the window automatically on load. |
Auto-opening windows
Add open: true to any app and its window opens on load. Flag several and they
cascade; flag none for an empty desktop the visitor opens themselves.
- { id: about, type: page, source: "about", open: true }
- { id: blog, type: folder, source: "blog", open: true }
A complete example
profile:
name: "Paradox"
role: "Fullstack Developer"
initials: "P"
email: "paradox@example.com"
about: "Designer-developer who builds useful things for the web."
bookmarks:
- { label: "GitHub", icon: "๐", url: "https://github.com/octocat" }
apps:
- { id: about, label: "About Me", icon: "๐", title: "About Me - Notes", type: page, source: "about", open: true }
- { id: handbook, label: "Handbook", icon: "๐", title: "Handbook - Wiki", type: wiki, source: "docs" }
- { id: blog, label: "Blog", icon: "๐ฐ", title: "Blog - File Explorer", type: folder, source: "blog" }
- { id: photos, label: "Photos", icon: "๐ผ๏ธ", title: "Photos - Gallery", type: gallery, source: "photos" }
- { id: terminal, label: "Terminal", icon: ">_", title: "Terminal", type: terminal }
- { id: settings, label: "Settings", icon: "โ๏ธ", title: "System Settings", type: settings }
- { id: browser, label: "Browser", icon: "๐", title: "Browser", type: browser }
- { id: github, label: "GitHub", icon: "๐", type: web, url: "https://github.com/octocat" }
Deep links
Each window is deep-linkable. Opening https://example.com/#about opens the
About window on load. The URL hash updates as you focus windows, so links are
shareable.
App Types Reference
The type field on each app decides what it does. Types fall into two groups:
those that open your content, and built-in tools that need no content.
Content apps
type | Opens | Requires |
|---|
page | a single markdown page as a document | source: "<page>" |
folder | a content section as a file manager | source: "<section>" |
wiki | a content section as a sidebar wiki | source: "<section>" |
gallery | an image page-bundle as a photo gallery | source: "<bundle>" |
page
Opens one markdown file in a document window.
- { id: about, label: "About Me", icon: "๐",
title: "About Me - Notes", type: page, source: "about" }
source: "about" resolves to content/about.md.
folder
Renders a whole content section as a file-manager window. Each file is a tile;
clicking one opens it in its own document window. Files are listed newest-first
by date.
- { id: blog, label: "Blog", icon: "๐ฐ",
title: "Blog - File Explorer", type: folder, source: "blog" }
source: "blog" lists every page in content/blog/.
wiki
Like folder, but renders the section as a documentation wiki: a searchable
sidebar of page titles on the left, the selected page on the right. This
Handbook is a wiki. Order the pages with a weight in each file’s front
matter (lowest first).
- { id: handbook, label: "Handbook", icon: "๐",
title: "Handbook - Wiki", type: wiki, source: "docs" }
gallery
Renders an image page-bundle as a thumbnail grid with a lightbox. Raster images
(JPG/PNG/WebP) get auto-generated WebP thumbnails via Hugo image processing;
SVGs are used as-is.
- { id: photos, label: "Photos", icon: "๐ผ๏ธ",
title: "Photos - Gallery", type: gallery, source: "photos" }
See Photo Galleries for the folder layout.
These need no source.
type | What it is |
|---|
terminal | A small shell. Type help for commands. |
browser | A web browser. Frames embeddable sites, renders a live GitHub profile, falls back gracefully when a site blocks embedding. |
settings | Live accent colour, wallpaper, and dark / light theme (persisted per browser). |
calculator | A working calculator. |
sticky | Sticky notes you can write on; saved in the browser. |
sysmon | A mock system monitor with live-ish graphs. |
trash | A trash can. |
- { id: terminal, label: "Terminal", icon: ">_", title: "Terminal", type: terminal }
- { id: browser, label: "Browser", icon: "๐", title: "Browser", type: browser }
- { id: settings, label: "Settings", icon: "โ๏ธ", title: "Settings", type: settings }
- { id: calc, label: "Calc", icon: "๐งฎ", title: "Calculator", type: calculator }
- { id: notes, label: "Notes", icon: "๐๏ธ", title: "Sticky Notes", type: sticky }
- { id: monitor, label: "Monitor", icon: "๐", title: "System Monitor", type: sysmon }
- { id: trash, label: "Trash", icon: "๐๏ธ", title: "Trash", type: trash }
Shortcuts
type | What it is |
|---|
web | A desktop icon that opens a URL in the browser app. Needs url. |
- { id: github, label: "GitHub", icon: "๐", type: web, url: "https://github.com/octocat" }
A web shortcut needs a browser app present to open into.
Adding Content
Content lives in content/, exactly as in any Hugo site. The desktop file
decides which content shows up as which app; this page covers how to write the
content itself.
A single page
Create a markdown file and point a page app at it.
<!-- content/about.md -->
---
title: "About Me"
---
# Hi, I'm Paradox
Your content hereโฆ
# data/desktop.yaml
- { id: about, label: "About Me", icon: "๐",
title: "About Me - Notes", type: page, source: "about" }
The document window renders whatever the markdown produces, so the leading
# Heading is your page title.
A section (folder or wiki)
A section is a folder of pages under content/. Add files to it and they
appear automatically - no code or config changes per file.
content/blog/
โโโ first-post.md
โโโ shipping-small.md
โโโ design-systems.md
- { id: blog, label: "Blog", icon: "๐ฐ",
title: "Blog - File Explorer", type: folder, source: "blog" }
Each post is a normal Hugo page:
---
title: "Shipping small"
date: 2024-04-02
tags: ["process", "engineering"]
---
Body textโฆ
Front matter that matters
| field | used for |
|---|
title | The file/tile name and the window title. |
date | Sort order in folder apps (newest first) and the shown date. |
tags | Shown next to the file in folder apps. |
weight | Sort order in wiki apps (lowest first). |
draft | true hides the page from normal builds. |
Ordering
folder apps sort by date, newest first.wiki apps sort by weight, lowest first - give each page a weight to
control the sidebar order (this Handbook uses 10, 20, 30, โฆ).
Writing markdown
Standard Hugo / Goldmark markdown is supported - headings, lists, tables, code
fences with highlighting, blockquotes, images, links. With
markup.goldmark.renderer.unsafe = true (see Installation & Setup) you can
also drop in raw HTML. The Markdown Showcase page shows how everything
renders inside a window.
Drafts and dates
- A page with
draft: true is skipped unless you run hugo server -D. - A page with a future
date is skipped unless you run hugo server -F.
Photo Galleries
A gallery app renders an image page-bundle as a thumbnail grid with a
lightbox.
Folder layout
A page bundle is a folder with an index.md and the images beside it:
content/photos/
โโโ index.md
โโโ 01-aurora.jpg
โโโ 02-dunes.jpg
โโโ 03-forest.svg
<!-- content/photos/index.md -->
---
title: "Photos"
---
# data/desktop.yaml
- { id: photos, label: "Photos", icon: "๐ผ๏ธ",
title: "Photos - Gallery", type: gallery, source: "photos" }
How images are handled
- Raster images (JPG, PNG, WebP) are resized to fit a 600ร600 box and
encoded as WebP thumbnails, loaded lazily. The lightbox shows the full
original. (WebP encoding needs Hugo Extended - see Installation & Setup.)
- SVGs are used as-is for both thumbnail and full view.
Captions
Captions are derived automatically from the file name: numeric prefixes and
separators are stripped and the rest is title-cased. So 01-blue_hour.jpg
becomes “Blue Hour”. Name your files descriptively and you get captions for
free.
Ordering
Images are shown in file-name order, which is why the demo prefixes them
01-, 02-, 03-. Use zero-padded numeric prefixes to control the sequence.
Multiple galleries
Each gallery is its own bundle and its own app. Add another folder and another
app entry:
content/travel/
โโโ index.md
โโโ 01-lisbon.jpg
- { id: travel, label: "Travel", icon: "โ๏ธ",
title: "Travel - Gallery", type: gallery, source: "travel" }
Customization & Settings
There are two layers of customization: defaults you set in config, and
choices visitors make in the Settings app (saved in their browser).
Defaults in config
# hugo.toml
[params]
accent = "#3daee9" # desktop accent colour
The accent colour drives highlights, focus rings, the launcher hover, and links.
Pick something that reads well on both the dark and light themes.
The Settings app
Add a settings app and visitors get a live control panel:
- { id: settings, label: "Settings", icon: "โ๏ธ",
title: "System Settings", type: settings }
From it they can change, live:
- Accent colour - any colour.
- Theme - dark or light.
- Wallpaper - pick from the built-in set.
Persistence
Every visitor choice is stored in their browser’s localStorage, so the desktop
looks the same when they come back. Nothing is sent to a server - this is a
static site. Keys used: theme, accent, wallpaper, and (if they’ve played with
it) the retro era. Clearing site data resets everything to your config defaults.
Profile and branding
The launcher header, the terminal whoami, and page metadata read from
profile in data/desktop.yaml:
profile:
name: "Paradox"
role: "Fullstack Developer"
initials: "P"
email: "paradox@example.com"
about: "One-line bio."
Going further
The desktop’s look lives in themes/hugos-theme/static/css/desktop.css, built almost
entirely on CSS custom properties (--accent, --panel-h, window colours, and
so on). If you want to fork the visual design, that file is the place - overriding
a handful of variables changes the whole feel without touching the JavaScript.
SEO & No-JS Pages
The desktop is a single page powered by JavaScript - but the content is not
trapped inside it. Every piece of content also exists as a plain, crawlable
Hugo page.
Why this matters
- Search engines index the real pages, not a blank
<div>. - No-JS visitors (and tools, readers, archivers) still get the content.
- Deep links to
/blog/some-post/ work and render a readable page.
How it works
Alongside the desktop, the theme renders the normal Hugo page tree:
single.html for individual pages and postslist.html for sections- standard meta tags, Open Graph, Twitter cards, canonical URLs, and an RSS link
These plain pages are intentionally minimal - they exist so the content is
accessible and indexable. The desktop experience is layered on top for people
with JavaScript.
Page-level front matter feeds the meta pack:
---
title: "Shipping small"
description: "Why I keep changes small and frequent."
date: 2024-04-02
---
description becomes the meta description and the Open Graph / Twitter summary.
Without it, the theme falls back to the site or page defaults.
Sitemap and RSS
Hugo generates sitemap.xml and section RSS feeds automatically. Submit the
sitemap to search consoles as you would for any Hugo site.
A note on unsafe
markup.goldmark.renderer.unsafe = true only affects how your own
author-written markdown is rendered (it allows raw HTML). Because this is a
static site with no user input, it carries none of the risk that the name might
suggest.
Easter Eggs
hugOS hides a few things for the curious. None of them affect your content or
the plain, indexable pages - they are pure decoration. Here is the full list, so
nothing stays a secret if you don’t want it to.
Time travel
Click the clock in the panel, then the ๐ฐ button in the calendar (or use the
desktop’s context menu). Pick a year and the whole desktop re-skins itself:
- 1995 - a Windows 95 style: grey bevels, the classic title bars, the pixel
font, even the boot splash.
- 2001 - a Windows XP style: the blue Luna taskbar, the green Start button,
Bliss-like wallpaper.
- 2009 - a Windows 7 / Aero style: glass taskbar and title bars, the waving
flag wallpaper.
Each jump plays an era-appropriate boot splash, and the choice is remembered
until you travel back to the present.
Era surprises
- 2001 brings back a familiar paperclip assistant in the corner. Click
it for tips; close it with the ร.
- 2009 greets you with a “Configuring Windows updatesโฆ” screen the first
time you arrive. Do not turn off your computer.
Crash screens
Press Ctrl + Alt + B for a crash screen appropriate to the current era:
- modern desktop โ a Linux kernel panic dump,
- 1995 โ a Windows 9x blue screen,
- 2001 / 2009 โ a Windows XP / 7 STOP error.
Press any key or click to recover. (Some game icons may “crash” on purpose too.)
The terminal
The Terminal app is a small Unix-like shell. Type help to see every
command - there are a couple of playful ones in there alongside the useful
open, ls, and neofetch.
Turning them off
The easter eggs live entirely in the theme’s JavaScript and CSS. If a site of
yours wants none of them, they can be removed from
themes/hugos-theme/static/js/desktop.js without affecting the content apps.
Markdown showcase
A quick taste of how rich content renders inside a document window.
Text
You get bold, italic, strikethrough, and inline code. Links look
like this.
A list
- First
- Second
- Third
A quote
Simplicity is the ultimate sophistication.
Code
function greet(name) {
return `Hello, ${name}!`;
}
A table
| Feature | Status |
|---|
| Windows | โ
|
| Terminal | โ
|
| Browser | โ
|