App::WRT - WRiting Tool, a static site/blog generator and related utilities


Using the commandline tools:

    $ mkdir project
    $ cd project
    $ wrt init         # set up some defaults
    $ wrt config       # dump configuration values
    $ wrt ls           # list entries
    $ wrt display new  # print HTML for new entries to stdout
    $ wrt render-all   # publish HTML to project/public/

Using App::WRT in library form:

    #!/usr/bin/env perl

    use App::WRT;
    my $w = App::WRT->new(
      entry_dir => 'archives',
      url_root  => '/',
      # etc.
    print $w->display(@ARGV);


It's possible this would run on a Perl as old as 5.14.0. In practice, I know that it works under 5.26.2. It should be fine on any reasonably modern Linux distribution, and might work on BSD of your choosing. Maybe even MacOS. It's possible that it would run under the Windows Subsystem for Linux, but it would definitely fail under vanilla Windows; it currently makes too many assumptions about things like directory path separators and filesystem semantics.

(Although I would like the code to be more robust across platforms, this is not a problem I feel much urgency about solving at the moment, since I'm pretty sure I am the only user of this software. Please let me know if I'm mistaken.)

To install the latest development version from the main repo:

    $ git clone
    $ cd wrt
    $ perl Build.PL
    $ ./Build installdeps
    $ ./Build test
    $ ./Build install

To install the latest version released on CPAN:

    $ cpanm App::WRT


    $ cpan -i App::WRT

You will likely need to use sudo or su to get a systemwide install.


This started life somewhere around 2001 as, a CGI script to concatenate fragments of handwritten HTML by date. It has since accumulated several of the usual weblog features (lightweight markup, feed generation, embedded Perl, poetry tools, image galleries, and ill-advised dependencies), but the basic idea hasn't changed that much.

The wrt utility now generates static HTML files, instead of expecting to run as a CGI script. This is a better idea, for the most part.

By default, entries are stored in a simple directory tree under entry_dir.



Which will publish files like so:


Contents will be generated for each year and for the entire collection of dated entries. Month indices will consist of all entries for that month. A top-level index file will consist of the most recent month's entries.

An entry may be either a plain UTF-8 text file, or a directory containing several such files. If it's a directory, a file named "index" will be treated as the text of the entry, and all other lower-case filenames without extensions will be treated as sub-entries or documents within that entry, and displayed accordingly. Links to certain other filetypes will be displayed as well.

Directories may be nested to an arbitrary depth, although it's probably not a good idea to go very deep with the current display logic.

A PNG or JPEG file with a name like


will be treated as an icon for the corresponding entry file.


Entries may consist of hand-written HTML (to be passed along without further mangling), a supported form of lightweight markup, or some combination thereof.

Header tags (<h1>, <h2>, etc.) will be used to display titles in feeds, navigation, and other places.

Other special markup is indicated by a variety of HTML-like container tags.

Embedded Perl - evaluated and replaced by whatever value you return (evaluated in a scalar context):

     <perl>my $dog = "Ralph."; return $dog;</perl>

This code is evaluated before any other processing is done, so you can return any other markup understood by the script and have it handled appropriately.

Interpolated variables - actually keys to the hash underlying the App::WRT object, for the moment:

     <perl>$self->{title} = "About Ralph, My Dog"; return '';</perl>

     <p>The title is <em>${title}</em>.</p>

This is likely to change at some point, so don't build anything too elaborate on it.

Embedded code and variables are intended only for use in the template file, where it's handy to drop in titles or conditionalize aspects of a layout. You want to be careful with this sort of thing - it's useful in small doses, but it's also a maintainability nightmare waiting to happen.

Includes - replaced by the contents of the enclosed file path, from the root of the current wrt project:


This is a bit constraining, since it doesn't currently allow for files outside of the current project, but is useful for including HTML generated by some external script in a page.

Several forms of lightweight markup:

     <markdown>John Gruber's Markdown, by way of

     <textile>Dean Allen's Textile, via Brad Choate's

     <freeverse>An easy way to
     get properly broken lines
     plus -- em dashes --
     for poetry and such.</freeverse>

And a couple of shortcuts:

     alt text, if any</image>

     one list item

     another list item

As it stands, freeverse, image, and list are not particularly robust. In practice, image and list have not proven all that useful, and may be deprecated in a future release.


A single template, specified by the template_dir and template config values, is used to render all pages. See example/templates/basic for an example, or run wrt init in an empty directory and look at templates/default.

Here's a short example:

    <!DOCTYPE html>
      <meta charset="UTF-8">
      <title>${title_prefix} - ${title}</title>



Within templates, ${foo} will be replaced with the corresponding configuration value. ${content} will always be set to the content of the current entry.


Configuration is read from a wrt.json in the directory where the wrt utility is invoked, or can (usually) be specified with the --config option.

See example/wrt.json for a sample configuration.

Under the hood, configuration is done by combining a hash called %default with values pulled out of the JSON file. Most defaults can be overwritten from the config file, but changing some would require writing Perl, since they contain things like subroutine references.


Here's a verbatim copy of %default, with some commentary about values.

    my %default = (
      root_dir       => '.',         # dir for wrt repository
      entry_dir      => 'archives',  # dir for entry files
      filter_dir     => 'filters',   # dir to contain filter scripts
      publish_dir    => 'public',    # dir to publish site to
      url_root       => "/",         # root URL for building links
      image_url_root => '',          # same for images
      template_dir   => 'templates', # dir for template files
      template       => 'default',   # template to use
      title          => '',          # current title (used in template)
      title_prefix   => '',          # a string to slap in front of titles
      stylesheet_url => undef,       # path to a CSS file (used in template)
      favicon_url    => undef,       # path to a favicon (used in template)
      feed_alias     => 'feed',      # what entry path should correspond to feed?
      feed_length    => 30,          # how many entries should there be in the feed?
      author         => undef,       # author name (used in template, feed)
      description    => undef,       # site description (used in template)
      content        => undef,       # place to stash content for templates
      default_entry  => 'new',       # what to display if no entry specified
      cache_includes => 0,           # should included files be cached in memory?

      # A license string for site content:
      license        => 'public domain',

      # A string value to replace all pages with (useful for occasional
      # situations where every page of a site should serve some other
      # content in-place, like Net Neutrality protest blackouts):
      overlay        => undef,

      # We'll show links for these, but not display them inline:
      binfile_expr   => qr/[.](tgz|zip|tar[.]gz|gz|txt|pdf)$/,

A hashref which contains a map of entry titles to entry descriptions.


A hashref which contains a cache of entry titles, populated by the renderer.


For no bigger than this thing is, the internals are convoluted. (This is because it's spaghetti code originally written in a now-archaic language by a teenager who didn't know how to program.)


Takes a filename to pull JSON config data out of, and returns a new App::WRT instance with the parameters set in that file.


Get a new WRT object with the specified parameters set.


Render each renderable path, cache the HTML, and parse to extract titles.


If there's any metadata, such as tagged relationships, for a given entry, populate an HTML blob for that stuff.

XXX: Here is where we put the list of pages for a given tag, but also maybe other things about a page or its properties. There should be a template / partial involved.

display($entry1, $entry2, ...)

Return a string containing the given entries, which are in the form of date/entry strings. If no parameters are given, default to default_entry.

display() expands aliases ("new" and "all", for example) as necessary, collects entry content and metadata from the pre-rendered HTML caches, and wraps everything up in the template.

If overlay is set, will return the value of overlay regardless of options. (This is useful for hackily replacing every page in a site with a single blob of HTML, for example if you're participating in some sort of blackout or something.)


Return the text of an individual entry:

  nnnn/[nn/nn/]doc_name - a document within a day.
  nnnn/nn/nn            - a specific day.
  nnnn/nn               - a month.
  nnnn                  - a year.
  doc_name              - a document in the root directory.

Expands/converts 'all', 'new', and 'fulltext' to appropriate values.

Removes trailing slashes.

Returns a little context-sensitive navigation bar.

Returns context-sensitive page navigation (next / previous links).


List out the updates for a year.


Prints the entries in a given month (nnnn/nn).

entry_stamped($entry, $level)

Wraps entry() + a datestamp in entry_markup().


Get tag links for the entry.


Returns the contents of a given entry. May recurse, slightly.


Returns the markup for an entry's body - which will be either the contents of the entry if it's a text file, or an index file contained therein if it's a directory.

Also handles any filters.

list_contents($entry, @entries)

Returns links (maybe with icons) for a set of sub-entries within an entry.


Returns a title for the entry - potentially a cached one extracted earlier from the entry's HTML; otherwise just reuse the entry path itself.

icon_markup($entry, $alt)

Check if an icon exists for a given entry if so, return markup to include it. Icons are PNG or JPEG image files following a specific naming convention:

  index.icon.[png|jp(e)g] for directories
  [filename].icon.[png|jp(e)g] for flat text files

Calls image_size, uses filename to determine type.


Returns a nice html datestamp / breadcrumbs for a given entry.


Given an entry, return the appropriate concatenations with entry_dir and url_root.


Print $count recent entries, falling back to the configured $feed_length.


Print $count recent entries in JSON, falling back to the configured $feed_length.


Return an Atom feed for the given list of entries.

Requires XML::Atom::SimpleFeed.

XML::Atom::SimpleFeed will give bogus results with input that's just a string of octets (I think) if it contains characters outside of US-ASCII. In order to spit out clean UTF-8 output, we need to use Encode::decode() to flag entry content as UTF-8 / represent it internally as a string of characters. There's a whole lot I don't really understand about how this is handled in Perl, and it may be a locus of bugs elsewhere in wrt, but for now I'm just dealing with it here.

Some references on that:


Like feed_print(), but for JSON Feed.

SEE ALSO, Blosxom, rassmalog, Text::Textile, XML::Atom::SimpleFeed, Image::Size, and about a gazillion static site generators.


Copyright 2001-2022 Brennen Bearnes


    wrt is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, version 2 or 3 of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <>.