static site generator powered by Podlite markup
Built with it: podlite.org, which is this project's own site, axona.app, and the Raku knowledge base, which publishes 2274 documents.
Run it from the directory holding your index.podlite:
docker run -it --rm -v ${PWD}:/app/pub -p 3000:3000 \
podlite/podlite-web dev ./pub --preset everythingOpen http://localhost:3000.
Both the content path and --preset are required. See Presets. The index is looked up as index.podlite, then index.pod6; documents may carry either extension.
Content is processed once, when the server starts. Editing a document means restarting the container; the dev server watches the code, not the content.
A site can be one file:
=begin pod
= :puburl<https://example.com>
=TITLE My site
=head1 Hello
Written in Podlite, published as HTML.
=end pod
docker run --rm -v ${PWD}:/app/pub podlite/podlite-web \
export-zip ./pub -s 'https://example.com' --preset everything > site.zipexport-tgz writes a gzipped tarball instead; export leaves the site in /app/out.
A preset decides which documents become pages.
everythingβ every document is published. Use it while writing, and for sites that are not a blog.pubdateβ only documents carrying a publication date, and only once that date has passed. This is what a blog wants.
There is no default: a run without --preset stops with --preset undefined not valide.
# minimal site
yarn dev examples/01-minimal --preset everything
# multi-page with links
yarn dev examples/02-pages --preset everything
# blog with notes and React components
yarn dev examples/03-blog --preset pubdate- custom domain:
-s https://example.com, orSITE_URLin the environment; the flag wins - timezone:
TZ=Europe/London - index file other than the one found by default:
-i path/to/index.podlite - file mask:
-g '**/*.{podlite,pod6}'
cd examples/01-minimal
docker run --rm -v ${PWD}:/app/pub \
-e 'TZ=Europe/London' \
-e 'SITE_URL=https://example.com' \
podlite/podlite-web export-zip ./pub --preset everything > site.zipA site can list the repositories its content comes from. The build clones them before it reads the files. The list goes into podlite-web.config.js in the content directory:
module.exports = {
mounts: [
{ name: 'spec', repo: 'https://github.com/podlite/podlite-specs', ref: 'main' },
{ name: 'spec-v1.0', repo: 'https://github.com/podlite/podlite-specs', ref: 'v1.0' },
{ name: 'spec-v3.0', repo: 'https://github.com/podlite/podlite-specs', ref: 'release/v3.0', required: false },
{ name: 'skills', repo: 'https://github.com/podlite/podlite-skills', ref: 'main', prefix: '/skills' },
],
}| field | meaning |
|---|---|
name |
the name used elsewhere in the config, and the directory the checkout goes into |
repo |
the repository to clone |
ref |
the branch or tag to build from. For a repository you do not own, use a tag: a branch can change at any time |
required |
true by default. If a required source cannot be prepared, the build stops. With required: false the source is skipped and the build prints a warning |
prefix |
the part of the site this source answers at. Without it, addresses come from the documents |
Checkouts go into .mounts/ next to the build, not into the content directory. You build the same site on your own machine, and files from another repository should not end up among your own. If a checkout is already there, the build asks git ls-remote for the commit and clones again only if it differs, so a second local build costs almost nothing. git has to be on the PATH.
If a required source gives the build no files at all, the build stops. If an optional one does, the build prints a warning and goes on. The first lines of output list what was mounted: name, ref, commit, and whether it came from the network or from the cache.
Two flags belong to this step:
--offlinebuilds from the checkouts already on disk and does not use the network.--mounts-onlyprepares the sources and stops. A build server can use it to see which commits the refs point to before it decides to build.
Several mounts of the same document can be published as versions of it:
versions: {
default: 'v2.0',
entries: [
{ prefix: 'v1.0', mount: 'spec-v1.0', label: 'v1.0' },
{ prefix: 'v2.0', mount: 'spec', label: 'v2.0' },
{ prefix: 'v3.0', mount: 'spec-v3.0', label: 'v3.0' },
],
},List the entries oldest first. Everything after default is not released yet. Everything before it is superseded.
The current version answers at the address written in its own documents, and once more under its prefix. The copy under the prefix has a canonical link to the first address and is not indexed. A version that is not released yet gets noindex and stays out of the sitemap. A superseded version keeps its address in both, but stays out of the search on the site. The list of versions is passed to the site as data, so the site draws the version picker itself.
A mount used as a version cannot also have a prefix. Two rules for one address would disagree, so the build stops if you set both.
A site can draw its own page, and keep its React components next to the content without a package of its own.
Declare the template on the root block of index.podlite:
=begin pod
= :templateFile<src/template.podlite>
=end pod
The template renders the whole page, so anything it declares sits above the title. It reaches the page body through the shared component:
=useReact {DefaultTemplateComponent} from '@Components/service'
Components of your own live beside the content and are imported by path:
=useReact SiteNav from './components/sitenav'
The import map is keyed by path, so declare each component once: the same name reached through two paths collides. A path is resolved when it starts with ./ or /.
A theme bundles header-image styling and layout defaults. Select one with the :theme<> attribute on =begin pod:
=begin pod
= :theme<portrait-avatar>
= :puburl<https://example.com>
=TITLE My Blog
=end pod
The themes live in src/styles/themes/:
- portrait-avatar β round, grayscale header image; for sites with an author photo
- product β for emblem or logo headers; sets
#header img { max-width: 51% } - docs β placeholder for documentation sites
- minimal β no overrides; the default look
Theme styles load first, then :globalStyles<> (if set) overrides them.
@import "@Styles/themes/<name>.css" from page.styles.css keeps working; :theme<> is the same effect declared on the pod.
# install dependencies
yarn
# run dev server against a content directory
yarn dev examples/01-minimal --preset everything
# export to zip
yarn export-zip examples/01-minimal -s 'https://example.com' --preset everything > file.zipA local run attaches the content directory: it adds the directory to workspaces in package.json and writes an alias into next.config.js. Restore both when you are done:
yarn detach_path- turns a folder of Podlite and Markdown documents into a static site
- gives each page a fixed address from
:puburl, or one built from its publication date - holds a post back until its publication date has passed, under the
pubdatepreset - writes
sitemap.xml,rss.xmlandrobots.txt, and builds a search index - embeds the Podlite editor in a page, with the preview beside it
- renders
=Mermaiddiagrams,=pictureimages and video,=toccontents,=markdownblocks - takes a page template and React components kept next to the content, with no package of their own
The demo site: a post with its table of contents, and the editor embedded in a page.
Copyright (c) 2022β2026 Alexandr Zahatski
Released under a MIT License.

