This guide is for people who want to change Attegi's source: fix a bug, add a feature, or maintain a fork. If you only want to adjust colors or fonts, Customizing Attegi shows how to do that without a build step.

🧭
The Attegi handbook, part five. You should already be comfortable with Git and a terminal. No Ghost internals knowledge is required.

What you need

  • Bun 1.3 or newer. It installs dependencies, runs the build, and serves the preview.
  • Git, and a fork of bunizao/Attegi.
  • A Ghost site to borrow content from. Any Ghost 5.118+ or Ghost 6 site works, including this demo.

Five commands

git clone https://github.com/<you>/Attegi.git
cd Attegi
bun install
cp .env.example .env.local   # then fill in the two values
bun run dev:preview          # http://localhost:3020

dev:preview is the loop you will use most. It renders Attegi's Handlebars templates locally, fills them with real posts fetched from your Ghost site through the Content API, and reloads the browser whenever you save a template, stylesheet, or script.

To get the two values for .env.local, open Ghost Admin → Settings → Integrations → Add custom integration. Copy the Content API key and the API URL:

GHOST_CONTENT_API_URL=https://your-site.example
GHOST_CONTENT_API_KEY=your-content-api-key
.env.local

Three ways to run it

Command What it does Use it when
bun run dev:preview Local template renderer + remote content Almost always. Fastest loop for templates, CSS, and JS.
bun run dev:hot Proxies a real Ghost site and injects your local assets You need Ghost's exact output: members, comments, search.
bun run dev Only watches and rebuilds CSS and JS You run your own Ghost locally with the theme symlinked.

The preview renderer is an approximation. Portal, comments, search, and Ghost's image resizing are stubbed out. When something only breaks on a real Ghost, switch to dev:hot (set GHOST_DEV_URL to the site you are proxying).

Where things live

Attegi/
├── default.hbs          # page shell: <head>, header, footer
├── index.hbs  post.hbs  page.hbs  tag.hbs  author.hbs
├── page-tags.hbs        # layout for the page with URL /tags/
├── page-links.hbs       # layout for the page with URL /links/
├── partials/            # shared template pieces and inline SVG icons
├── locales/             # interface translations, one JSON per language
├── src/
│   ├── sass/            # style.scss + color, font, TOC and poem modules
│   └── js/
│       ├── entries/     # site.js, post.js, page.js
│       ├── features/    # one file per feature, each exports init*()
│       └── core/        # tiny helpers: qs, qsa, onReady, throttle
├── scripts/             # build, preview server, deploy, version bump
└── package.json         # also defines the Ghost Admin theme settings
Repository layout

Two rules keep the repository sane:

  1. Edit sources, never build output. assets/css/style.css and assets/js/*.js are generated and ignored by Git.
  2. No frameworks at runtime. JavaScript is plain ES modules bundled by esbuild. No jQuery, no React.

Common tasks

Change a color

Colors are CSS custom properties in src/sass/_colors.scss. Each one has a light value and a dark value. If you change one, change both, then check the page in both modes.

Add a theme setting

Theme settings live in package.json under config.custom. Add an entry, then read it in a template as @custom.your_key. The preview renderer reads the default value from the same place, so it works locally right away.

"show_reading_time": {
  "type": "boolean",
  "default": true,
  "group": "post"
}
package.json

Add a translated string

Wrap the text with {{t "Your text"}} in a template, then add the key to the files in locales/. A missing translation falls back to the English key, so nothing breaks while a language catches up.

Add a JavaScript feature

Create src/js/features/your-feature.js that exports an initYourFeature() function, and call it from the right entry in src/js/entries/. post.js only loads on posts, page.js on pages, and site.js everywhere.

Before you open a pull request

bun run validate

This builds production CSS and JS, packages dist/attegi.zip, and runs GScan, Ghost's official theme checker. A pull request that fails GScan cannot be released, and CI runs the same check on every push.

Then look at your change the way readers will:

  • Light and dark mode.
  • Widths of 420, 480, 640, and 960 pixels. Those are the theme's breakpoints.
  • An English post and a Chinese post. Line breaks and punctuation behave differently.

Commit messages follow Conventional Commits with a scope, for example fix(toc): keep active heading in view.

Shipping a release

Releases are cut by the maintainer:

./scripts/bump-version.sh minor
git commit -am "chore(release): bump version to x.y.z"
git tag vx.y.z && git push --follow-tags

Pushing the tag makes GitHub Actions build the theme, run GScan, and attach attegi.zip to a new GitHub release. That zip is what everyone downloads.