Skip to content

Latest commit

 

History

History
984 lines (789 loc) · 40.8 KB

doc.org

File metadata and controls

984 lines (789 loc) · 40.8 KB

Как сделан сайт rigidus.ru

О проекте

Я делал этот сайт, тренируясь в литературном программировании. Литературное программирование (Literate Programming) - методология программирования и документирования, в которой программа состоит из прозы на естественном языке вперемежку с макроподстановками и кодом на языках программирования.

Он содержит в себе весь код, который обеспечивает работу сайта и сделан максимально простым, чтобы служить наглядным введением в literate programming с использованием lisp и набора инструментов, реализованного в emacs: org-mode, babel и других.

Документ ниже является литературным исходником сайта http://rigidus.ru.

Сам сайт был изначально создан для сбора редких и интересных материалов о вычислительных моделях и языках программирования. Позже он также превратился в занимательное упражнение в реализации разнообразных программистких концепций.

Исходный код открыт по лицензии GNU v.3. Данные, в том числе руководства и книги, могут иметь свои лицензии.

Репозиторий с исходным кодом и историей коммитов доступен на https://github.com/rigidus/rigidus.ru

Вы можете делать любые дополнения и предложения в форме pull-requests и issue.

Установка и настройка

Этот сайт работает внутри образа Common Lisp под управлением веб-сервера hunchentoot. В качестве высокоуровневой библиотеки используется RESTAS, которую написал Андрей Москвитин (archimag).

Веб-сервер, библиотеку и все необходимые зависимости лучше всего установить при помощи менеджера библиотек Quicklisp.

Чтобы просто запустить сайт и попробовать его в работе, пройдите раздел “Легкий старт”. Все что идет дальше потребуется вам чтобы обеспечить инструментарий для литературного программирования.

Легкий старт

Установите git - систему управления версиями, если она еще не установлена:

sudo apt-get install git

Клонируйте репозиторий проекта:

mkdir -p ~/repo
cd ~/repo
git clone git@github.com:rigidus/rigidus.ru.git

Установите sbcl - реализацию Common Lisp

sudo apt-get install sbcl

Установите quicklisp - менеджер библиотек для Common Lisp

mkdir -p ~/build
cd ~/build
wget https://beta.quicklisp.org/quicklisp.lisp
sbcl --load quicklisp.lisp

Теперь мы внутри quicklisp-а, работающего в образе sbcl. Попросим его добавить себя в инициализационный файл, чтобы quicklisp загружался каждый раз, когда стартует sbcl

(ql:add-to-init-file)

Выйдите из лиспа:

(quit)

Откройте файл ~~/.sbclrc~ и добавьте в конец файла следующие строки, чтобы quicklisp знал, где находится репозиторий с сайтом:

#+quicklisp
(mapcar #'(lambda (x)
            (pushnew x ql:*local-project-directories*))
        (list #P"~/src/rigidus.ru/"))

Снова запустите sbcl

sbcl

И в нем загрузите сайт:

(ql:quickload "rigidus")

Наберите в адресной строке броузера http://localhost:9993 и загрузите главную страницу.

Инструменты разработки

Чтобы настроить инструменты emacs для поддержки литературного программирования совершите следующие действия:

Установите emacs, если он еще не установлен.

apt-get install emacs23

Добавьте в файл конфигурации /.emacs.d/init.el следующие строки, при необходимости изменив пути к папкам:

(defun org-custom-link-img-follow (path)
  (org-open-file-with-emacs
   (format "../img/%s" path)))

(defun org-custom-link-img-export (path desc format)
  (cond
    ((eq format 'html)
     (format "<img src=\"/img/%s\" alt=\"%s\"/>" path desc))))

(org-add-link-type "img" 'org-custom-link-img-follow 'org-custom-link-img-export)

(setq org-export-time-stamp-file nil)
(setq org-publish-project-alist
      '(("org-notes"
         :base-directory "~/src/rigidus.ru/org/"
         :base-extension "org"
         :publishing-directory "~/src/rigidus.ru/www/"
         :recursive t
         :publishing-function org-html-publish-to-html
         :timestamp nil
         :html-doctype "html5"
         :section-numbers nil
         :html-postamble nil
         :html-preamble nil
         :with-timestamps nil
         :timestamp nil
         :with-date nil
         :html-head-extra "<link href=\"/css/style.css\" rel=\"stylesheet\" type=\"text/css\" />"
         :html-head-include-default-style nil
         :html-head-include-scripts nil)
        ("org-static"
         :base-directory "~/src/rigidus.ru/org/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|djvu"
         :publishing-directory "~/src/rigidus.ru/www/"
         :recursive t
         :publishing-function org-publish-attachment)
        ("org"
         :components ("org-notes" "org-static"))))

