Sytes

Sytes is a small Common Lisp library for building simple websites. I wouldn't call it a framework—it doesn't deal with any of those things you'd expect frameworks to do, like handling authentication, database abstraction etc. Sytes just implements a template engine with a Lispy syntax and a Hunchentoot dispatcher that maps URLs to templates. Sytes is the PHP of Common Lisp (I hope this doesn't sound like an insult). If you're a Lisper, you might enjoy it.

Template syntax

The templates are HTML (we're not limited to HTML, but that's the common use case) intermixed with Lisp expressions. Sytes implements a parser which transforms your template into S-expressions which are then compiled into Lisp closures. The language is not Common Lisp; it's instead much simpler (think Scheme). Templates are compiled only once, then the main closure is stored and just executed whenever a request comes in. Templates are re-compiled only if the file modification time changed since the cache time.

Here is an example:

;; a semicolon in the first column starts a comment
;; and it's discarded from the output
<h1>{page.title}</h1>

;; show links
<ul class="links">
  {(foreach link '(("http://foo.com" . "The Foo")
                   ("http://bar.com" . "The Bar")
                   ("http://baz.com" . "The Baz"))
     (let ((url (car link))
           (name (cdr link)))
       {<li><a href="{\url}">{\name}</a></li>}))}
</ul>

Essentially, the bracket characters are special, in that they commute the parser mode. But in those cases where you'd rather use brackets for something else (such is the case for this page, where I want to type brackets literally to show you examples) you can customize it using a special directive.

You can read more about the syntax.

The filesystem structure

Templates are stored on disk in a directory structure that resembles the URL shape. Each Syte defines a root directory where templates are stored. Then, for example an URL like "http://.../foo/bar/baz" is mapped to one of the following files, depending on how they are found on disk:

Autohandlers / dhandlers

This is an idea I borrowed from Perl's HTML::Mason module. Autohandlers are a facility to ease including headers/footers in a template. For example in a language like PHP, you would need to include manually header/footer in each template—something I find unpleasant.

Before executing a template, the directory where it resides and any parent directories up to the Syte ROOT are searched for a file named “autohandler.syt”. If that file is found it is executed instead of the template, and inside that file a special function (“call-next”) may be used to execute the actual template. Autohandlers themselves are subject to the same treatment (if a parent autohandler exists, it is executed instead), allowing you to cascade changes to the output into the parent directories; if you want, it's a simple inheritance mechanism.

For an example, here is the main autohandler of this very website:

