Я делал этот сайт, тренируясь в литературном программировании. Литературное программирование (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)))
Копирайт вставляется в каждый сгенерированный файл для того чтобы соблюсти требования лицензии 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&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}
В процессе работы бывает очень полезным представление страницы в виде дерева
s-выражений. Для того чтобы разбирать html в дерево и собирать его обратно используется
парсер из библиотеки html5-parser
и простой сборщик, сохраняющий отступы:
(in-package :rigidus)
<<html_to_tree>>
<<tree_to_html>>
Разбираем html в дерево s-выражений
(in-package :rigidus)
(defun html-to-tree (html)
;; (html5-parser:node-to-xmls
(html5-parser:parse-html5-fragment html :dom :xmls))
(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 ошибкой.
[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
(in-package #:rigidus)
(restas:define-route robots ("/robots.txt")
(format nil "User-agent: *~%Disallow: "))
Для отображения страниц, экспортированных из 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")))