Bookstacks is an Astro static site for reading public domain literature at https://bookstacks.org.
The library is built from one TEI XML file per book. A custom content-collection
loader parses every file in tei-source/ at build time and generates the author
pages, book tables of contents (with character and place registries), and
individual chapter reading pages.
tei-source/<author-slug>_<title-slug>_<lang>.xml: the TEI source for a book (one per language, e.g.austen-jane_pride-and-prejudice_en.xml). For parallel-text works, the source is split by language (e.g.,tolstoy-leo_war-and-peace_ru.xml,tolstoy-leo_war-and-peace_en.xml); the parser will auto-discover and merge files that share the same<author-slug>_<title-slug>prefix at build time.src/utils/teiParser.ts: parses a TEI file into{ title, author, persons, places, chapters }and converts each chapter's TEI body to reading HTML.src/content.config.ts: defines theauthorscontent collection. A custom loader reads everytei-source/*.xmland emits synthetic entries — oneauthor_index, onebook_index(carrying the person/place registries), and onebook(chapter) entry per chapter.src/pages/index.astro: the home page. Lists every author as a card and reports author / book / chapter counts.src/pages/authors/[...slug].astro: one dynamic route that renders every author index, book index (table of contents + registries), and chapter reading page. It branches on thelayoutvalue of the entry.src/layouts/BaseLayout.astro: the shared page shell — header, site network, footer, Material Icons, and the inline theme / accent-color scripts.src/styles/global.css: all theme, index, table-of-contents, registry, and reader styles.public/: files copied verbatim to the site root at build time, includingCNAME(custom domain) andimg/authors/<slug>.pngportraits..github/workflows/deploy.yml: builds the site and deploys it to GitHub Pages on every push tomain.
Most books are stored as TEI documents under tei-source/, named by their author
slug, title slug, and language (e.g. austen-jane_pride-and-prejudice_en.xml for Pride
and Prejudice). Books with multiple parallel translations are split into multiple
language-specific files that share the same <author-slug>_<title-slug> prefix (e.g.
tolstoy-leo_war-and-peace_ru.xml, tolstoy-leo_war-and-peace_en.xml,
tolstoy-leo_war-and-peace_es.xml for War and Peace). A document has:
- A
teiHeaderwith thetitle,author(<persName>), source credit (<sourceDesc>), and the registries:<particDesc><listPerson>for characters and<settingDesc><listPlace>for places. Each<person>/<place>has anxml:id, a name, and a short<note>. - A
<text><body>containing one<div type="chapter" n="N">per chapter, each with a<head>(the chapter title) followed by the chapter content.
Slugs are derived automatically: the author slug is surname-given
(austen-jane, tolstoy-leo); the book slug is the slugified title
(pride-and-prejudice, war-and-peace). The chapter n attribute becomes the
chapter order and must be unique within a book.
The chapter body is semantically encoded, not a transliteration of any edition's presentational markup. The parser understands:
<p>paragraphs,<emph>emphasis,<title>work titles (rendered as<cite>),<foreign xml:lang="…">foreign phrases.<said who="#id" direct="true">direct speech, attributed to a person in the registry.<persName ref="#id">,<placeName ref="#id">, and<rs ref="#id">references, which resolve to registry entries and render as hover-tooltips in the reader.<pb n="…"/>page-break milestones; verse via<lg>/<l>; correspondence via<floatingText>,<opener>,<closer>,<salute>,<signed>,<dateline>.
- Create
tei-source/<author-slug>_<title-slug>_<lang>.xml(one per language; all language files for a book share the same<author-slug>_<title-slug>prefix). - Write the
teiHeader(title, author, source credit, and the person/place registries). - Add
<div type="chapter" n="N">blocks under<text><body>, each with a<head>and the chapter content, using the reading markup above. - Optionally add a portrait at
public/img/authors/<author-slug>.png(see below).
That's it — the loader and the dynamic route pick the book up automatically. There are no per-book index files to maintain.
Portraits are optional. If public/img/authors/<author-slug>.png exists, the home
page and author page show it; otherwise a fallback icon is used.
Portraits are duotone tiles (a colored ground plus dark ink) so they render correctly in both light and dark themes without inversion, and so each author can have a distinct color. To make one from a grayscale engraving:
magick <engraving>.png -resize 500x500 -colorspace Gray \
+level-colors "#333333","<author-color>" \
public/img/authors/<author-slug>.png#333333 is the ink; the second color is that author's ground (e.g. coral for
Austen).
Requires Node.js (18.20.8+, 20.3+, or 22+).
npm install # install dependencies
npm run dev # start the dev server
npm run build # type-check (astro check) then build into dist/
npm run preview # preview the built outputPushing to main triggers .github/workflows/deploy.yml, which builds the site
and publishes dist/ to GitHub Pages. The repository's Pages source must be set to
GitHub Actions. The custom domain is preserved by public/CNAME.
Works hosted here are intended to be public domain in the United States. Users outside the United States should verify the copyright status in their own jurisdiction.