Development
Local development server, build tools, and troubleshooting.
Local development server
tools/lazysite-server.pl runs the full processor on a non-privileged port.
No Apache configuration required.
perl tools/lazysite-server.pl
Options:
--port PORT Port to listen on (default: 8080)
--docroot PATH Document root (default: ../starter)
--processor PATH Processor path (default: ../lazysite-processor.pl)
--extensions DIR Also look for extensions in DIR (repeatable; see below)
--cache Respect cache files (default: always regenerate)
--auto-index Generate an index + breadcrumb nav for a tree with no
index.md (writes nothing to the docroot)
--no-seed Never seed scaffolding into the docroot
--help Show help
With no arguments, the server serves the starter/ directory on port 8080.
To serve your own site:
perl tools/lazysite-server.pl --docroot /path/to/public_html
The server always regenerates pages by default (equivalent to
LAZYSITE_NOCACHE=1) so edits are visible immediately without deleting
cache files.
Extensions kept outside the tree
Run from the tree, the server finds extensions in the plugins/ directory beside
the docroot. An extension kept elsewhere (an app's own, in its own repository)
is named with --extensions, once for each directory:
perl tools/lazysite-server.pl --docroot /path/to/public_html \
--extensions /work/crm/extensions --extensions /work/shop/extensions
- The directories are searched in the order given, then the docroot's
plugins/. A name found in two of them is served from the first, and the startup lines say so (shared.pl is in A and B: A wins). - Each must be a real directory: one that is itself a symlink stops the server
with a message. A symlinked script inside is skipped, as in
plugins/, and named at startup. - The option exists in the dev server only. An installed engine finds extensions
where it always did; nothing set in its environment widens that. The server
also puts the engine's
lib/onPERL5LIBfor the scripts it runs, so an extension outside the tree finds the engine's modules.
Browse an arbitrary tree of Markdown
The dev server doubles as a zero-install Markdown browser: point it at any tree
and add --auto-index.
perl tools/lazysite-server.pl --docroot /path/to/tree --auto-index
In this mode it:
- generates a directory index (sub-folders and pages as links, page labels from
each note's front-matter
title, the directory'sREADMElinked as an overview) for any directory that has noindex.md; - injects a breadcrumb nav (
index / area / page) at the top of every rendered page, so notes link back up without anav.conf; - writes nothing into the tree - scaffolding seeding is suppressed, and the
processor's compile/layout cache is relocated to a temporary directory
(via
LAZYSITE_CACHE_DIR) instead of{docroot}/lazysite/cache.
Scaffolding (auth/forms/conf seed files) is otherwise only ever written into a
real lazysite docroot - one that already has a lazysite/ directory or a
lazysite.conf.example. Use --no-seed to force it off even there.
LAZYSITE_NOCACHE
Set LAZYSITE_NOCACHE=1 to bypass the cache read path when running the
processor manually:
LAZYSITE_NOCACHE=1 REDIRECT_URL=/about \
DOCUMENT_ROOT=/path/to/public_html \
perl cgi-bin/lazysite-processor.pl
This forces the page to regenerate on every run. Note that cache files are
still written - LAZYSITE_NOCACHE skips reading the cache but not writing it.
Running the processor manually
The most direct way to diagnose a page error:
REDIRECT_URL=/about \
DOCUMENT_ROOT=/home/username/web/example.com/public_html \
perl /home/username/web/example.com/cgi-bin/lazysite-processor.pl
Prints full HTML output or Perl errors to the terminal. Adjust
REDIRECT_URL to the failing page path.
Override the default lazysite.conf path for testing:
LAZYSITE_CONF=/path/to/alt.conf \
REDIRECT_URL=/page \
DOCUMENT_ROOT=/path/to/public_html \
perl cgi-bin/lazysite-processor.pl
Or via command-line argument:
perl cgi-bin/lazysite-processor.pl --conf /path/to/alt.conf
Static site generation
Pre-render all pages for static hosting:
# Build in-place
bash tools/build-static.sh https://example.com
# Build to a separate output directory
bash tools/build-static.sh https://example.com ./dist
Deploy the output to GitHub Pages, Netlify, Cloudflare Pages, or any plain web server.
Link audit
plugins/audit.pl scans the docroot and reports orphaned pages
(source files with no inbound links) and broken links:
perl plugins/audit.pl /home/username/web/example.com/public_html
Pass --exclude to omit specific pages from the orphan report:
perl plugins/audit.pl --exclude changelog,contributing /path/to/docroot
Cache management
Local .md pages regenerate automatically when the .md file is newer
than the cached .html. Editing and saving is sufficient - no manual step
needed.
Remote .url pages use TTL-based invalidation (default 1 hour). The stale
cache is served immediately; the refetch happens after TTL expiry.
To force regeneration:
rm public_html/about.html
find public_html -name "*.html" -delete
Whether the homepage cache (index.html) is served by the web server or by
the processor depends on the vhost template:
The plain Hestia
lazysitetemplate (lazysite.tpl/lazysite.stpl) hasDirectoryIndex index.html index.htm. Onceindex.htmlexists,/is served from it directly and the processor is not asked, so an edit toindex.mddoes not show until the cache is deleted:rm public_html/index.html
The nginx and Apache templates and the Hestia
lazysite-cgi,lazysite-fcgiandlazysite-apptemplates leaveindex.htmlout of the index list (index.htm index.shtml), and the one-rule templates have none./with anindex.mdreaches the processor, which serves the cache and re-renders it after an edit. Anindex.htmlwith noindex.mdbeside it is served as a static home.
Cache housekeeping
Content type sidecar files (.ct) in lazysite/cache/ct/ are
managed automatically - created when a page is cached, deleted when
the corresponding .html is deleted.
To clear all cached state manually:
find public_html/lazysite/cache -delete
mkdir -p public_html/lazysite/cache
This removes both the layout cache and content type cache.
Troubleshooting
Check the error log
tail -50 /home/username/web/example.com/logs/example.com.error.log
End of script output before headers- The script crashed before printing anything. Run the processor manually to see the Perl error.
lazysite: Cannot write cache file ... Fix with: chmod g+ws-
The web server cannot write the generated
.htmlto the docroot. Pages render correctly but are not cached. Fix with:
chown ispadmin:www-data /home/username/web/example.com/public_html
chmod g+ws /home/username/web/example.com/public_html
Index page serves blank or stale content
On the plain Hestia lazysite template, Apache serves index.html
directly via DirectoryIndex, bypassing the processor. After editing
index.md, delete the cache:
rm public_html/index.html
The other shipped templates hand / to the processor, so this does not
arise on them.
A page caches as an empty file
Usually caused by a layout template error. The processor falls back to
minimal HTML rather than crashing, but check the error log for layout
warnings. Delete the empty .html file to force regeneration after
fixing the template.
Registries not generating
Registries only generate when a page is rendered - not on cached serves. If missing, delete the registry file and force a page render:
rm public_html/llms.txt
rm public_html/index.html
curl -s https://example.com/ > /dev/null
Syntax check the processor
perl -c cgi-bin/lazysite-processor.pl
Logging
Log level and format are set in lazysite.conf:
log_level: INFO # ERROR, WARN, INFO, DEBUG
log_format: text # text or json
Override at startup with environment variables:
LAZYSITE_LOG_LEVEL=DEBUG perl tools/lazysite-server.pl ...
LAZYSITE_LOG_FORMAT=json perl tools/lazysite-server.pl ...
Each script declares a $LOG_COMPONENT identifier and calls
log_event($level, $context, $message, %extra). Logs go to
lazysite/logs/COMPONENT.log when that directory is writable,
otherwise stderr.
Rsync deployment
Standard command for syncing local development to a deployed site while preserving runtime state:
rsync -av --delete \
--exclude='.git' \
--exclude='test-site' \
--exclude='starter/lazysite/auth/' \
--exclude='starter/lazysite/forms/contact.conf' \
--exclude='starter/lazysite/forms/handlers.conf' \
--exclude='starter/lazysite/forms/smtp.conf' \
--exclude='starter/lazysite/lazysite.conf' \
--exclude='starter/lazysite/nav.conf' \
--exclude='starter/lazysite/cache/' \
--exclude='starter/lazysite/logs/' \
/home/user/lazysite/ /srv/projects/lazysite/
Runtime state files (user credentials, form config, site config, nav, cache, logs) are excluded so local site data survives the sync.
TTL and browser caching
Pages with ttl: set in front matter use
Cache-Control: public, max-age=N. When combined with Vary: Cookie
(sent on all responses), proxy caches will create separate entries per
cookie value. For public pages with ttl:, this is generally fine
since anonymous visitors share the same cache entry. For pages visible
to both authenticated and anonymous users, consider whether TTL caching
is appropriate.