Orbit
MVC desenvolvimento Web em Lua

Tutorial Orbit

Este tutorial mostra como criar uma aplicação blog simples, apoiada por uma base de dados. Ela é muito simples porque não inclui nenhuma página de administração ("admin"); você tem que adicionar posts diretamente na base de dados (embora você possa postar através de um console Lua, e este tutorial mostrará como), mas tem uma interface para comentários nos posts.

O tutorial parte do princípio que você já tem o Orbit instalado (preferencialmente como parte do Kepler, ou pelo LuaRocks, e já tem um servidor web que aceita configuração WSAPI (o servidor web Xavante que vem com o Kepler é uma boa escolha).

Os códigos fontes para este blog estão nas pastas ´samples´ da distribuição do Orbit. Se você instalou o Orbit pelo Kepler ou LuaRocks, veja dentro da pasta ´rocks´ da sua instalação.

Inicialização

Você deve criar um arquivo ´blog.lua´, que será o arquivo principal de códigos fontes da nossa aplicação. A primeira coisa que você deve por no arquivo é o código para carregar o Orbit e outras bibliotecas que você usará no seu aplicativo.

 
require "orbit" 
require "orbit.cache" 
require "markdown" 

Neste exemplo usaremos a página de  cache do Orbit, e o parser Markdown para marcar os posts.

Todas as aplicações Orbit são módulos Lua, portanto incluiremos esta linha:

 
module("blog", package.seeall, orbit.app) 

Isso configura o módulo ´blog´ e o inicializa como uma aplicação Orbit.

´orbit.app´ coloca muitas coisas no namespace do módulo ´blog´. Os mais importantes são os metódos ´dispatchget´, ´dispatchpost´ e ´model´ que permitem que você defina a funcionalidade principal da sua aplicação. Eles também definem a variável ´mapper´ que o Orbit usa para criar os modelos (Orbit inicializa essa variável para o seu mapeador objeto-relacional padrão). Por último, eles também definem os controles padrões para os códigos de erros 404 e 500 HTTP como as variáveis ´notfound´ e ´servererror´, respectivamente. Redefina essas variáveis se quiser páginas customizadas para a sua aplicação.

Vamos carregar um script de configuração para o blog (um modelo comum em aplicações). Você pode pegar este script aqui.

 
require "blog_config" 

As próximas linhas carregam um driver de base de dados LuaSQL (definido na configuração), e configura o mapeador objeto relacional do Orbit.

 
require("luasql." .. database.driver) 
local env = luasql[database.driver]() 
mapper.conn = env:connect(unpack(database.conn_data)) 
mapper.driver = database.driver 

O mapeador do Orbit precisa usar uma conexão de base de dados, e de qual driver você estiver usando (no momento apenas o "sqlite3" e "mysql" são aceitos).

Você precisa iniciar o mapeador antes de criar os modelos de sua aplicação porque o mapeador do Orbit consulta a base de dados durante a criação de modelos para pegar o esquema. Falando em esquema, agora é uma boa hora para criar a base de dados do seu blog. Parto do princípio que você está usando o SQLite3. Crie uma base de dados ´blog.db´ com o script SQL abaixo:

 
CREATE TABLE blog_post 
("id" INTEGER PRIMARY KEY NOT NULL, 
"title" VARCHAR(255) DEFAULT NULL, 
"body" TEXT DEFAULT NULL, 
"n_comments" INTEGER DEFAULT NULL, 
"published_at" DATETIME DEFAULT NULL); 

CREATE TABLE blog_comment 
("id" INTEGER PRIMARY KEY NOT NULL, 
"post_id" INTEGER DEFAULT NULL, 
"author" VARCHAR(255) DEFAULT NULL, 
"email" VARCHAR(255) DEFAULT NULL, 
"url" VARCHAR(255) DEFAULT NULL, 
"body" TEXT DEFAULT NULL, 
"created_at" DATETIME DEFAULT NULL); 

CREATE TABLE blog_page 
("id" INTEGER PRIMARY KEY NOT NULL, 
"title" VARCHAR(30) DEFAULT NULL, 
"body" TEXT DEFAULT NULL); 

O mapeador do Orbit usa o campo ´id´ para identificar objetos na base de dados, portanto você precisará de um para cada um dos tipos de objetos que estiver mapeando.

Por último, vamos iniciar o cache de páginas do Orbit antes de criar nossos modelos:

 
local cache = orbit.cache.new(blog, cache_path) 

O cache de páginas acelera o acesso a qualquer página que você cacheie, mas você precisará ser cuidadoso e limpar o cache para uma página quando qualquer conteúdo nela mudar. Veremos como cachear e invalidar páginas na seção de controle deste tutorial.

Criando Modelos

Nossa aplicação de blog tem três tipos de objetos: posts, comentários e páginas "estáticas" (como a página de "Sobre" do blog, por exemplo). Não é coincidência que também temos três tipos de tabelas na base de dados, cada tabela mapeia um tipo de objeto que a nossa aplicação reconhece, e para cada tipo criaremos um modelo. Primeiro criaremos um objeto modelo para posts:

 
posts = blog:model "post" 

O parâmetro para o método ´model´ é o nome de uma tabela na base de dados. O objeto ´posts´ que esse método cria representa a coleção de posts, e ao mesmo tempo é um protótipo para todos os posts (veremos as implicação disso em breve). O mapeador do Orbit cria um objeto funcional por conta própria: você pode fazer ´post:find(3)´, por exemplo, e pegar o post com ´id´ 3, ou ´post:findall("ncomments < ?", { 3, order = "published_at desc"})´ e ter uma lista de todos os posts com menos de três comentários, do mais recente ao mais antigo.

Você pode usar o método ´find´ pré-definido para todas as buscas na base de dados, mas ajuda simplificar buscas comuns nos seus métodos. Você pode fazer isso adicionando métodos no objeto ´posts´:

 
function posts:find_recent() 
return self:find_all("published_at is not null", 
{ order = "published_at desc", 
count = recent_count }) 
end 

As linhas acima adicionam um método ´find_recent´ no objeto ´posts´, retornando uma lista dos posts publicados mais recentementes (o número está no script de configuração), do mais recente ao mais antigo. A aplicação irá usar este método para gerar a lista de posts na home page, assim como a seção "Posts recentes" na lateral do blog.

Outra característica do nosso blog será a página de arquivo que mostra todos os posts de um certo mês e ano. Definiremos um método para isto também:

 
function posts:find_by_month_and_year(month, year) 
local s = os.time({ year = year, month = month, day = 1 }) 
local e = os.time({ year = year + math.floor(month / 12), 
month = (month % 12) + 1, 
day = 1 }) 
return self:find_all("published_at >= ? and published_at < ?", 
{ s, e, order = "published_at desc" }) 
end 

Este é o método mais complicado, já que temos que converter de um mês e ano simples para data de começo e fim no formato Lua padrão. Por último, também definiremos um método para retornar todos os meses (e anos) que tem posts, para mais tarde gerar os links para a seção "Arquivo" no sidebar:

 
function posts:find_months() 
local months = {} 
local previous_month = {} 
local posts = self:find_all("published_at is not null", 
{ order = "published_at desc" }) 
for _, post in ipairs(posts) do 
local date = os.date("*t", post.published_at) 
if previous_month.month ~= date.month or 
previous_month.year ~= date.year then 
previous_month = { month = date.month, year = date.year, 
date_str = os.date("%Y/%m", post.published_at) } 
months[#months + 1] = previous_month 
end 
end 
return months 
end 

Este método pega todos os posts na base de dados, ordenados por data, e iterates over them armazenando cada par de mês e ano numa lista.

We can also define methods for individual post objects by defining methods in the posts object, the only difference is how they are used (you use find_recent by doing posts:find_recent(), but you will use find_comments by doing p:find_comments(), where p is a particular post object. We will define a method to retrieve all comments of a post:

Também podemos definir métodos para objetos de posts individuais definindo métodos no objeto ´posts´, a única diferença é como eles são usados (você usa ´findrecent´ criando ´posts:findrecent()´, mas você usará ´findcomments´ criando ´p:findcomments()´, onde ´p´ é um objeto post específico. Definiremos um método para recuperar todos os comentários de um post:

 
function posts:find_comments() 
return comments:find_all_by_post_id{ self.id } 
end 

Este método usa um método pré-definido do objeto ´comments´ (que criaremos em breve) que pega todos os comentários com o campo ´post_id´ iguais ao id do post atual (´self.id´). Este método cria uma relação entre os posts e os comentários; uma versão futura do mapeador Orbit permitirá que você defina isso declaradamente.

Criar o objeto ´comments´ é simples:

 
comments = blog:model "comment" 

Vamos adicionar um método de conveniência para comentários que constróem o link de comentário a partir de seus dados:

 
function comments:make_link() 
local author = self.author or strings.anonymous_author 
if self.url and self.url ~= "" then 
return "" .. author .. "" 
elseif self.email and self.email ~= "" then 
return "" .. author .. "" 
else 
return author 
end 
end 

O objeto ´pages´ é mais simples ainda, a funcionalidade padrão fornecida pelo mapeador Orbit é suficiente, então criaremos com o ´model´:

 
pages = blog:model "pages" 

Isso conclui a parte "model" da nossa aplicação. Agora podemos seguir para definir a flow página da aplicação, definindo controllers e mapeando-os para URLs.

Definindo controladores

Controladores são a interface entre a web e sua aplicação. Com o Orbit você pode mapear a parte path das URLs de sua aplicação (pore exemplo, em http://myserver.com/myapp.ws/foo/bar o path é /foo/bar) em controladores. Em termos Lua, um controlador Orbit é uma função que recebe um objeto request/response (chamado web) e parâmetros obtidos do path, e retorna um texto que é enviado para o cliente (geralmente HTML, mas pode ser XML ou mesmo uma imagem).

Você mapeia paths em controladores com os métodos dispatch_get e dispatch_post para requisições GET e POST respectivamente. O primeiro parâmetro destes métodos é o controlador, uma função Lua, e todos os outros parâmetros são os padrões de mapeamento, escritos na sintaxe de padrões de strings de Lua, de forma que um controlador pode responder a mapeamentos diversos.

Abaixo esta o controlador para a pagina principal do blog:

 
function index(web) 
local ps = posts:find_recent() 
local ms = posts:find_months() 
local pgs = pgs or pages:find_all() 
return render_index(web, { posts = ps, months = ms, 
recent = ps, pages = pgs }) 
end 

blog:dispatch_get(cache(index), "/", "/index") 

A última linha estabelece o mapeamento entre a função index e root da aplicação. O chamado do cache define o caching para esse controlador, usando o cache que criamos anteriormente (este é outro idioma comum do Lua, funciona como"decorators").

O controlador indexmostra todos os posts recentes, e é bem direto. Ele somente chama os dados modelo solicitados apartir do banco de dados, depois chama uma função auxiliar (chamada view na tecnologia MVC) para  renderizar o código HTML atual.

Outro importante controlador é o que mostra posts únicos:

 
function view_post(web, post_id, comment_missing) 
local post = posts:find(tonumber(post_id)) 
if post then 
local recent = posts:find_recent() 
local pgs = pages:find_all() 
post.comments = post:find_comments() 
local months = posts:find_months() 
return render_post(web, { post = post, months = months, 
recent = recent, pages = pgs, 
comment_missing = comment_missing }) 
else 
return not_found(web) 
end 
end 

blog:dispatch_get(cache(view_post), "/post/(%d+)") 

Aqui nós mapeamos todos os paths como /post/53 para o controlador view_post. O pattern captura os números, e é passado por todo controlador pelo Orbit. Para /post/53, o controlador recebe a string ''53'' como post_id e usa isto para chamar o post correspondente. Novamente, a renderização do HTML esta factored out para outra função, e este controlador esta cached.

Se nenhum post com este id for encontrado, o controlador default de páginas perdidas será chamado blog.not_found (orbit.app coloque isto no  namespace do blog)

Arquivos e paginas tem estruturas similares:

 
function view_archive(web, year, month) 
local ps = posts:find_by_month_and_year(tonumber(month), 
tonumber(year)) 
local months = posts:find_months() 
local recent = posts:find_recent() 
local pgs = pages:find_all() 
return render_index(web, { posts = ps, months = months, 
recent = recent, pages = pgs }) 
end 

blog:dispatch_get(cache(view_archive), "/archive/(%d%d%d%d)/(%d%d)") 

function view_page(web, page_id) 
local page = pages:find(tonumber(page_id)) 
if page then 
local recent = posts:find_recent() 
local months = posts:find_months() 
local pgs = pages:find_all() 
return render_page(web, { page = page, months = months, 
recent = recent, pages = pgs }) 
else 
not_found(web) 
end 
end 

blog:dispatch_get(cache(view_page), "/page/(%d+)") 

Os arquivos utilizam o mesmo layout que o index, logo ele reutiliza seu gerador de HTML. Os arquivos também extraem dois parametros do path, o mês e o ano, logo os paths são como /archive/2008/05.

Finalmente você pode também pode definir arquivos estáticos com o método de conveniência dispatch_static.

 
blog:dispatch_static("/head%.jpg", "/style%.css") 

Esses também são patterns, logo os pontos são escapados. Você pode definir em sua aplicação, uma pasta como estática com blog:dispatch_static("/templates/.+"). O Orbit sempre procura pelos arquivos nas pastas das aplicações. Claro que você pode deixar sua aplicação comportar somente conteúdos dinâmicos e deixar seu servidor web servir conteúdo estátic; dispatch_static é só uma conveniência para ter aplicações "zero-configuration"

Tem um controlador para adicionar comentários. Este irá responder ao POST em vez de receber:

 
function add_comment(web, post_id) 
local input = web.input 
if string.find(input.comment, "^%s*$") then 
return view_post(web, post_id, true) 
else 
local comment = comments:new() 
comment.post_id = tonumber(post_id) 
comment.body = markdown(input.comment) 
if not string.find(input.author, "^%s*$") then 
comment.author = input.author 
end 
if not string.find(input.email, "^%s*$") then 
comment.email = input.email 
end 
if not string.find(input.url, "^%s*$") then 
comment.url = input.url 
end 
comment:save() 
local post = posts:find(tonumber(post_id)) 
post.n_comments = (post.n_comments or 0) + 1 
post:save() 
cache:invalidate("/") 
cache:invalidate("/post/" .. post_id) 
cache:invalidate("/archive/" .. os.date("%Y/%m", post.published_at)) 
return web:redirect(web:link("/post/" .. post_id)) 
end 
end 

blog:dispatch_post(add_comment, "/post/(%d+)/addcomment") 

O controlador add_comment primeiro valida o input, delegando ao view_post se o campo de comentário estiver vazio (o qual irá mostrar uma menssagem de erro na página). Você acessa o parâmetro POST pela tabela web.input, que é convenientemente  aliased para um input local variável. 

O controlador cria um novo objeto comment, preenche com dados e depois salva no banco de dados. Ele também atualiza o objeto post para aumentar o número de comment o post tem por um, alem de salvar. Depois continua para invalidar (em cache) todas as páginas que talvez mostre esta informação: o index, a página de postagem e os arquivos para este post em particular. Finalmente, ele redireciona para a página de postagem, que irá mostrar o novo comentário. Este é um idioma comum na programação web chamada POST-REDIRECT-GET, onde todo POST é seguido por um redirecionamente para um GET. Isso evita a dupla postagem no caso do usuário carregar a página novamente.

A única coisa que resta agora é a geração de HTML. Está é um tópico da próxima sessão.

Visualizações: Gerando HTML

Visualizações é o último componente do trio MVC. Para o Orbit, visualizações são funções simples que geram conteúdo (geralmente HTML), e são estritamente opcionais, o que significa que você pode devolver conteúdo diretamente do seu controle. Mas ainda é bom ter prática em programação para separar controles e visualizações.

Como você gera conteúdo é escolha sua: concatene correntes Lua, use ´table.concat´, use um template de biblioteca de terceiros... Orbit fornece geração de HTML/XML programático através de órbit.htmlify´, mas você está livre para usar qualquer método que preferir. Neste tutorial manteremos a geração programática, embora, assim como outros métodos (strings retas, Cosmo, etc.) são inteiramente documentadas em outro lugar.

Quando você htmlify uma função, o Orbit muda o ambiente da função permitindo que você gere HTML chamando as tags de funções. É melhor mostrar como funciona do que explicar, então aqui vai a visualização básica da aplicação do blog, ´layout´:

 
function layout(web, args, inner_html) 
return html{ 
head{ 
title(blog_title), 
meta{ ["http-equiv"] = "Content-Type", 
content = "text/html; charset=utf-8" }, 
link{ rel = 'stylesheet', type = 'text/css', 
href = web:static_link('/style.css'), media = 'screen' } 
}, 
body{ 
div{ id = "container", 
div{ id = "header", title = "sitename" }, 
div{ id = "mainnav", 
_menu(web, args) 
}, 
div{ id = "menu", 
_sidebar(web, args) 
}, 
div{ id = "contents", inner_html }, 
div{ id = "footer", copyright_notice } 
} 
} 
} 
end 

Esta visualização é um decorador para outras visualizações, e gera o boilerplate para cada página do blog (cabeçalho, rodapé, sidebar). Você pode ver as funções de gerador HTML por todo o código, como ´title´, ´html´, ´head´, ´div´. Cada um tem ou uma string ou uma tabela, e gera o HTML correspondente. Se você dispensar uma tabela, a parte de banco de dados é concatenada e usada como conteúdo, enquanto a parte hash é usada como atributos HTML para aquela tag. Uma tag sem conteúdo gera um tag self-closing (´meta´ e ´link´ no código acima).

Digno de nota no código acima são as chamadas ´web:staticlink´ e as funções ´menu´ e ´sidebar´.O método ´staticlink´ gera um link para um recurso estático da aplicação, tirando o SCRIPT_NAME da URL (por exemplo, se a URL é http://myserver.com/myblog/blog.ws/index irá voltar como /myblog/style.css como o link).

As funções ´menu´ e ´sidebar´ são apenas visualizações de ajuda para gerar a barra de menu e sidebar do blog:

 
function _menu(web, args) 
local res = { li(a{ href= web:link("/"), strings.home_page_name }) } 
for _, page in pairs(args.pages) do 
res[#res + 1] = li(a{ href = web:link("/page/" .. page.id), page.title }) 
end 
return ul(res) 
end 

function _sidebar(web, args) 
return { 
h3(strings.about_title), 
ul(li(about_blurb)), 
h3(strings.last_posts), 
_recent(web, args), 
h3(strings.blogroll_title), 
_blogroll(web, blogroll), 
h3(strings.archive_title), 
_archives(web, args) 
} 
end 

Aqui você vê uma mistura de idiomas básicos do Lua (preenchendo uma tabela e passando para uma função concatenada) e o HTML programático do Orbit. Eles também usam o método ´web:link´, que gera links intra-aplicação. A função ´sidebar´ usa mais funções de conveniência, para fatorar melhor:

 
function _blogroll(web, blogroll) 
local res = {} 
for _, blog_link in ipairs(blogroll) do 
res[#res + 1] = li(a{ href=blog_link[1], blog_link[2] }) 
end 
return ul(res) 
end 

function _recent(web, args) 
local res = {} 
for _, post in ipairs(args.recent) do 
res[#res + 1] = li(a{ href=web:link("/post/" .. post.id), post.title }) 
end 
return ul(res) 
end 

function _archives(web, args) 
local res = {} 
for _, month in ipairs(args.months) do 
res[#res + 1] = li(a{ href=web:link("/archive/" .. month.date_str), 
blog.month(month) }) 
end 
return ul(res) 
end 

Note como essas funções não chamam nada no modelo, apenas usam qualquer dado que foi passado (desde o controle).

Agora podemos ir para as funções. de visualização principal. Começaremos com a mais fácil e menor, para páginas renderizadas.

 
function render_page(web, args) 
return layout(web, args, div.blogentry(markdown(args.page.body))) 
end 

Isto é uma chamada direta para o ´layout´, passando o corpo da página dentro de um ´div´. A única coisa importante é a sintaxe ´div.blogentryp´, que gera um ´div´ com um atributo ´class´ igual ao "blogentry", ao invés de um ´div´ direto.

Seguindo em frente, escreveremos a visualização para páginas index (e páginas de arquivo):

 
function render_index(web, args) 
if #args.posts == 0 then 
return layout(web, args, p(strings.no_posts)) 
else 
local res = {} 
local cur_time 
for _, post in pairs(args.posts) do 
local str_time = date(post.published_at) 
if cur_time ~= str_time then 
cur_time = str_time 
res[#res + 1] = h2(str_time) 
end 
res[#res + 1] = h3(post.title) 
res[#res + 1] = _post(web, post) 
end 
return layout(web, args, div.blogentry(res)) 
end 
end 

Novamente misturamos Lua com gerador programático, e parte fatoral do emissor (o próprio HTML para o corpo dos posts) para outra função (poderemos reutilizar esta função para visualização de apenas um post). O único pedaço incomum de lógica é o de implementar datas especiais, o código só publica quando a data muda, portanto muitos posts do mesmo dia aparecem com a mesma data.

A ajuda do ´_post´ é bem simples:

 
function _post(web, post) 
return { 
markdown(post.body), 
p.posted{ 
strings.published_at .. " " .. 
os.date("%H:%M", post.published_at), " | ", 
a{ href = web:link("/post/" .. post.id .. "#comments"), strings.comments .. 
" (" .. (post.n_comments or "0") .. ")" } 
} 
} 
end 

Agora podemos seguir para a piece-de-resistance, a visualização que renderiza posts únicos, junto com seus comentários, e o formulário "post a comment":

 
function render_post(web, args) 
local res = { 
h2(span{ style="position: relative; float:left", args.post.title } 
.. " "), 
h3(date(args.post.published_at)), 
_post(web, args.post) 
} 
res[#res + 1] = a{ name = "comments" } 
if #args.post.comments > 0 then 
res[#res + 1] = h2(strings.comments) 
for _, comment in pairs(args.post.comments) do 
res[#res + 1 ] = _comment(web, comment) 
end 
end 
res[#res + 1] = h2(strings.new_comment) 
local err_msg = "" 
if args.comment_missing then 
err_msg = span{ style="color: red", strings.no_comment } 
end 
res[#res + 1] = form{ 
method = "post", 
action = web:link("/post/" .. args.post.id .. "/addcomment"), 
p{ strings.form_name, br(), input{ type="text", name="author", 
value = web.input.author }, 
br(), br(), 
strings.form_email, br(), input{ type="text", name="email", 
value = web.input.email }, 
br(), br(), 
strings.form_url, br(), input{ type="text", name="url", 
value = web.input.url }, 
br(), br(), 
strings.comments .. ":", br(), err_msg, 
textarea{ name="comment", rows="10", cols="60", web.input.comment }, 
br(), 
em(" *" .. strings.italics .. "* "), 
strong(" **" .. strings.bold .. "** "), 
" [" .. a{ href="/url", strings.link } .. "](http://url) ", 
br(), br(), 
input.button{ type="submit", value=strings.send } 
} 
} 
return layout(web, args, div.blogentry(res)) 
end 

São muitos códigos para se digerir de uma vez, então vamos aos poucos. As primeiras linhas geram o corpo do post, usando a ajuda ´post´. Depois temos a lista de comentários, novamente com o corpo de cada comentário gerado por uma ajuda ´comment´. No meio temos uma mensagem de erro que é gerada se o usuário tentar postar um comentário vazio, e então o formulário "add a comment". Um formulário precisa de muito HTML, então tem bastante código, mas é um HTML bem básico e auto-explicatório (torná-lo bonito é responsabilidade do style sheet).

A ajuda ´_comment´ é bem simples:

 
function _comment(web, comment) 
return { p(comment.body), 
p.posted{ 
strings.written_by .. " " .. comment:make_link(), 
" " .. strings.on_date .. " " .. 
time(comment.created_at) 
} 
} 
end 

Por último, precisamos configurar todas essas funções de visualização para gerador programático de HTML:

 
orbit.htmlify(blog, "layout", "_.+", "render_.+") 

A função ´orbit.htmlify´ pega uma tabela e uma lista de modelos, e configura todas as funções nessa tabela com nomes que casem com um dos modelos para gerar HTML. Aqui configuraremos a função ´layout´, todas as funções ´render´, e todas as ajudas (as funções começando com ´´´).

Distribuição

Para esta parte do tutorial é melhor você utilizar o diretório samples/blog da distribuição Orbit (novamente, procure no diretório rocks caso você tenha instalado o Orbit via Kepler ou LuaRocks). Uma aplicação Orbit é uma aplicação WSAPI, portanto a distribuição é bastante simples, basta copiar todos os arquivos (blog.lua, blog_config.lua, blog.db, head.jpg, e style.css) para um diretório em sua raiz web (se você instalou Kepler, este seria o diretório kepler/htdocs), e criar um script disparador neste diretório. O script disparador é bem curto (chame-o de blog.ws):

 
#!/usr/bin/env wsapi.cgi 
require "blog" 
return blog 
 
#!/usr/bin/env wsapi.cgi 
require "blog" 
return blog 

Dependendo de sua configuração, você pode ter que instalar os rocks luasql-sqlite3 e markdown antes de executar sua aplicação. Feito isso basta iniciar o Xavante, apontar o seu browser para blog.ws, e você deve ver a página inicial do blog. Se você criou um arquivo blog.db a partir do zero você não verá nenhum post. A aplicação de blog em `samples/blog' inclui um arquivo blog.db já contendo posts e comentários de exemplo.

Valid XHTML 1.0!