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 `index`mostra 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!