Documentation

Custom Edition

People — Custom Edition A brutalist, editorial Ghost theme built on top of the People Themes base. Bold uppercase type, thick black borders, and a homepage that is your navigation. This

KALESERENA

People — Custom Edition

A brutalist, editorial Ghost theme built on top of the People Themes base. Bold uppercase type, thick black borders, and a homepage that is your navigation. This is a heavily customized fork — this document describes what's actually in this package, not the generic base theme.


Key Features

  • Homepage-as-index — the homepage has no post feed. It's a locked, full-screen list of big bordered rows, one per item in your Ghost Navigation menu.
  • Sticky post layout — article pages use a two-column layout: bordered title block on the left, cover image + auto-generated Table of Contents sticky on the right, article body below.
  • Native CSS title truncation — long post titles on the Project index are truncated with a plain text-overflow: ellipsis. No JavaScript, no measurement delay, no possibility of a flash — the browser truncates it instantly as part of normal rendering, on the very first paint.
  • Auto-built Table of Contents — generated automatically from every H2/H3 heading in a post. No manual setup.
  • Cursor preview — on Project and Authors list pages, hovering a row shows a floating thumbnail (feature image or author photo) that follows the mouse. Square with a thick border; automatically disabled on touch devices.
  • Auto-sectioned Pages — on standard Pages (About, Careers, etc.), every H2/H3 and the content under it is automatically wrapped in its own bordered box. The opening paragraph before the first heading gets a border too. This logic runs as an inline script directly in page.hbs, immediately after the content — not as a separate deferred script file, which could never run early enough. Content is hidden very briefly (with a tight 150ms failsafe, not the long wait an early version of this used) so the reflow from "plain" to "boxed" is never visible.
  • Sticky sidebar on Pages — a Page's feature image (if set) plus a "Quick Facts" card (author, published date, reading time) sit in a sticky right column, mirroring the article layout.
  • Tag ticker — an infinite scrolling marquee of every tag with published posts, shown above the Project index.
  • Authors & Tags directories — dedicated index templates styled like the homepage row list.
  • Instant page navigation — no animated transitions between pages. This theme previously used the browser's View Transitions API for cross-fade/morph effects, but it caused a series of hard-to-fully-resolve bugs (flashing on refresh, scroll position jumping, Safari incompatibilities) and was removed entirely by request. Navigating the site is now a plain, standard browser navigation — less flashy, but fully reliable.
  • Single, fixed color scheme — this theme is intentionally black-on-white only. A dark mode / color-scheme toggle was built, tested, and then removed entirely by request. There is no dark mode, no color scheme setting, and no toggle anywhere in this theme's code.
  • Mobile menu with dropdown groups — supports the same # / - / + / % prefix syntax as the base theme for building submenus in the hamburger menu (see Navigation).

Installation

  1. In Ghost Admin, go to Settings → Design and branding → Change theme.
  2. Click Upload theme and select this theme's .zip file.
  3. Once it validates, click Activate.
If the upload is rejected, the most common cause is the zip containing a wrapper folder instead of the theme files sitting at the top level.

Quick Start Checklist

  1. Set your logo and accent color under Settings → Design and branding → Brand.
  2. Build your Navigation menu — every item becomes a homepage row. See Navigation below before adding items.
  3. Create your directory pages: a Page with slug authors and a Page with slug tags (see Page Templates).
  4. For any other static Page (About, Contact, etc.), just write content with the Ghost editor — headings will auto-box themselves.
  5. Leave every post's Template set to Default unless you specifically want the old, unstyled layout (see Post Template Warning).

Theme Settings

Found under Settings → Design and branding → Brand and theme.

This custom build has no theme-specific settings. The site uses a single, fixed black-on-white color scheme by design.

An earlier version of this theme included a full Light/Dark/Auto color scheme with a manual toggle button. That feature has since been removed completely — the color_scheme setting, the toggle button, its JavaScript, and every related CSS rule (including the dark-mode variable system) were all deleted, not just disabled. There's nothing dark-mode-related left in the codebase to re-enable; it would need to be rebuilt from scratch.