{[define content (call-next)]}~
<!DOCTYPE html>
<html>
  <head>
    <title>{\(or (@attr 'TITLE) "Lisperator.net — to create, to ilisperate")}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0" />

    <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22256%22 height=%22256%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%232e418a%22></rect><path fill=%22%23fff%22 d=%22M38.32 20.02L38.32 20.02Q42.15 20.02 44.49 21.58Q46.84 23.14 48.13 26.86L48.13 26.86L64.80 70.18Q65.59 72.17 66.46 72.93Q67.34 73.69 68.71 73.69L68.71 73.69L70.20 73.57L70.43 79.51Q69.02 79.98 67.38 79.98L67.38 79.98Q64.38 79.98 62.85 79.26Q61.33 78.54 60.12 76.86Q58.91 75.18 57.77 72.05L57.77 72.05L48.79 48.89L37.62 79.32L29.57 79.32L44.96 39.12L41.21 29.94Q40.31 27.75 39.28 26.86Q38.24 25.96 36.48 25.96L36.48 25.96L34.65 26.04L34.61 20.49Q36.25 20.02 38.32 20.02Z%22></path></svg>" />

    <link href="/atom" type="application/atom+xml" rel="alternate" title="Mishoo's blog feed" />

    <link rel="stylesheet" type="text/css" href="{static-file-link "dl/new-theme/default.css"}" />
    <link rel="stylesheet" type="text/css" href="{static-file-link "css/x.css"}" />
    <link rel="stylesheet" type="text/css" href="{static-file-link "css/icons.css"}" />
    <link rel="stylesheet" type="text/css" href="{static-file-link "css/ymacs.css"}" />

    <link rel="stylesheet" media="print" type="text/css"
          href="{static-file-link "css/style-light.css"}" />

    <link rel="stylesheet" type="text/css" href="{static-file-link "css/common.css"}" />
    <link class="theme-css" name="light" rel="stylesheet"
          media="screen" type="text/css" href="{static-file-link "css/style-light.css"}" />
    <link class="theme-css" name="dark" rel="stylesheet"
          media="screen" type="text/css" href="{static-file-link "css/style-dark.css"}" />

    {(if-logged-in {<script>LOGGED_IN=true</script>})}

    {(foreach style (reverse (@attr 'STYLES))
       {<link rel="stylesheet" type="text/css" href="{\style}" />})}

    {.SYNTAX "«" "»"}
    <script type="module">
     (function(LS){
       let useDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
       let theme = LS && LS.getItem("colorTheme") || (useDark ? "dark" : "light");
       [...document.querySelectorAll(".theme-css")].forEach(s =>
         s.disabled = s.getAttribute("name") != theme
       );
       document.documentElement.classList.add("theme-" + theme);
     })(window.localStorage);
    </script>
    «.SYNTAX "{" "}"»

    <script src="{static-file-link "dl/js/thelib.js"}"></script>
    <script src="{static-file-link "dl/extras/sha1.js"}"></script>
    <script src="{static-file-link "dl/extras/md5.js"}"></script>

    {if NOT-IN-PRODUCTION {
      <script src="{static-file-link "x/x.js"}"></script>
      <script src="{static-file-link "x/x-datetime.js"}"></script>
      <script src="{static-file-link "x/x-dialog.js"}"></script>
      <script src="{static-file-link "x/x-messages.js"}"></script>
      <script src="{static-file-link "x/x-genericform.js"}"></script>
      <script src="{static-file-link "x/x-confirmdlg.js"}"></script>
      <script src="{static-file-link "x/x-html5upload.js"}"></script>

      <script src="{static-file-link "coloroloc/js/coloroloc.js"}"></script>
      <script src="{static-file-link "coloroloc/js/lang-lisp.js"}"></script>
      <script src="{static-file-link "coloroloc/js/lang-xml.js"}"></script>
      <script src="{static-file-link "coloroloc/js/lang-syt.js"}"></script>
      <script src="{static-file-link "coloroloc/js/lang-js.js"}"></script>
      <script src="{static-file-link "coloroloc/js/lang-css.js"}"></script>
      <script src="{static-file-link "coloroloc/js/lang-html.js"}"></script>
      <script src="{static-file-link "coloroloc/js/lang-lambda.js"}"></script>
      <script src="{static-file-link "js/zepto.min.js"}"></script>

      <script src="{static-file-link "js/init.js"}"></script>
      <script type="module" src="{static-file-link "js/editor.js"}"></script>
      <script src="{static-file-link "js/main.js"}"></script>
    }{
      <script src="{static-file-link "js/zepto.min.js"}"></script>
      <script src="{static-file-link "everything.js"}"></script>
      <script type="module" src="{static-file-link "js/editor.js"}"></script>
    }}

    {(foreach script (reverse (@attr 'SCRIPTS))
       {<script src="{\script}"></script>})}

  </head>
  <body>
    <div class="header" id="L-header">
      <canvas class="background" id="bazon-fractal"></canvas>
      <a class="logo" href="/">
        Lisperator.net
        <span class="tagline">You are iLisperated!</span>
      </a>
    </div>
    {(let ((path (@attr 'PATH))) {
       <div class="breadcrumb" id="L-breadcrumb">
         <div class="main-links">
           {(page-link "./blog/index.syt" "Blog")}
           {(page-link "./ymacs/index.syt" "Ymacs")}
           {(page-link "./pltut/index.syt" "PL Tutorial")}
           {(page-link "./uglifyjs/index.syt" "UglifyJS")}
           {(page-link "./slip/index.syt" "SLip")}
           {(page-link "./sytes/index.syt" "Sytes")}
         </div>
         <div class="breadcrumb-links">
           <div class="color-prefs">
             <a href="javascript:toggleColorTheme()"></a>
           </div>
           {(page-link "/" "Home")}
           {(foreach i (reverse path)
              (let ((name (car i))
                    (url (cadr i)))
                { <span>▶</span> {(page-link url name)}}))}
         </div>
       </div>})}
    {when (@attr 'SUBCONTENT) { <button class="toggle-toc si si-menu"></button> }}
    <div class="content"><div class="l-empty"></div>
      <div class="body" id="L-body">{content}</div>
      {let ((sc (@attr 'SUBCONTENT))) {
        <div class="toc" id="L-toc">
          <div class="prefs">
            {if-logged-in
              {Welcome, {\user.name}! (<a href="/@logout">logout</a>)
                <br /><a href="javascript:new_blog_post()">New blog post</a>
                <br /><a href="javascript:edit_last_page()">Edit last page</a>
                {(foreach op (@attr 'OPERATIONS)
                  {<br />{op}})}}}
          </div>
          {when sc {
            <div class="toc-content">
              {reverse sc}
            </div>
          }}
        </div>
      }}
    <div class="r-empty"></div></div>
    <div class="footer">
      © <a href="https://github.com/mishoo">Mihai Bazon</a> 2012 - 2024<br />
      Proudly NOT powered by WordPress.<br />
      {(page-link "./about-this-site.syt" "Humbly powered by Common Lisp")}.<br /><br />

      Views expressed here are my own and do not represent the
      opinions of any entity that I was, am or will be involved with.
    </div>
    <div id="x-desktop"></div>
    <div id="TIME-SPENT" onclick="new LoginDialog().show(true)"></div>
    {(awhen (@attr 'FORK-ME-ON-GITHUB)
      {<div class="fork-me-on-github"><a href="{\it}"><span class="text">Fork me on Github</span></a></div>})}
  </body>
  <script>ITS_ALIVE_HAHAHA()</script>
</html>
Fork me on Github