вторник, 26 апреля 2011 г.

Блог переехал

Блог переехал, теперь я буду жить тут: http://archimag.lisper.ru/

пятница, 25 марта 2011 г.

cl-sanitize

Написал cl-sanitize - библиотеку для очистки HTML от нежелательного содержания. Точнее, не написал, а портирова Sanitize (Ruby). Хотя "портировал" тоже не совсем верное слово, ибо реализация алгоритма очистки тривиальна и его я написал сам (это просто быстрее, чем разбирать код на Ruby), но вот самое важное - конфигурационные данные и набор тестов взял из Sanitize.

cl-sanitize основана на cl-libxml2. Я сомневался некоторое время, что взять за основу cxml или cl-libxml2 (сомнения тем более оправданны, что cxml и cl-libxml2 плохо совместимы между собой из-за конфликта имён), но всё таки решил использовать cl-libxml2. Во-первых, оригинальная версия основанна на libxml2, так что это позволило легко получить идентичные результаты на тестах. Во-вторых, я не имею такого доверия к "Closure HTML", как к libxml2. А это очень важный момент, ибо после просмотра тестов мне стало немного страшно ходить в интерент.

В процессе пришлось немного допилить cl-libxml2 - как то мне сейчас несколько страшновато заглядывать внутрь, всё таки это был фактически мой первый серьёзный опыт использования CL, отрефракторить бы её основательно, да боюсь, что до этого руки у меня не скоро дойдут.

вторник, 15 марта 2011 г.

Новый парсер для cl-closure-template

Полностью переписал парсер для cl-closure-template на базе esrap. Я вообще давно хотел переписать его на какой-нибудь более серьёзной базе, но те решения для парсинга на CL, которые мне попадались, меня совершенно не устраивали - запутанно и слишком громоздко. esrap понравилась мне сразу, во-первых это PEG, во-вторых описание грамматики можно разбивать на множество отдельных кусков, а не загонять их в один большой макрос, в-третьих - описание грамматики объединяется с кодом по обработке, так что никаких раздельных стадий лексического и синтаксического анализа - всё объединено.

Вообще, esrap принципиально очень сильно похожа на моё старое решение (которое я подсмотрел в коде dokuwiki, а туда оно попало из какого-то решения на Perl), но гораздо совершенней и основано на более выразительных инструментах. Благодаря принципиальной схожести переписывание оказалось довольно простым и заняло 2 дня. При этом, не сказать, что размер код сильно уменьшился, но вот соответствие оригинальной спецификации стало значительно более точным. Например, появилась возможность использовать экспоненциальную форму записи для чисел с плавающей точкой, целые в шестнадцатеричной форме, или напрямую писать коды Unicode в строковых литералах.

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

Мне пришлось исправить несколько своих шаблонов по одной причине - старая версия дозволяла использовать символы { и } достаточно свободным образом (что противоречит спецификации) и пришлось завернуть соответствующие куски в literal секции. Плюс, было несколько мелких опечаток, которые проглатывались старой версией. Ещё в старой версии можно было записывать имена переменных как $foo-bar-x, что противоречит спецификации и я это запретил, но возможность записывать имена шаблонов в "лисп-стиле" пока всё таки оставил (может тоже запретить?).

Полный код парсера здесь.

P.S. Если придётся что-нибудь ещё парсить, то однозначно буду использовать esrap.

четверг, 3 марта 2011 г.

Ещё пара полезных декораторов

В веб-приложениях для отдачи статических файлов принято использовать возможности веб-серверов (Apache, Nginx), поскольку традиционные языки веб-разработки (Python, Ruby, PHP, Perl) справляются с этой задачей очень плохо. Однако, это связано с рядом ограничений. Во-первых, нельзя автоматически генерировать ссылки на основе информации о маршрутах. Но если это ещё можно пережить (хотя, честное слово, очень неудобно), то есть куда более серьёзная проблема - часто нужен "управляемый" доступ к статическим файлам. Например, скрипт должен проверить права доступа. Для решения этой дилеммы была придумана техника, когда скрипт выполняет свою логику, а реальную передачу файла делегирует веб-серверу путем настройки специальных заголовков ответа. Для nginx это X-Accel-Redirect, а для Apache - X-Sendfile.