Если вы желаете заняться написанием кода на лиспе для этого проекта - установите slime с официального сайта или используя quicklisp и сконфигурируйте его в файле конфигурации emacs /.emacs.d/init.el поправив путь к slime:

(setq inferior-lisp-program "sbcl")
(setq slime-lisp-implementations '((sbcl ("sbcl"))))
(setq slime-startup-animation nil)
;; SLIME
(add-to-list 'load-path "~/quicklisp/dists/quicklisp/software/path/to/slime")
(require 'slime)

Теперь вы готовы писать лисп-код в литературном стиле.

Как это работает

Мне нравится работать в emacs и использовать orgmode для формирования структурированных документов,

Orgmode включает в себя систему публикации, которая хорошо конфигурируется. Обычно я просто выполняю из емакса команду org-publish-all. Емакс осуществляет экспорт всех .org-файлов проекта в .html, в процессе выполняя директивы в них, такие как INCLUDE. Настройки экспорта задаются в конфигурации, результат попадает в папку ./www/

Тем не менее, мне всегда хотелось большей гибкости, поэтому я решил взять тот результат, который она производит, построить из него дерево s-выражений и применить все преобразования, которые мне могут понадобиться. После этого, преобразованный результат может быть снова транслирован в html/css/javascript и отображен на сайте.

Для того чтобы разбирать HTML-код в LHTML я использую библиотеку cl-html-parse. Переносимые пути обеспечиваются механизмом трансляции логических путей.

Вебсервер, запущенный на порту 9993, имеет несколько маршрутов, некоторые из которых связаны с файлами из этой папки. Соответствующий файл преобразовывется и отдается пользователю.

Сборка

Файл определения системы

Файл определения системы представляет собой каркас проекта и содержит в себе определение системы:

  • библиотеки, от которых зависит система
  • набор всех файлов, который должны быть загружены в лисп-процесс.

Определение системы экпортируется из литературного исходника в корневой каталог проекта.

;;;; <<copyright>>
(asdf:defsystem #:rigidus
  :version      "0.0.3"
  :author       "rigidus <i.am.rigidus@gmail.com>"
  :licence      "AGPLv3"
  :description  "site http://rigidus.ru"
  :depends-on   (#:anaphora
                 #:closer-mop
                 #:cl-ppcre
                 #:cl-base64
                 #:cl-json
                 #:cl-html5-parser
                 #:cl-who
                 #:cl-fad
                 #:optima
                 #:closure-template
                 #:drakma
                 #:restas
                 #:restas-directory-publisher
                 #:split-sequence
                 #:postmodern
                 #:restas
                 #:optima
                 #:fare-quasiquote-extras
                 #:fare-quasiquote-optima)
  :serial       t
  :components   ((:module "src"
                          :serial t
                          :pathname "src"
                          :components ((:static-file "templates.htm")
                                       (:file "prepare")
                                       (:file "defmodule")
                                       (:file "html")
                                       (:file "ext-html")
                                       (:file "orgmode")
                                       (:file "render")
                                       (:file "routes")
                                       (:file "init")
                                       (:static-file "daemon.conf")
                                       (:static-file "daemon.lisp")
                                       (:static-file "daemon.sh")))))

Подготовка к запуску

Этот файл компилирует шаблоны и создает пакет TPL. Он делает это еще до объявления базового пакета. Для того чтобы в процессе загрузки все ссылки на этот пакет были правильно разрешены, необходимо, чтобы создание пакета завершилось к моменту появления ссылок на него. А для этого нужно помещать компиляцию в отдельный файл.

Однако тогда у нас возникает проблема, заключающаяся в том, что base-dir, путь, от которого отсчитываются все пути придется объявлять дважды - вне пакета и внутри него. Мы решаем эту проблему средствами подстановки литературного программирования:

(merge-pathnames
 (make-pathname :directory '(:relative "src/rigidus.ru"))
 (user-homedir-pathname))
;;;; <<copyright>>

(closure-template:compile-template
 :common-lisp-backend (merge-pathnames
                       (make-pathname :name "templates" :type "htm")
                       (merge-pathnames
                        (make-pathname :directory '(:relative "src"))
                        <<base_dir>>)))

Определение пакетов

Что такое пакет и зачем он нужен лучше всего прочитать тут. Обычно определение пакетов экспортируется в файл src/package.lisp, но этот проект слишком простой, он содержит всего один пакет. Поэтому определение пакета происходит в разделе Определение модуля

Утилиты

Несколько маленьких утилитарных функций определены здесь. При экспорте они подключатся в тот же файл, где происходит определение модуля. Это функции:

  • отладочного вывода и ошибок
  • получения содержимого директории
  • трансформации дерева, в которое разбирается html из файла
(in-package :rigidus)

(defmacro bprint (var)
  `(subseq (with-output-to-string (*standard-output*)
             (pprint ,var)) 1))

(defmacro err (var)
  `(error (format nil "ERR:[~A]" (bprint ,var))))

(define-condition pattern-not-found-error (error)
  ((text :initarg :text :reader text)))

(defun extract (cortege html)
  (loop :for (begin end regexp) :in cortege :collect
     (multiple-value-bind (start fin)
         (ppcre:scan regexp html)
       (when (null start)
         (error 'pattern-not-found-error :text regexp))
       (subseq html (+ start begin) (- fin end)))))

(defun get-directory-contents (path)
  "Функция возвращает содержимое каталога"
  (when (not (equal "/" (coerce (last (coerce path 'list)) 'string)))
    (setf path (format nil "~A/" path)))
  (directory (format nil "~A*.*" path)))

(defun maptree-transform (predicate-transformer tree)
  (multiple-value-bind (t-tree control)
      (aif (funcall predicate-transformer tree)
           it
           (values tree #'mapcar))
    (if (and (consp t-tree)
             control)
        (funcall control
                 #'(lambda (x)
                     (maptree-transform predicate-transformer x))
                 t-tree)
        t-tree)))

;; mtm - синтаксический сахар для maptree-transform
(defmacro mtm (transformer tree)
  (let ((lambda-param (gensym)))
    `(maptree-transform #'(lambda (,lambda-param)
                            (values (optima:match ,lambda-param ,transformer)
                                    #'mapcar))
                        ,tree)))

Copyright

Копирайт вставляется в каждый сгенерированный файл для того чтобы соблюсти требования лицензии AGPL

Copyright © 2014-2017 Glukhov Mikhail. All rights reserved.
Licensed under the GNU AGPLv3

Определение модуля

Файл определения модуля экспортируется в каталог src. Во время экспорта в него включаются утилиты.

;;;; <<copyright>>
(restas:define-module #:rigidus
  (:use #:closer-mop #:cl #:iter #:alexandria #:anaphora #:postmodern)
  (:shadowing-import-from :closer-mop
                          :defclass
                          :defmethod
                          :standard-class
                          :ensure-generic-function
                          :defgeneric
                          :standard-generic-function
                          :class-name))

(in-package #:rigidus)

;; special syntax for pattern-matching - ON
(named-readtables:in-readtable :fare-quasiquote)

;; Здесь подключаются утилиты
<<utility>>

;; Механизм трансляции путей
<<pathname-translations>>

;; Работа с html tree
<<html_s_tree>>

;; Механизм преобразования страниц
<<enobler>>

Инициализация

Эта часть запускает сервер на 9993 порту.

;;;; <<copyright>>
(in-package #:rigidus)

;; start
(restas:start '#:rigidus :port 9993)
(restas:debug-mode-on)
;; (restas:debug-mode-off)
(setf hunchentoot:*catch-errors-p* t)

Трансляция путей

Трансляция путей производится с помощью встроенного механизма logical-pathname-translations

По-умолчанию считается, что директория, от которой отсчитываются пути: ~~/src/rigidus.ru~. Я не стал создавать отдельный конфигурационный файл для этой информации.

(in-package :rigidus)

(defparameter *base-dir*
  <<base_dir>>)

(defparameter *base-path* (directory-namestring *base-dir*))

(setf (logical-pathname-translations "org")
      `(("source;*.*"
         ,(concatenate 'string *base-path* "org/*.org"))
        ("publish;*.*"
         ,(concatenate 'string *base-path* "www/*.html"))))

;; (translate-logical-pathname "org:source;articles;about.txt")
;; ;; #P"/home/rigidus/src/rigidus.ru/org/articles/about.org"
;; (translate-logical-pathname "org:source;articles;emacs;about.txt")
;; ;; #P"/home/rigidus/src/rigidus.ru/org/articles/emacs/about.org"
;; (translate-logical-pathname "org:publish;articles;about.txt")
;; ;; #P"/home/rigidus/src/rigidus.ru/www/articles/about.org"
;; (translate-logical-pathname "org:publish;articles;emacs;about.txt")
;; ;; #P"/home/rigidus/src/rigidus.ru/www/articles/emacs/about.org"

Шаблон блоков статистики

Это статистика от яндекса, гугла и liveinternet counter

[TODO:gmm] - обновить

// -*- mode: closure-template-html; fill-column: 140 -*-

{namespace tpl}

{template stat}

{literal}
 <div style="position:absolute; left:-9999px;">

    <!--Google Analitics -->
    <script type="text/javascript">
        var _gaq = _gaq || [];
        _gaq.push(['_setAccount', 'UA-20801780-1']);
        _gaq.push(['_trackPageview']);
        (function() {
        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
        })();
    </script>
    <!--Google Analitics -->

    <!--LiveInternet counter-->
    <script type="text/javascript">
        <!--
             document.write("<a href='http://www.liveinternet.ru/click' "+
             "target=_blank><img src='//counter.yadro.ru/hit?t24.5;r"+
             escape(document.referrer)+((typeof(screen)=="undefined")?"":
             ";s"+screen.width+"*"+screen.height+"*"+(screen.colorDepth?
             screen.colorDepth:screen.pixelDepth))+";u"+escape(document.URL)+
             ";h"+escape(document.title.substring(0,80))+";"+Math.random()+
             "' alt='' title='LiveInternet: показано число посетителей за"+
             " сегодня' "+
             "border='0' width='88' height='15'><\/a>")
    //-->
    </script>
    <!--/LiveInternet-->


    <!-- Yandex.Metrika informer -->
    <a href="https://metrika.yandex.ru/stat/?id=3701317&amp;from=informer"
    target="_blank" rel="nofollow"><img src="//bs.yandex.ru/informer/3701317/1_0_9F9F9FFF_7F7F7FFF_0_pageviews"
    style="width:80px; height:15px; border:0;" alt="Яндекс.Метрика" title="Яндекс.Метрика: данные за сегодня (просмотры)"
                                        onclick="try{Ya.Metrika.informer({i:this,id:3701317,lang:'ru'});return false}catch(e){}"/></a>
    <!-- /Yandex.Metrika informer -->

    <!-- Yandex.Metrika counter -->
    <script type="text/javascript">
    (function (d, w, c) {
        (w[c] = w[c] || []).push(function() {
            try {
                w.yaCounter3701317 = new Ya.Metrika({id:3701317,
                        webvisor:true,
                        clickmap:true,
                        trackLinks:true,
                        accurateTrackBounce:true});
            } catch(e) { }
        });

        var n = d.getElementsByTagName("script")[0],
            s = d.createElement("script"),
            f = function () { n.parentNode.insertBefore(s, n); };
        s.type = "text/javascript";
        s.async = true;
        s.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//mc.yandex.ru/metrika/watch.js";

        if (w.opera == "[object Opera]") {
            d.addEventListener("DOMContentLoaded", f, false);
        } else { f(); }
    })(document, window, "yandex_metrika_callbacks");
    </script>

    <noscript><div><img src="//mc.yandex.ru/watch/3701317" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
    <!-- /Yandex.Metrika counter -->

  </div>
{/literal}

{/template}

Html-tree

В процессе работы бывает очень полезным представление страницы в виде дерева s-выражений. Для того чтобы разбирать html в дерево и собирать его обратно используется парсер из библиотеки html5-parser и простой сборщик, сохраняющий отступы:

(in-package :rigidus)

<<html_to_tree>>
<<tree_to_html>>

Парсинг html

Разбираем html в дерево s-выражений

(in-package :rigidus)

(defun html-to-tree (html)
  ;; (html5-parser:node-to-xmls
  (html5-parser:parse-html5-fragment html :dom :xmls))

Сборка в html

(in-package :rigidus)

(defun tree-to-html (tree &optional (step 0))
  (macrolet ((indent ()
               `(make-string (* 3 step) :initial-element #\Space)))
    (labels ((paired (subtree)
               (format nil "~A<~A~A>~%~A~4:*~A</~A>~%"
                       (indent)
                       (car subtree)
                       (format nil "~:[~; ~1:*~{~A~^ ~}~]"
                               (mapcar #'(lambda (attr)
                                           (let ((key (car attr))
                                                 (val (cadr attr)))
                                             (format nil "~A=\"~A\"" key val)))
                                       (cadr subtree)))
                       (format nil "~{~A~}"
                               (progn
                                 (incf step)
                                 (let ((ret (mapcar #'(lambda (x)
                                                        (subtree-to-html x step))
                                                    (cddr subtree))))
                                   (decf step)
                                   ret)))))
             (singled (subtree)
               (format nil "~A<~A~A />~%"
                       (indent)
                       (car subtree)
                       (format nil "~:[~; ~1:*~{~A~^ ~}~]"
                               (mapcar #'(lambda (attr)
                                           (let ((key (car attr))
                                                 (val (cadr attr)))
                                             (format nil "~A=\"~A\"" key val)))
                                       (cadr subtree)))))
             (subtree-to-html (subtree &optional (step 0))
               (cond ((stringp subtree) (format nil "~A~A~%" (indent) subtree))
                     ((numberp subtree) (format nil "~A~A~%" (indent) subtree))
                     ((listp   subtree)
                      (let ((tag (car subtree)))
                        (cond ((or (equal tag "img")
                                   (equal tag "link")
                                   (equal tag "meta"))  (singled subtree))
                              (t (paired subtree)))))
                     (t (format nil "[:err:~A]" subtree)))))
      (reduce #'(lambda (a b) (concatenate 'string a b))
              (mapcar #'(lambda (x) (subtree-to-html x step))
                      tree)))))

Преобразование страниц

Здесь механизм, который разбирает файлы, строит из них дерево s-выражений и осуществляет его трансформацию.

Я обнаружил определенную проблему с ним, связанную с выводом листингов внутри тега <pre></pre> - из-за отступов, которые формирует tree-to-html сьезжает форматирование исходного кода. Поэтому, до написания своего парсера, учитывающего эти аспекты, я закомментировал такую обработку, тем более, что в данный момент трансформация заключается просто в присоединении шаблона, содержащего трекеры статистики.

(in-package :rigidus)

(defun enobler (pathname &optional dbg)
  (let* ((file-contents (alexandria:read-file-into-string pathname))
         (onestring (cl-ppcre:regex-replace-all "(\\n|\\s*$)" file-contents (if dbg "" " ")))
         (tree (html-to-tree onestring))
         ;; (inject-css '("link" (("href" "/css/style.css") ("rel" "stylesheet") ("type" "text/css"))))
         ;; (replace-css #'(lambda (in)
         ;;                  (optima:match in
         ;;                    (`("style" (("type" "text/css")) ,_) inject-css))))
         ;; (remove-css (maptree-transform replace-css tree))
         ;; (inject-js '("script" (("src" "scripts.js"))))
         ;; (replace-js  #'(lambda (in)
         ;;                  (optima:match in
         ;;                    (`("script" (("type" "text/javascript")) ,_) inject-js))))
         ;; (remove-js (maptree-transform replace-js remove-css))
         (result tree))
    (if dbg
        result
        (format nil "~A~A~%~A~%~A"
                ;; "<!DOCTYPE html>\n"
                ""
                ;; (tree-to-html result)
                file-contents
                (tpl:stat)
                "  <div id=\"linker\"><a href=\"/\">Home</a></div>"
                ))))

Рендеринг

RESTAS использует концепцию рендера чтобы отделить отображение страницы от ее маршрута. Нам надо определить рендер для вывода orgmode-страниц:

;;;; <<copyright>>
(in-package #:rigidus)

(defclass orgmode-handler () ())

(defmethod restas:render-object ((renderer orgmode-handler) (file pathname))
  ;; NOTE: Оставлено как пример вызова CGI
  ;; (cond
  ;;   ((and (string= (pathname-type file) "cgi"))
  ;;    (hunchentoot-cgi::handle-cgi-script file))
  ;;   (t
  ;;    (call-next-method)))
  (enobler file))

Маршрутизация

Маршрутизация осуществляется средствами библиотеки RESTAS, документация по которой доступна здесь.

;;;; <<copyright>>
(in-package #:rigidus)

<<route_static_files>>
<<route_404>>
<<route_robots>>
<<route_orgmode>>
<<route_pages>>

Статические файлы

Для всех файлов, которые должны отдаваться “как есть”, таких как картинки, скрипты и стили предусмотрены соответствующие маршруты:

(in-package #:rigidus)

(restas:mount-module -css- (#:restas.directory-publisher)
  (:url "/css/")
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "css"))
                    *base-dir*)))

(restas:mount-module -img- (#:restas.directory-publisher)
  (:url "/img/")
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "img"))
                    *base-dir*)))

(restas:mount-module -js- (#:restas.directory-publisher)
  (:url "/js/")
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "js"))
                    *base-dir*)))

(restas:mount-module -resources- (#:restas.directory-publisher)
  (:url "/resources")
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "resources"))
                    *base-dir*)))

404 страница

Для ненайденных страниц мы определяем страницу с 404 ошибкой.

[TODO:gmm] - Сделать ее более функциональной и красивой

(in-package #:rigidus)

(defparameter *log-404* nil)

(defun page-404 (&optional (title "404 Not Found") (content "Страница не найдена"))
  "404 Not Found")

(restas:define-route not-found-route ("*any")
  (push any *log-404*)
  (restas:abort-route-handler
   (page-404)
   :return-code hunchentoot:+http-not-found+
   :content-type "text/html"))

Страница robots.txt

Для указаний поисковым краулерам делаем страницу robots.txt

(in-package #:rigidus)

(restas:define-route robots ("/robots.txt")
  (format nil "User-agent: *~%Disallow: "))

Страницы orgmode

Для отображения страниц, экспортированных из orgmode, используется render-method, который преобразует код страницы перед выдачей пользователю:

(in-package :rigidus)

;; (restas:mount-module -base- (#:restas.directory-publisher)
;;   (:url "/")
;;   (:render-method (make-instance 'orgmode-handler))
;;   (restas.directory-publisher:*directory*
;;    (merge-pathnames (make-pathname :directory '(:relative "www"))
;;                     *base-dir*)))

(restas:mount-module -doc- (#:restas.directory-publisher)
  (:url "/doc")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/doc"))
                    *base-dir*)))

(restas:mount-module -about- (#:restas.directory-publisher)
  (:url "/about")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/about"))
                    *base-dir*)))

(restas:mount-module -prj- (#:restas.directory-publisher)
  (:url "/prj")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/prj"))
                    *base-dir*)))

(restas:mount-module -holy- (#:restas.directory-publisher)
  (:url "/holy")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/holy"))
                    *base-dir*)))

(restas:mount-module -lrn/asm- (#:restas.directory-publisher)
  (:url "/lrn/asm")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/lrn/asm"))
                    *base-dir*)))

(restas:mount-module -lrn/forth- (#:restas.directory-publisher)
  (:url "/lrn/forth")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/lrn/forth"))
                    *base-dir*)))

(restas:mount-module -lrn/lisp- (#:restas.directory-publisher)
  (:url "/lrn/lisp")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/lrn/lisp"))
                    *base-dir*)))

(restas:mount-module -lrn/java- (#:restas.directory-publisher)
  (:url "/lrn/java")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/lrn/java"))
                    *base-dir*)))

(restas:mount-module -lrn/crypto- (#:restas.directory-publisher)
  (:url "/lrn/crypto")
  (:render-method (make-instance 'orgmode-handler))
  (restas.directory-publisher:*directory*
   (merge-pathnames (make-pathname :directory '(:relative "www/lrn/crypto"))
                    *base-dir*)))

Маршруты страниц

Для всех остальных страниц маршруты определены напрямую, так, чтобы ведомый слэш не приводил к появляению 404-ой ошибки:

(in-package :rigidus)

(restas:define-route index ("/")
  (enobler (translate-logical-pathname "org:publish;index")))

(restas:define-route index.html ("/index.html")
  (enobler (translate-logical-pathname "org:publish;index")))

(defmacro def/route (name param &body body)
  `(progn
     (restas:define-route ,name ,param
       ,@body)
     (restas:define-route
         ,(intern (concatenate 'string (symbol-name name) "/"))
         ,(cons (concatenate 'string (car param) "/") (cdr param))
       ,@body)
     (restas:define-route
         ,(intern (concatenate 'string (symbol-name name) ".html"))
         ,(cons (concatenate 'string (car param) ".html") (cdr param))
       ,@body)))

(def/route research ("research")
  (enobler (translate-logical-pathname "org:publish;research")))

(def/route slides ("slides")
  (enobler (translate-logical-pathname "org:publish;slides")))

(def/route projects ("projects")
  (enobler (translate-logical-pathname "org:publish;projects")))