# 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](/examples/projects.zip).

Você precisa de um projeto com um site local rodando. Veja [Instalação](/pt/docs/learn/installation/).

## 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:

```html
[#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:

```html
<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`:

```html
<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`:

```html
<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`:

```html
<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`:

```json
{
  "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/projects` com 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`:

```html
<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:

```html
<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](/pt/docs/concepts/libraries/).

## 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
<?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`:

```html
[#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`:

```html
<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:

```bash
./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`:

```html
<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`:

```json
"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:

```bash
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](/pt/docs/guides/integrations/).

## 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](/pt/docs/guides/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](/pt/docs/concepts/atomic-design/) explica como esses blocos se combinam.
- [Recursos](/pt/docs/concepts/resources/) trata de campos, índices e histórico.
- [Usuários e permissões](/pt/docs/guides/permissions/) configura uma função de editor.