При разработке на Common Lisp проблема отдачи статических файлов стоит значительнее менее остро, ибо язык сам по себе намного быстрее и Hunchentoot справляется с этой работой достаточно быстро. Я для публикации статических файлов пользуюсь restas-directory-publisher - модулем для RESTAS. Однако, следует признать, что возможны ситуации, когда отдавать статику средствами CL окажется не очень разумно.

Тут вот и возникла идея обеспечить возможность использования механизма XSendfile прозрачным для основной логики образом. В итоге, я добавил в RESTAS соответствующие декораторы для Apache и Nginx.

Ниже я приведу возможные примеры использования этих декораторов в пакете restas-doc, обеспечивающего работу http://restas.lisper.ru/.

для публикации файлов я использую такой код:
(restas:mount-submodule -publisher- (#:restas.directory-publisher))
При этом, сами файлы с документацией лежат в каталоге /usr/share/doc/restas/.

@apache-xsendfile
При работе с Apache всё просто:
(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@apache-xsendfile))
а в конфиге Apache необходимо установить значение параметра XSendFilePath таким образом, что бы публикуемые файлы оказались внутри указанного каталога. Самый простой путь
XSendFilePath /
Но это может оказаться не очень разумным с точки зрения администрирования, так что в данном случае можно и так
XSendFilePath /usr/share/doc/restas/
Более подробная информация о настройке сервера есть в официальной документации.

@nginx-accel-redirect
Настройка nginx является более сложной. Необходимо создать internal-секцию и дальше есть два варианта: либо установить root, либо задать alias. В зависимости от выбранного варианта необходимо различным образом формировать заголовок X-Accel-Redirect. Для поддержки этих вариантов в RESTAS добавлено три переменных:
(defvar *nginx-internal-location* nil)
(defvar *nginx-internal-alias* nil)
(defvar *nginx-internal-root* nil)
и их надо настроить точно в соответствии с настройками nginx. При этом, переменные *nginx-internal-alias* и *nginx-internal-root* являются взаимоисключающими и одна из них должна быть установлена в NIL. В принципе, их можно выставить глобально, но RESTAS позволяет сделать лучше. Например, для такого конфига nginx:

location /restas {
internal;
root /usr/share/doc;
}
Код на CL может быть таким:
(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@nginx-accel-redirect)
(restas:*nginx-internal-location* #P"/restas/")
(restas:*nginx-internal-root* #P"/usr/share/doc/"))
Настройка с root представляется мне довольно неудобной и если nginx поднят только для CL, то гораздо проще делать так
location /protected/ {
internal;
alias /usr/share/doc/restas/;
}
и
(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@nginx-accel-redirect)
(restas:*nginx-internal-location* "/protected/")
(restas:*nginx-internal-alias* "/usr/share/doc/restas/"))
Вообще, описанный функционал обеспечивает очень важную возможность работать со статическими файлами средствами CL, например, создавать повторно используемые компоненты, а в случае возникновения необходимости легко подключить механизм XSendFile с помощью подключения нужного декоратора.

Я пока разбирался с этими вопрос успел посмотреть советы по использованию XSendFile с RoR и Django - там это носит вид хаков, жёстко прибивающих код к конкретной конфигурации веб-сервера. Этот факт подкрепил моё мнение, что модель декораторов RESTAS вкупе с возможностями CL (такими, как CLOS) обеспечивает более гибкие и мощные возможности, чем более традиционная схема с middleware-компонентами.

Прозрачное использование Parenscript

Я использую Parenscript в реализации cl-closure-template. Но обычный код предпочитаю писать на JavaScript. На то есть несколько причин и одна из самых существенных - возникающие инфраструктурные трудности. На самом деле я бы хотел просто размещать файлы с кодом на Parenscript среди других статических файлов и больше ни о чём не думать, а оно пусть там как-нибудь само обрабатывается. После добавления в RESTAS декораторов реализовать такое поведение оказалось очень просто:
(defclass ps-compile-route (routes:proxy-route) ())

(defmethod restas:process-route ((route ps-compile-route) bindings)
(let ((result (call-next-method)))
(cond
((and (pathnamep result)
(string= (pathname-type result) "parenscript"))
(setf (hunchentoot:content-type*)
"text/javascript")
(let ((*package* (find-package '#:ps)))
(ps:ps-compile-file result)))
(t result))))

(defun @ps-compile (origin)
(make-instance 'ps-compile-route :target origin))
В случае если основной обработчик маршрута возвращает pathname с расширением ".parenscript", то данный декоратор компилирует его содержимое в код на JavaScript и отправляет клиенту, иначе возвращает результат без какой-либо обработки. Теперь я просто публикую статические файлы
(restas:mount-submodule -static- (#:restas.directory-publisher @ps-compile)
(restas.directory-publisher:*directory* #P"/path/to/static/")
И если мой Parenscript-код находится в файле "/path/to/static/js/core.parenscript", то в HTML он подключается как
<script src="/js/test.parenscript"></script>

среда, 2 марта 2011 г.

Интернет-магазина на Common Lisp

Если вы мечтали купить что-нибудь в интернет-магазине, написанном на Common Lisp, то с недавних пор имеете такую счастливую возможность (особенно, если живёте в Питере) - www.320-8080.ru, разрабатывается rigidus-ом на основе RESTAS и cl-closure-template!

понедельник, 28 февраля 2011 г.

Декораторы

В предыдущем сообщении я использовал термин "middleware", который показался мне в итоге не очень удачным. Всё же middleware это больше про WSGI или про тот же Clack. А в контексте RESTAS более точным и адекватным видимо является термин "декоратор". Я изменил это в коде, а также добавил несколько новых возможностей.

Теперь restas:define-module и restas:define-route имеют keyword аргумент :decorators, в котором можно указать список декораторов, используемых для преобразования маршрутов. Вместо :decorators в restas:define-module также можно использовать переменную *decorators*, создаваемую при определении модуля. Т.е. теперь указывать декораторы можно при определении модуля, определении маршрута и подключении субмодуля. При этом, декораторы, указанные на различных уровнях, не замещают друг друга, а применяются последовательно.

В качестве примера декоратора, а также как просто удобную возможность, добавил restas:no-cache-decorator:
(defclass no-cache-route (routes:proxy-route) ())

(defmethod process-route :before ((route no-cache-route) bindings)
(setf (hunchentoot:header-out :expires)
(hunchentoot:rfc-1123-date))
(setf (hunchentoot:header-out :cache-control)
"max-age=0, no-store, no-cache, must-revalidate"))

(defun no-cache-decorator (route)
(make-instance 'no-cache-route :target route))
Кстати, функция no-cache из Hunchentoot, делает какую-то ерунду.

четверг, 24 февраля 2011 г.

RESTAS middleware

Я сегодня рассматривал Clack и должен сказать, что мне это определённо не нравится. Впрочем WSGI у меня тоже никогда больших восторгов не вызывал. Слишком универсальный протокол и слишком слабый API. Но, сама концепция middleware (в том виде, как она есть, например, в Pylons) не так уж и плоха. Просто, на мой вкус, её надо сделать более конкретной. Кое какие мысли у меня на эту тему уже крутились и я даже показывал недавно, как можно тонко влиять на процесс обработки запроса. И вот сегодня идея начала приобретать конкретную форму. В частности, изменился макрос restas:mount-submodule, теперь можно писать примерно так:
(restas:mount-submodule -mysubmodule- (#:mymodule middleware1 middleawre2)
..)
Здесь middleware1 и middleware2 это функции, которые должны принимать маршрут и возвращать другой.

В момент построения дерева маршрутов (а это происходит каждый раз при вызове функции restas:recconect-all-routes) для конкретного субмодуля строиться список обрабатываемых им маршрутов и пропускается через цепочку middleware-вызовов. middleware-функция может как угодно изменить маршрут или вообще вернуть другой. Специально для поддержки этого я добавил в cl-routes новый класс routes:proxy-route (который реализует известный паттерн proxy).

Скажем, с помощью модуля restas-directory-publisher можно публиковать директории со статикой:
(restas:mount-submodule -tmp- (#:restas.directory-publisher)
(restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/)
(restas.directory-publisher:*autoindex* t))
Такой код позволит любому просматривать содержимое директории /tmp на сервере, но кто знает что там может быть. Теперь с помощью middleware можно защитить содержимое этого каталога, требуя от посетителей пройти HTTP-авторизацию:
(defclass http-auth-route (routes:proxy-route) () )

(defmethod routes:route-check-conditions ((route http-auth-route) bindings)
(if (call-next-method)
(multiple-value-bind (user password) (hunchentoot:authorization)
(or (and (string= user "hello")
(string= password "world"))
(hunchentoot:require-authorization)))))

(defun http-auth-middleware (route)
(make-instance 'http-auth-route :target route))

(restas:mount-submodule -tmp- (#:restas.directory-publisher http-auth-middleware)
(restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/)
(restas.directory-publisher:*autoindex* t))
Здесь определяется новый класс http-auth-route, наследующий от routes:proxy-route, и для него специализируется метод routes:route-check-conditions, который вызывается для проверки соответствия маршрута условиям запроса. Если маршрут проходит все проверки, то проверяется прошёл ли пользователь HTTP-авторизацию. Функция http-auth-middleware используется для создания таких маршрутов и указывается в списке middlewares макроса restas:mount-submodule.

Для маршрутов, наследующих от routes:proxy-route, имеет смысл определять специализации методов routes:route-check-conditions и/или restas:process-route.

Чуть более сложный пример использования описанных возможностей можно найти здесь.

воскресенье, 20 февраля 2011 г.

Крякнем, плюнем и надёжно скрепим скотчем (с)

Итак, мне удалось запустить локальную версию сайта lisper.ru на своей машине под управлением Mongrel2 без Hunchentoot. В наличии имеется некоторое количество костылей, жуткого оверхеда и ещё не решённых вопросов, но это работает, включая обработку POST-запросов (как application/x-www-form-urlencoded, так и multipart/form-data), работу с cookie и отдачу статики.

Изменение в коде компонентов lisper.ru были самыми минимальными и заключались в переключении с использования Hunchentoot API на библиотеку cl-wsal.

Common Lisp Web servers abstraction layer

Идея cl-wsal зрела у меня уже давно и нужен был лишь маленький толчок, который и был сделан в предыдущем обсуждении. Я вынес из Hunchentoot значительную часть кода, которая может быть полезной в любых веб-серверах для CL в эту библиотеку и с её помощью довольно легко довёл cl-mongrel2 до необходимого состояния.

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

Весь этот код хорошо протестирован, поскольку просто взят из Hunchentoot. Правда, кое-что я изменил. Использование flexi-streams я заменил на babel. Пришлось вырвать кусок из chunga. А также скопировать и несколько отредактировать rfc2388.lisp из одноимённого пакета.

Вся работа над соответствующими изменениями в RESTAS ведётся в бранче Mongrel2. Следующий релиз RESTAS не будет включать этих изменений, посколькуо будет увязан с новым релизом Hunchentoot. А вот после этого я буду полностью переключаться на использование cl-wsal.

четверг, 17 февраля 2011 г.

RESTAS и Mongrel2

Hunchentoot предоставляет очень удобный интерфейс, но есть некоторые сомнения о возможности использования его под высокой нагрузкой, всё таки схема поток на соединения имеет достаточно понятные пределы для масштабирования. Правда, судя по патчам ITA они таки используют Hunchentoot под достаточно высокой нагрузкой, но хотелось бы всё таки иметь и другое решение. Писать асинхронный веб-сервер на CL, который бы предоставлял уровень сервиса сопоставимый с Hunchentoot, меня сейчас не прельщает. Я достаточно долго хотел просто форкнуть Hunchentoot, переделать его на базе iolib с использованием epoll и т.п., но сейчас уже отказался от этой идеи. Отказался после того, как узнал о существовании Mongrel2, который обещает все плюшки асинхронного веб-сервера и при этом не зависит от языка.

Сейчас моя самая большая цель - научить RESTAS работать с Mongrel2 (сохранив при этом возможность работать и с Hunchentoot). Но есть проблема, что Mongrel2 не предоставляет такого интерфейса, как Hunchentoot, он только умеет обрабатывать HTTP и всё. Соответственно, задача сводится фактически к вырыванию довольно больших кусков кода из Hunchentoot и адаптации их для Mongrel2. Но в данный момент я не имею достаточных временных ресурсов для выполнения этой работы. Отсюда и вопрос. Нет ли желающих помочь в этом (достойном) деле? ;)

вторник, 15 февраля 2011 г.

Управление обработкой запросов в RESTAS

Добавил в RESTAS экспериментальную возможность: переменные restas:*before-dispatch-request-hook* и restas:*after-dispatch-request-hook*. В эти переменные можно складывать (pushnew) функции, которые будут вызываться соответственно до и после обработки запроса. restas:*before-dispatch-request-hook* может быть полезен, например, для безусловного требования авторизации от пользователей - не-авторизованных пользователей можно перенаправлять на страницу входа или запрашивать HTTP-авторизацию с помощью hunchentoot:require-authorization. А restas:*after-dispatch-request-hook* можно использовать, например, для сбора статистики обращения к различным URL, или можно делать косметическую пост-обработку результата, скажем, добавлять заголовки ответа.

Для иллюстрации использования restas:*before-dispatch-request-hook* я написал тривиальный пример модуля для сбора статистики, посмотреть его можно здесь.

restas:*before-dispatch-request-hook* и restas:*after-dispatch-request-hook* не позволяют серьёзным образом влиять на обработку запроса (хотя с их помощью и можно вносить небольшие изменения), кроме того - они применяются для всех запросов, обрабатываемых веб-сервером.

Есть другая, более тонкая возможность влиять на ход обработки запросов. Например, если необходимо ограничить доступ к маршрутам из какого-либо модуля или проводить тонкую настройку обработки маршрутов из конкретного модуля.
(in-package #:mymodule)

(defclass myroute (restas:route) ())

(defmethod restas:module-routes ((module (eql #.*package*)) submodule)
(let ((routes (call-next-method)))
(iter (for route in routes)
(change-class route 'myroute))
routes))
Здесь объявляется свой класс маршрутов myroute и для конкретного модуля специализируется метод restas:module-routes, в котором основная работа делается с помощью call-next-method, а затем у полученных маршрутов изменяется класс с restas:route на myroute. Теперь можно определить собственные специализации для методов обработки маршрутов и определить произвольную логику обработки любым удобным способом. Для класса myroute имеет смысл специализировать следующие методы:
  • routes:route-check-conditions (route bindings) - проверяется соответствует ли запрос требования маршрута
  • restas:process-route (route bindings) - здесь происходит вызов обработчика маршрута и, соответственно, генерация контента

среда, 9 февраля 2011 г.

swank-js - удивительное рядом

Поскольку мне приходится писать и отлаживать много JavaScript кода, то я уже давно мечтал о возможность изменять исходный код при работе в Emacs и сразу же отправлять изменения в браузер. Так же очень здорово было бы иметь JavaScript-консоль в Emacs, которая бы реально взаимодействовал с открытой веб-страницей. Или очень часто нужно немного подправить CSS и заставить браузер применить эти изменения без перезагрузки страницы. Звучит несколько фантастически, но сейчас это совершенно реально благодаря проекту swank-js.

Мне, правда, пришлось внести небольшое изменение в оригинальный код, что бы это заработало для меня. Суть отличий в том, что в веб-страницу надо дополнительно включать такой JavaScript-код:
 SwankJS.setup("localhost", {port: 8009});
Мои изменения здесь.

вторник, 8 февраля 2011 г.

RESTAS и JavaScript

Популярность JavaScript в качестве серверного языка стремительно увеличивается, чему способствует в том числе и свойства самого языка - JavaScript это удивительно гибкий и пластичный язык, он даже мягче, чем лисп. Кроме того, в современных веб-приложениях часто логика перемешивается между клиентом и сервером и очень удобно использовать и там и там один и тот же код. cl-closure-template решает многие проблемы разделения логики, но не все. Поэтому, у меня зародилась мысль дать возможность создавать модули для RESTAS на JavaScript. Тем более, что существует CL-JavaScript - достаточно качественная реализация JavaScript на Common Lisp.

restas-javascript - проект, который должен дать возможность смешивать код на Common Lisp и JavaScript при разработке web-приложений на базе RESTAS. Кое-что уже работает. В частности, я уже смог переписать на JavaScript примеры из статьи Hello World - смотрите код в файле demo.js.

Структура этого кода полность аналогична соответствующему кода на Common Lisp, только использует идиомы JavaScript. Доступ к объектам request и reply (которые в CL оформлены в виде специальных переменных) в обработчиках маршрутом осуществляется через this.request и this.reply. Интерфейсы request и reply повторяет функции из документации к Hunchentoot, но именуются в стиле CamelCase, плюс некоторые методы оформлены в виде "активных свойств".

Загрузить данный файл можно следующим образом:
(restas.javascript:execute #P"/path/to/demo.js")
после чего уже можно будет идти в браузер смотреть результат.

С помощью
(restas.javascript:repl)
можно запустить примитивную JavaScript-консоль и поиграться с маршрутами. Что бы несколько упростить это я добавил простейшую реализацию console.log.

Кстати, дизайн интерфейса для JavaScript мне нравится в некоторых аспектах больше, чем для CL.

среда, 2 февраля 2011 г.

Ещё раз про cl-uglify-js

Я уже писал про cl-uglify-js, но тут случилось примечательное событие, которое на мой взгляд стоит отметить отдельно. cl-uglify-js и UglifyJS суть библиотеки-близнецы, от одного автора, развивающиеся синхронно и делающие совершенно одно и то же, просто написанные на разных языках - одна на Common Lisp, а другая на JavaScript (для Node.js). Так вот, теперь система сборки jquery использует именно UglifyJS (вместо Google Closure Compiler!), подтверждение чему можно найти здесь или здесь (раздел про BUILD SYSTEM).

В общем, сейчас можно смело утверждать, что cl-uglify-js (наряду с UglifyJS) претендует на роль одного из лучших (или даже лучшего) решений в своей области.

IE 9 - я удивлён

Я, конечно, слышал, что IE 9 это чудо техники и поддерживает все современные стандарты почти полностью и т.п. Слышал, но не верил. Но вот стало любопытно, попросил админа установить на его машине сей продукт и решил попробовать запустить на нём свой мерчендайзинг. Тут должен заметить, что я никогда не планировал использовать данное приложение под IE. Мои пользователи используют Firefox, разработку я веду в основном с помощью Chromium, время от времени тестирую под Opera. Приложение основано на XHTML + SVG, использует достаточно нетривиальную обработку XML (DOMParser и т.п.). Под этими тремя (Firefox, Chrome, Opera) браузерами приложение заработало не сразу, был целый ряд различий в поведении, так что пришлось искать код, который одинаково работает во всех этих браузерах.

И вот, просто ради любопытства запускаю под IE 9 - и, о чудо, оно просто работает. Вообще без какой-либо адаптации. У меня, честно говоря, культурный шок.

пятница, 21 января 2011 г.

Автоматическая генерация ссылок в RESTAS

Одной из типовых проблем веб-разработки является генерация ссылок. В некоторых простых случаях на неё можно просто закрыть глаза и создавать ссылки "вручную". Например, недавно rigidus опубликовал на хабре статью, в которой рассказал про создание простого сайта на Common Lisp (с использование RESTAS). В данном примере для генерации главного меню используется такой код:
(defun menu ()
(list (list :link "/" :title "Главная")
(list :link "/about" :title "About")
(list :link "/articles" :title "Статьи")
(list :link "/resourses" :title "Ресурсы")
(list :link "/contacts" :title "Контакты")))
Т.е. ссылки жёстко задаются в теле программы. Поскольку здесь их всего 5 и они очень простые, то в данном случае это не создаёт больших проблем.

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

Самый лучший способ решения данной проблемы использовать автоматическую генерацию ссылок. В RESTAS для этого есть специальная поддержка на базе функции restas:genurl. Например, с использованием данной функции вышеприведённый код можно было бы переписать следующим образом:
(defparameter *mainmenu*
'((main . "Главная")
(about . "About")
(articles . "Статьи")
(resources . "Ресурсы")
(contacts . "Контакты")))

(defun menu ()
(iter (for (route . title) in *mainmenu*)
(collect (list :link (restas:genurl route)
:title title))))
Здесь для генерации ссылок используется символ, связанный с конкретным маршрутом, а получающиеся ссылки будут учитывать базовый url, по которому подключается разрабатываемый модуль.

Это очень простая ситуация, которая решается совершенно тривиальным образом. На сайте lisper.ru имеет место более сложный случай. Исходный код данного ресурса разбит на несколько совершенно независимых пакетов, которые объединяются в один сайт на основе механизма модулей. Простое использование restas:genurl здесь не подходит, поскольку маршруты, на которые ссылается главное меню, находятся в разных модулях. Для определения состава главного меню используется такое объявление:
(defparameter *mainmenu* `(("Главная" nil main)
("Статьи" rulisp-articles restas.wiki:main-wiki-page)
("Планета" rulisp-planet restas.planet:planet-main)
("Форум" rulisp-forum restas.forum:list-forums)
("Сервисы" nil tools-list)
("Practical Common Lisp" rulisp-pcl rulisp.pcl:pcl-main)
("Wiki" rulisp-wiki restas.wiki:main-wiki-page)
("Файлы" rulisp-files restas.directory-publisher:route :path "")
("Поиск" nil google-search)))
Здесь каждому элементу меню соответствует список, содержащий следующие элементы: заголовок, субмодуль (символ, который используется при вызове restas:mount-submodule), символ маршрута (указанный в restas:define-route) и возможно несколько ключевых параметров (параметров маршрута). А для непосредственной генерации ссылок используется такой код:
(in-package #:rulisp)

(restas:with-submodule (restas:find-upper-submodule #.*package*)
(iter (for item in *mainmenu*)
(collect (list :href (apply #'restas:genurl-submodule
(second item)
(if (cdddr item)
(cddr item)
(last item)))
:name (first item)))))
Наиболее интересной в данном коде является строка
(restas:with-submodule (restas:find-upper-submodule #.*package*)
Дело в том, что генерация меню происходит каждый раз при генерации HTML-страницы для всех маршрутов, которые находятся в разных модулях и имеют различный контекст выполнения, а параметр *mainmenu* составлен с точки зрения самого верхнего модуля :rulisp, который используется для запуска приложения с помощью start.

Структура субмодулей в RESTAS образует иерархию и restas:find-upper-submodule позволяет найти нужный модуль выше по дереву, а макрос restas:with-submodule выполнить код в контексте найденного модуля. Таким образом, генерация ссылок работает всегда одинаково, не зависимо от контекста выполнения этого кода.

restas:find-upper-submodule и restas:with-submodule я добавил только сегодня, так что они пока есть только в git-версии RESTAS.