Accent Color, Publication Icon, Logo, and Cover Image are all still visible in Ghost Admin (Ghost always shows these — they're core settings, not theme-specific), but only some of them do anything in this theme:

  • Logo — works. Upload one and it replaces the text site title in the header.
  • Publication Icon (favicon) — works automatically, handled by Ghost core ({{ghost_head}}), not by theme code.
  • Accent Color — intentionally unused, by design. This theme is pure black-and-white; no element references the accent color variable.
  • Cover Image — not used anywhere. The homepage is a locked, full-screen navigation list with no space reserved for a cover banner.

Everything else in this theme (layout, borders, spacing, ticker speed, etc.) is controlled by CSS, not by a theme-settings panel — there's no columns/grid/pagination-style picker in this build, unlike the base People theme.


Set up navigation under Settings → Navigation. This is the most important setting in this theme, because the homepage is literally your navigation menu.

How it works

  • Every item you add under Settings → Navigation → Primary Navigation becomes one big bordered row on the homepage, in the order you set.
  • Label → the row's headline text (shown in bold, uppercase).
  • URL → where the row links to. This can be:
    • A Page you created (e.g. /about/)
    • A Tag archive (e.g. /tag/news/) — see Troubleshooting if posts don't show up
    • An external link

Mobile dropdown groups

The hamburger menu on mobile still supports the base theme's prefix system for grouping items into a dropdown, using the Label field:

Prefix Meaning
# Starts a dropdown group (with a link)
+ Starts a dropdown group (heading only, not clickable)
- A submenu item nested under the nearest #/+ group above it
% Starts a "More" (⋮) style group
@ / $ Submenu item nested under a % group

Important caveat: the homepage row list does not understand these prefixes — it just prints every navigation item as a flat row, prefix character included. If you use #, +, -, %, @, or $ to build a mobile dropdown, those items will still show up as their own literal rows on the homepage (e.g. a row labeled + Pages). If you want a clean homepage, keep your Primary Navigation flat and build any dropdown grouping in a secondary navigation menu instead (Settings → Navigation → Secondary Navigation, rendered in the footer).


Page Templates

Ghost auto-selects a template based on a Page's URL slug. Create a Page, set its slug to match, and leave the content as needed:

Slug Template Notes
authors Authors directory Lists every author as a bordered row with photo-preview on hover
tags Tags directory Lists every tag with a post count
(anything else) Standard page Auto-sections headings into bordered boxes; sticky sidebar with feature image + Quick Facts

For a standard Page:

  • Add a feature image in the Ghost editor to populate the sticky sidebar image.
  • Every H2/H3 you add in the content, plus the text below it, is automatically wrapped in its own bordered box — you don't need to do anything special, just use headings normally.
  • The paragraph before your first heading is treated as the intro and gets its own border too.

Post Template Warning

This theme ships with several legacy custom-*.hbs files (custom-wide, custom-hero, custom-cover, custom-small, custom-banner, custom-background, custom-fullwidth, custom-tags) inherited from the base People theme. These still use the original, unmodified base-theme layout — they were never updated to match this theme's bordered/sticky design.

They're still selectable in Ghost Admin under a post's Settings → Template dropdown. If you select one, that post will render in a completely different, unstyled layout.

Leave every post's Template set to "Default" unless you intentionally want that old look for a specific post.


Post Page Features

Every article automatically gets:

  • A bordered title block with an eyebrow tag, excerpt, author, and date.
  • A sticky right-hand column containing the feature image (if set) and an auto-built Table of Contents.
  • The Table of Contents is generated from every H2/H3 in your content — no manual setup, no shortcode.
  • On mobile, the sidebar un-stickies and stacks above the article body, and the article body's border is removed for easier reading on small screens.

Comments: this theme does not currently render Ghost's native comments widget on post pages, even if Comments are enabled under Settings → Membership → Comments. If you need comments, this needs to be added to post.hbs first.


Troubleshooting / FAQ

A tag row shows no posts

This almost always comes down to one of:

  1. The post isn't Published (drafts never appear on public tag pages).
  2. The tag's URL slug (Settings → Tags → your tag) doesn't exactly match the URL you set in Navigation. If Ghost auto-renamed your tag slug (e.g. to news-2 because news was already taken), update either the slug or the Navigation URL so they match.
  3. The tag name starts with # — that marks it as an internal tag, which Ghost hides from all public pages by design.

My homepage row shows a +, #, -, %, @, or $ character in the title

You used the mobile-dropdown prefix syntax in the Label field. The homepage doesn't strip these — see Navigation above.

A specific post looks completely different from the rest of my site

Check that post's Settings → Template in Ghost Admin — it's probably set to one of the legacy custom-* templates. Set it back to Default. See Post Template Warning.

The hover preview thumbnail doesn't reappear after using the browser Back button

This was an issue in much earlier builds tied to the View Transitions feature, which has since been removed from this theme entirely. The hover preview now works normally with the browser's Back/Forward buttons in every case. If you're on an old exported copy of this theme, re-download the latest version.

How do I keep certain tags out of the ticker and Tags directory?

Useful for tags that exist purely to power a Navigation row (like news or terms) that you don't want mixed into the general tag collection.

Both content.hbs (the ticker) and page-tags.hbs (the Tags directory) query tags with a filter. Add the slug you want excluded inside the slug:-[...] list, e.g.:

filter="count.posts:>0+slug:-[news,terms]"

Add as many slugs as you need, comma-separated, inside the brackets. This only hides the tag from those two listings — the tag itself, and every post tagged with it, keep working normally everywhere else (including any Navigation row pointing straight at it).

How do I add a Post that doesn't show up in the Project list?

Useful for a "Documentation" or "Style Guide" post you only want reachable via a Navigation link, not mixed into your Project index.

  1. In Ghost Admin, open the post and add an internal tag named #hidden (the leading # is what makes it internal — Ghost hides internal tags from all public tag pages automatically).
  2. That's it — the Project index (content.hbs) is filtered to exclude any post carrying the #hidden tag. The post itself still works normally: it's still published, still reachable at its own URL, and still works as a Navigation link target.
  3. Reuse the same #hidden tag on any other post you want excluded the same way.

Updating the Theme

Navigation items, tags, and pages all live in Ghost's database, not in the theme files — re-uploading a new .zip of this theme won't erase them. If you or anyone else edits the theme's code directly (as this custom build has been), back up those edits before uploading a replacement zip, since a fresh upload will overwrite the code files.


A Note on Script Loading Order

A script that needs to run as close as possible to when its target content becomes available should generally be inline, positioned immediately after that content — not a separate deferred external file. A deferred script file can never run before the entire document finishes parsing, no matter how it polls internally, since that's gated on full-document-parse by definition. That's why the auto-sectioning logic for Pages lives as an inline script directly in page.hbs, right after .people-page-body — it runs the moment the browser has parsed that far. The content is hidden for a very brief, tightly-capped window (150ms) while this runs — long enough that the script virtually always finishes naturally within it regardless of page length, short enough that the wait itself is never noticeable, unlike an earlier version of this that used a much longer (0.8s) failsafe.


A Note on Scroll Restoration

Refreshing the page while scrolled down used to visibly "bounce" — jump to the top for a moment, then snap back down. That was the browser's own automatic scroll restoration, not anything in this theme's CSS/JS: it paints the fresh page at the top, then restores the previous scroll position afterward, and that gap was visible.

This theme now takes manual control instead (history.scrollRestoration = "manual", set in default.hbs, paired with assets/js/scroll-restore.js continuously saving position to sessionStorage). On reload, the page is hidden (visibility: hidden) and polled every animation frame, restoring the scroll position and revealing the page the instant it's grown tall enough to actually reach it — rather than waiting for the entire document to finish parsing, which kept the page hidden longer than it needed to be. A 500ms failsafe guarantees the page is never stuck hidden if something goes wrong.

Restoration only happens for an actual reload or a Back/Forward navigation (checked via performance.getEntriesByType("navigation")) and only when there's a meaningfully non-zero saved position (≥10px). Refreshing while already at the top of the page, or a fresh visit — clicking a Navigation row or any link into a page — skips the hide/restore/reveal cycle entirely and just renders normally, with no flash at all.


Full-bleed elements (bordered title bars that span edge-to-edge, like every index page's H1) use margin-left: calc(50% - var(--viewport-w) / 2) to break out of the page's normal padding. --viewport-w is set by a small script in default.hbs's <head>.

This used to also re-check the width on a delayed timer (300ms and 1000ms after load) as a safety net for scrollbar-width edge cases. That was removed: re-measuring that late — after the person may already be looking at or scrolled into the page — could shift the value by a few pixels, which was occasionally just enough to make a large title rewrap onto a different number of lines and visibly shove the rest of the page down, then back up. This happened identically across every index page (About, Project, Tags, Authors, etc.) since they all share this same mechanism. The remaining checks (on initial load, DOMContentLoaded, load, and resize) cover every case that actually matters without that late-shift risk.


A Note on the Table of Contents

The Table of Contents is built from a post's H2/H3 headings by an inline script directly in post.hbs, immediately after the content — not a separate deferred script file. A deferred script can never run before the entire document finishes parsing, which meant the TOC box always popped into the sidebar noticeably late; on a post with many headings (a long documentation-style post, say) the box is large enough that this was clearly visible. Running inline, right after the content it reads headings from, means it runs the moment the browser has parsed that far — the earliest it possibly could. It also measures the height the box adds and corrects the scroll position by exactly that amount if the page was already scrolled past that point, so nothing visibly shifts either.

For a genuinely long post (thousands of words), the box still can't appear until all that content has been parsed — there's no way around needing the full heading list first. Rather than fight that unavoidable wait further, the box fades in gently (.people-toc's people-toc-appear animation) instead of popping in at full opacity — so however long the wait was, its arrival reads as a natural appearance rather than a flicker.


A Note on Cover Image Layout Shift

Both .people-post-detail__cover (Posts) and .people-page-cover (Pages) reserve a fixed aspect-ratio (with object-fit: cover) instead of leaving height as auto. Without that, the browser doesn't know an image's height until the file itself finishes downloading — so the space it occupies "pops in" late and shoves the TOC/Quick Facts card below it down. How long that takes (and so how noticeable the shove is) depends entirely on network speed, which is why it could show up as sometimes fast, sometimes slow, but never fully gone. Reserving the space immediately, before the image starts loading, removes it regardless of connection speed.


A Note on Shared Row Styling (Homepage, Project, Authors, Tags)

Every big bordered row in this theme — homepage nav rows, Project/Authors/Tags list rows — is built from the same four classes: .people-post-row, .people-post-link, .people-post-link__label, .people-post-link__arrow. The homepage previously re-declared several properties (border-bottom, font-family, font-weight, line-height, min-width) with values that were meant to be — and always were — identical to the shared base rule. That duplication is exactly how the row-height and centering bugs below kept slipping past one context while being fixed in another: a fix applied to the base rule had no effect on the homepage's separate copy of the same declaration, and vice versa.

The homepage-specific rules (.people-home-nav .people-post-row, .people-home-nav .people-post-link, etc.) now only declare what's genuinely different about that layout — its locked-to-one-screen sizing (flex / min-height) and its much larger, wrap-allowed label typography. Everything else (border, margin, the base flex centering, hover states, color) is intentionally left undeclared there, so it's inherited from the single shared rule instead of duplicated. A future fix to spacing, centering, or borders on one of these shared properties now only needs to happen in one place to apply everywhere.

A Note on Row Height Consistency (Homepage, Project, Authors, Tags)

The real root cause, confirmed via a live DevTools Computed-panel comparison: the base theme has a generic typography reset meant for ordinary prose lists in blog post content —

li+li {
    margin-top: .5em
}

Because li+li is an adjacent-sibling selector, it only matches an <li> that's immediately preceded by another <li>. In every row list this theme builds (.people-post-list), the first row is preceded by an <h1> title, not a sibling row — so it never matched this rule. Every row after it, though, is preceded by another row, and silently picked up an extra 0.5em (8px, at this list's inherited 16px font-size) of top margin that nothing else had. That's what made the first row look "correctly" sized and every row after it look inconsistent — on the homepage, Project, Authors, and Tags alike, since they all share this same list markup.

The fix is a single explicit margin: 0; on .people-post-row, which — being a class selector — already has enough specificity to override the generic two-type-selector li+li rule for good.

Two secondary issues were found and fixed along the way, before this root cause was pinned down, and are both still worth keeping:

  • The homepage's locked, full-screen row layout (.people-home-nav) positions itself below the header using a header-height offset that used to be hardcoded (64px) — wrong the moment Ghost's Announcement Bar (if enabled) makes the header taller. assets/js/header-height.js measures the header's actual rendered height with a ResizeObserver and keeps a --gh-head-h CSS variable in sync, so row positioning stays accurate regardless of header height.
  • .people-post-link__label has transform: translateY(-0.045em). This bold display font's line-height box reserves more space above the cap-height than below the baseline (space meant for accents/descenders this all-caps text never uses), so flexbox's align-items: center — which centers the line box, not the visible glyphs — makes text look like it's sitting slightly low, with more empty space above it than below. This was removed once, based on the incorrect assumption it was only masking the two bugs above — but direct pixel measurement on a page unaffected by either of them (the Authors index) still showed a consistent ~9px top/bottom imbalance across every row, confirming it's a real, separate issue.

.gh-head-logo.no-image (the text-only logo fallback, used whenever no image logo is uploaded) used to carry an arbitrary margin-top: -5px nudge. .gh-head-brand, its parent, is already a flex container with align-items: center — that nudge fought against the centering flexbox was already doing correctly on its own, making the text logo sit visibly out of line with its siblings (search icon, Sign in, Subscribe). Removing it lets flexbox center everything consistently.


A Note on the Arrow Icon Size

.people-post-link__arrow (and its SVG) used to be sized purely with font-size/1em, scaling with whatever font-size the parent happened to have. On the homepage, that was 17.4rem (174px) — so even hidden behind opacity: 0 before hover, the icon still occupied up to 174px of flex layout height, which could throw off how tall a row's content gets calculated, and so its centering. It's now capped at a fixed 48px regardless of context, removing every font-size-based override that existed only to scale it (several are now dead code and were removed).


Credits

  • Base theme: People, by People Themes (peoplethemes.com), MIT licensed.
  • This edition: customized on top of that base — bordered/brutalist layout, homepage-as-navigation, cursor preview, auto-TOC, auto-sectioned pages, and tag ticker are custom additions not present in the original theme.
  • See THIRD-PARTY-LICENSES.md for third-party library licenses included in this package.

© People Themes (base theme). Custom modifications are unlicensed/unpublished — for personal use on this site only unless you decide otherwise.