---
summary: Build, package, and deploy a complete theme.
title: First template pack
path: tutorials/first-template-pack
status: published
---

# First template pack

Build a working template pack from scratch.

## 1. Lay out the directory

```
my-pack/
  manifest.json
  templates/
    base.html
    page.html
    blog-post.html
    blog-list.html
    home.html
  assets/
    css/styles.css
```

## 2. manifest.json

```json
{
  "name": "My Pack",
  "slug": "my-pack",
  "version": "1.0.0",
  "templates": [
    {"slug": "base",        "file": "templates/base.html",        "type": "layout"},
    {"slug": "page",        "file": "templates/page.html",        "type": "page", "content_types": ["page"]},
    {"slug": "blog-post",   "file": "templates/blog-post.html",   "type": "page", "content_types": ["blog-post"]},
    {"slug": "blog-list",   "file": "templates/blog-list.html",   "type": "page"},
    {"slug": "home",        "file": "templates/home.html",        "type": "page"}
  ]
}
```

## 3. base.html

```jinja
<!DOCTYPE html>
<html lang="{{ locale | default(site.default_locale | default('en')) }}">
<head>
  <meta charset="UTF-8">
  <title>{% block title %}{{ site.name }}{% endblock %}</title>
  <link rel="stylesheet" href="{{ asset_url('css/styles.css') }}">
</head>
<body>
  <main>{% block content %}{% endblock %}</main>
</body>
</html>
```

## 4. blog-post.html

```jinja
{% extends "base.html" %}
{% block title %}{{ content.fields.title }}{% endblock %}
{% block content %}
<article>
  <h1>{{ content.fields.title }}</h1>
  <time datetime="{{ content.fields.published_at }}">
    {{ content.fields.published_at | format_date }}
  </time>
  {% if content.fields.hero_image %}
    <img src="{{ asset_url(content.fields.hero_image, 'large') }}" alt="">
  {% endif %}
  <div class="prose">{{ content.fields.body | markdown }}</div>
</article>
{% endblock %}
```

## 5. Verify, package, upload

```bash
python -m scaicms.cli.template_packs verify ./my-pack --check-syntax
python -m scaicms.cli.template_packs package ./my-pack -o /tmp/my-pack.zip

curl -X POST \
  "http://localhost:8000/api/v1/template-packs/upload?set_as_default=true&replace_existing=true" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Site-ID: $SITE" \
  -F "file=@/tmp/my-pack.zip"

redis-cli PUBLISH scaicms:cache:invalidate "site:$SITE:*"
```

## 6. View

```
http://localhost:8080/blog/your-first-post
```

If you don't see your changes, check the delivery service logs — Jinja
errors surface there. The most common gotcha: writing
`content.fields.items` (Python's dict method!) instead of
`content.fields["items"]` for bracket-notation access.

## What's next

- Add a `blocks` field type and declare `block_types` in your manifest.
- Build template-pack partials (`templates/partials/header.html`, etc.) for
  reuse with `{% include %}`.
- Add a `manifest.json → settings` block to let site admins toggle behavior
  without code changes.
