Aprender
Construa um site de projetos
Um tutorial: de uma página em HTML simples até texto editável, dados estruturados, páginas de detalhe e a API.
Este tutorial constrói, de cima para baixo, o site de projetos da Fieldwork, um estúdio de design fictício. Ele começa com uma página em HTML simples e acrescenta, passo a passo, texto editável, dados estruturados de projetos, páginas de detalhe e a API. O resultado é o exemplo mostrado na página inicial e na nossa aplicação de demonstração. O código completo está em projects.zip.
Você precisa de um projeto com um site local rodando. Veja Instalação.
1. Escreva a página
Uma rota é uma pasta em ext/uri/. A página inicial é a própria pasta. Substitua o ext/uri/index.tpl da página inicial padrão:
[#set page-title="Work"#]
[#set body-classes="bg-stone-50 text-stone-900"#]
[#html#]
O index.tpl define algumas variáveis da página e passa para [#html#], a shell da página padrão. A shell gera o head do documento, as folhas de estilo e scripts, e o corpo da página a partir do main.tpl na mesma pasta. Escreva ext/uri/main.tpl como HTML simples:
<section class="mx-auto max-w-7xl px-8 pb-10 pt-16">
<h1 class="max-w-4xl text-6xl font-semibold leading-none tracking-tight">Places people use every day</h1>
<div class="mt-8 max-w-2xl text-xl leading-relaxed text-stone-600">
<p>Fieldwork designs wayfinding, services and digital tools for libraries, ports, schools and markets.</p>
</div>
</section>
As classes são Tailwind CSS, que o projeto compila por padrão. Qualquer HTML e CSS funciona aqui: a página é sua. Rode ./nimbly watch enquanto edita, para que as classes novas entrem na folha de estilo.
2. Adicione um cabeçalho e um rodapé
A shell também gera header.tpl e footer.tpl a partir da pasta da rota. O cabeçalho será compartilhado por várias páginas, então ele vira um template reutilizável. Uma pasta em ext/tpl/ com um index.tpl é chamada pelo nome. Crie ext/tpl/site-header/index.tpl:
<div class="mx-auto flex max-w-7xl flex-col gap-4 px-8 py-6 sm:flex-row sm:items-center sm:justify-between sm:py-8">
<a href="[#base-url#]/" class="text-xl font-semibold tracking-tight">Fieldwork</a>
<nav class="flex flex-wrap gap-x-6 gap-y-2 text-stone-600 sm:gap-8">
<a href="[#base-url#]/#work" class="text-stone-900">Work</a>
<a href="[#base-url#]/#contact">Contact</a>
</nav>
</div>
Em celulares, o logo e o menu ficam um sobre o outro; a partir do breakpoint sm eles dividem uma linha. Depois, ext/tpl/site-footer/index.tpl:
<div id="contact" class="mx-auto max-w-7xl px-8 py-12 text-stone-500">Fieldwork is an example studio.</div>
Então chame-os a partir da rota. ext/uri/header.tpl tem uma linha, [#site-header#], e ext/uri/footer.tpl tem [#site-footer#]. O [#base-url#] mantém os links funcionando quando o site roda numa subpasta.
3. Torne o texto editável
Os editores devem poder mudar o título e a introdução sem um desenvolvedor. Marque os dois como editáveis em main.tpl:
<section class="mx-auto max-w-7xl px-8 pb-10 pt-16">
<h1 class="max-w-4xl text-6xl font-semibold leading-none tracking-tight"
data-nb-edit="[#cfield title#]" data-nb-edit-options='{"plain":true}'>
[#get-html [#cfield title#] plain#]
</h1>
<div class="mt-8 max-w-2xl text-xl leading-relaxed text-stone-600" data-nb-edit="[#cfield intro#]">
[#get-html [#cfield intro#]#]
</div>
</section>
[#cfield title#] nomeia um campo de conteúdo que pertence a esta página, e [#get-html#] o exibe. O data-nb-edit permite que um editor autenticado o altere na página: abra o site estando autenticado, escolha Edit nas ações da página e digite. O Nimbly guarda o texto por página. Nenhuma estrutura de dados é necessária.
4. Defina os projetos
Os projetos do estúdio se repetem e têm os mesmos campos, então viram um recurso. Um recurso é uma pasta em ext/data/ com um arquivo .meta que descreve seus campos. Crie ext/data/projects/.meta:
{
"name": {"singular": "Project", "plural": "Projects"},
"title_field": "title",
"fields": {
"title": {"name": "Title", "type": "text", "required": true},
"title_slug": {"name": "URL slug", "type": "slug", "source": "title", "admin_col": false},
"sector": {
"name": "Sector", "type": "select", "admin_filter": true,
"options": {
"culture": "Culture", "public": "Public", "retail": "Retail",
"education": "Education", "mobility": "Mobility"
}
},
"year": {"name": "Year", "type": "text"},
"published": {"name": "Published", "type": "boolean", "admin_filter": true},
"cover": {"name": "Cover", "type": "image"},
"description": {
"name": "Description", "type": "html",
"buttons": "bold,italic,anchor", "admin_col": false
}
},
"index": ["title_slug"],
"sort": {"field": "year", "flags": "numeric", "order": "desc"}
}
Esse único arquivo dá a você:
- uma visão geral em
/nb-admin/projectscom colunas, busca e filtros por setor e publicação, - formulários para adicionar e editar projetos, com seletor de imagem e editor de texto,
- validação do título obrigatório,
- a API REST em
/api/v1/projects, - permissões por função e um histórico de cada alteração.
O campo title_slug é derivado do título e indexado, para que um projeto possa ser encontrado pelo seu endereço. Adicione quatro ou cinco projetos no admin, com uma imagem de capa, e marque-os como publicados. Cada registro é salvo como um arquivo JSON em ext/data/projects/.
5. Mostre os projetos na página
Um cartão apresenta um projeto. Ele só é usado nesta página, então pode ficar na pasta da rota: um arquivo .tpl ao lado do index.tpl é um template dessa rota, não um novo endereço. Crie ext/uri/card-project.tpl:
<li id="project-[#item.uuid#]">
<div class="overflow-hidden rounded-2xl bg-stone-200" data-nb-edit-img="projects.[#item.uuid#].cover">
[#get-img-html [#item.cover#] sizes="sm-100,md-50,lg-33"#]
</div>
<p class="mt-5 text-sm uppercase tracking-widest text-stone-500">[#item.sector#] · [#item.year#]</p>
<h3 class="mt-2 text-2xl font-semibold"><a href="[#base-url#]/projects/[#item.title_slug#]">[#item.title#]</a></h3>
<div class="mt-3 leading-relaxed text-stone-600" data-nb-edit="projects.[#item.uuid#].description">
[#get-html item.description#]
</div>
</li>
Adicione a lista ao main.tpl, abaixo da introdução:
<section id="work" class="mx-auto max-w-7xl px-8 py-12">
[#data projects filter=published:1 sort=year|numeric|desc#]
<ul class="grid gap-x-8 gap-y-14 md:grid-cols-2 lg:grid-cols-3">
[#repeat data.projects tpl=card-project#]
</ul>
</section>
O [#data#] seleciona os projetos publicados, do mais recente para o mais antigo, e o [#repeat#] exibe o cartão uma vez para cada um, com o registro em item. O [#get-img-html#] escreve uma imagem responsiva: o servidor gera os tamanhos que o layout pede, em WebP. A descrição e a capa podem ser editadas tanto na página quanto no formulário do admin, porque apontam para o campo do registro.
O template não contém lógica: ele seleciona, repete e exibe. Quando uma página precisa de uma decisão ou de um cálculo, isso vai numa biblioteca.
6. Dê uma página a cada projeto
O endereço de um projeto é /projects/ seguido do seu slug. Um nome de pasta entre parênteses é um segmento dinâmico. Crie ext/uri/projects/(slug)/route.inc, que decide se o endereço pertence a um projeto publicado:
<?php
$parts = router_match(__FILE__);
if ($parts === false) return;
// A slug is lowercase letters, digits and dashes; anything else is not a project
$slug = $parts[0];
if (!preg_match('/^[a-z0-9-]{1,120}$/', $slug)) return;
load_library('data');
load_library('util');
$records = data_read_index('projects', 'title_slug', md5_uuid($slug));
$record = $records ? reset($records) : null;
if (!$record || empty($record['published'])) return;
set_variable_dot('project', $record);
router_accept();
A rota verifica a entrada, procura o slug no índice e aceita a requisição só para um projeto publicado. Todo o resto termina num 404. Adicione o index.tpl:
[#set page-title="[#project.title#]"#]
[#set body-classes="bg-stone-50 text-stone-900"#]
[#html#]
um header.tpl com [#site-header#], um footer.tpl com [#site-footer#], e o main.tpl:
<article class="mx-auto max-w-4xl px-8 pb-24 pt-10">
<a href="[#base-url#]/#work" class="text-sm text-stone-500">← Work</a>
<p class="mt-10 text-sm uppercase tracking-widest text-stone-500">[#project.sector#] · [#project.year#]</p>
<h1 class="mt-3 text-5xl font-semibold leading-tight tracking-tight">[#project.title#]</h1>
<div class="mt-10 overflow-hidden rounded-2xl bg-stone-200">
[#get-img-html [#project.cover#] sizes="sm-100,md-100,lg-75"#]
</div>
<div class="mt-10 max-w-2xl text-xl leading-relaxed text-stone-600">
[#get-html project.description#]
</div>
</article>
As rotas dinâmicas são registradas, para que o roteador saiba que deve consultá-las. Rode:
./nimbly routes:sync
Os títulos dos projetos na página inicial agora levam às suas próprias páginas.
7. Visualize a partir do admin
Os editores trabalham no admin e querem ver a página que estão editando. Adicione um botão ao painel de ações de cada projeto. Crie ext/tpl/btn-preview/index.tpl:
<a href="[#base-url#]/projects/[#record.title_slug#]" class="[#btn-class-secondary#] flex w-full items-center justify-center text-center">[#text Preview#]</a>
e coloque-o no .meta do recurso, ao lado de fields:
"record_actions": [
{"template": "btn-preview", "scope": "view,edit"}
]
8. Use os dados em qualquer lugar
Os mesmos registros respondem pela API REST, para um app, uma tela ou outro sistema. Quem chama entra como usuário do Nimbly, recebe um token e pede os registros.
Um usuário só pode pedir um token quando o acesso à API está ligado para a conta dele. Ligue API access no painel de ações do usuário no admin, ao lado do formulário de edição em /nb-admin/users/<uuid>; isso exige uma função que possa editar usuários. Ao desligar, o token do usuário para de funcionar na hora.
Depois:
TOKEN=$(curl -s -X POST http://localhost/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "your password"}' | jq -r .token)
curl "http://localhost/api/v1/projects?filter=published:1&sort=year|desc" \
-H "Authorization: Bearer $TOKEN"
O que quem chama pode fazer segue sua função, exatamente como no admin. Numa integração real, dê a quem chama um usuário próprio com uma função que só pode ler projetos. Veja Integrações.
9. Vá além: um mapa com outra biblioteca
O exemplo também tem um mapa dos projetos em /map, feito com Leaflet, uma biblioteca que o projeto não inclui por padrão. O .meta dele acrescenta um campo location-picker, para que os editores coloquem cada projeto num mapa no formulário do admin. A rota carrega o Leaflet e seu próprio script só nessa página, e lê os projetos com [#data#], passados ao script como JSON. Esse padrão funciona com qualquer biblioteca ou framework. Veja uri/map/ no download e Stack de frontend.
O que você construiu
| Bloco | Arquivos | Papel |
|---|---|---|
| Rotas | ext/uri/, ext/uri/projects/(slug)/ |
Endereços e composição da página |
| Templates | site-header, site-footer, card-project, btn-preview |
Apresentação, reutilizada entre páginas |
| Recurso | ext/data/projects/.meta |
Dados, admin, API e permissões |
| Conteúdo | campos da página via [#cfield#] |
Texto que os editores mudam na própria página |
A partir daqui:
- Composição atômica explica como esses blocos se combinam.
- Recursos trata de campos, índices e histórico.
- Usuários e permissões configura uma função de editor.