Skip to content
Get started

Lab 1 — Start from nothing

An empty folder to a published site, with the browser open the whole time.

You have: nothing. You will have: a site you wrote, running locally, then published.

Needs: zel, and a container runtime from step 2 onward. Check with zel doctor.

  1. Create a content folder

    terminal
    zel new docs my-docs
    ✓ my-docs
      site.yaml
      pages.yaml
      sections/home/hero.yaml
      sections/home/features.yaml
      menu.yaml
      01-guide/_sidebar.yaml
      01-guide/index.mdx
      assets/.gitkeep
    
      Next: zel dev my-docs
    

    What just happened. You have a small site that is complete and correct — not a template with TODO in it. Every field written is a field the schema accepts, so each file is a working example of its own format. zel check my-docs passes right now.

    No package.json, no node_modules, nothing to install. This folder is only the words and the structure.

  2. Watch it while you write

    terminal
    zel dev my-docs
      Engine   ghcr.io/mylife-inc/zellij-base:latest
      Content  my-docs
      Serving  http://localhost:3000
    
      Edit anything under my-docs and reload. Ctrl-C to stop.
    
    ✓ /app/content
      site        Acme · theme paper
      pages       1
      guides      1 (1 page) · /guide
    

    It validates first, installs what the site needs, then serves. The first run takes a couple of minutes — it pulls a 237 MB image and installs Next.js into a named volume. Later runs reuse both and start in seconds.

    Leave this running. Open my-docs/sections/home/hero.yaml in your editor:

    my-docs/sections/home/hero.yaml
    type: hero
    headline: Your product, explained
    subhead: Change this line and watch the browser.
    cta:
      - { label: Read the guide, href: /guide, style: primary }

    Save. The browser updates before you get back to it.

    Now break something on purpose — change type: hero to type: heroo — and save. The terminal and the browser both say:

    my-docs/sections/home/hero.yaml
      unknown section type "heroo"
      did you mean "hero"?
    

    That is the loop. You are never guessing whether a change was valid.

  3. Add a page

    terminal
    zel new page pricing --sections hero,pricing-table,faq

    Three section files appear, and pages.yaml gains a route. The browser picks up /pricing without a restart.

    Not sure what a section type takes?

    terminal
    zel list sections          # all nineteen
    zel explain pricing-table  # one of them, in full

    explain prints every field, which are required, the allowed values, and the shape of anything nested. It is generated from the same schemas that validate your content, so it describes exactly what will be accepted.

  4. Add a guide

    A landing page is sections. Documentation is Markdown.

    terminal
    zel new guide docs --title "Documentation"
    ✓ 01-docs/_sidebar.yaml
    ✓ 01-docs/index.mdx
    

    Add pages by creating files. 01-docs/install.mdx:

    my-docs/01-docs/install.mdx
    ---
    title: Installing
    description: Get it running in two minutes.
    ---
    
    ## Requirements
    
    Anything that runs a container.
    
    <Callout type="tip" title="On a Mac">
    `brew install colima && colima start`
    </Callout>

    Then list it in the sidebar:

    my-docs/01-docs/_sidebar.yaml
    title: Documentation
    order: 1
    
    nav:
      - index
      - install

    It appears at /docs/install, in the sidebar, in the table of contents, and in ⌘K search.

  5. Validate before you ship

    terminal
    zel check my-docs
    ✓ my-docs
      site        Acme · theme paper
      pages       1
      guides      1 (1 page) · /guide
      menu        Guide
      home        /
      routes      2
    

    Every internal link is resolved, every section type checked, every sidebar entry matched to a file that exists. It exits non-zero when something is wrong, so it works as a pre-commit hook.

    This runs the engine in a container with your folder mounted read-only, so it needs a container runtime — and about a second.

  6. Ship it

    Two ways, and they produce genuinely different things. Pick one.

    A · A static site on Netlify

    Free, fast, and nothing runs. Right for almost every documentation site.

    Get a Netlify account and its CLI. The CLI is the only npm thing in this lab, and only because Netlify's upload protocol is theirs:

    terminal
    npm install -g netlify-cli
    netlify login

    A browser opens; authorise it.

    Create a project.

    terminal
    netlify sites:create --name my-docs
    Site Created
    
    Admin URL: https://app.netlify.com/projects/my-docs
    URL:       https://my-docs.netlify.app
    Project ID: 91774174-d4c2-41b5-aa4b-ac61eeadbf51
    

    Build and upload.

    terminal
    zel build my-docs --out dist
    netlify deploy --prod --dir dist
    ✓ dist  (19 entries)
    
    Deploy path:    /Users/you/dist
    Website URL:    https://my-docs.netlify.app
    

    Getting the two values CI needs

    The site ID. sites:create printed it as Project ID; Netlify's API calls the same value the site ID. If you have lost it:

    terminal
    netlify sites:list --json | jq -r '.[] | select(.name=="my-docs") | .site_id'

    The auth token. netlify login wrote one into netlify-cli's config, and you can read it back rather than making a second one:

    terminal
    # macOS
    NETLIFY_CONFIG="$HOME/Library/Preferences/netlify/config.json"
    # Linux
    [ -f "$NETLIFY_CONFIG" ] || NETLIFY_CONFIG="$HOME/.config/netlify/config.json"
    
    export NETLIFY_SITE_ID=91774174-d4c2-41b5-aa4b-ac61eeadbf51
    export NETLIFY_AUTH_TOKEN=$(jq -r '.users[.userId].auth.token' "$NETLIFY_CONFIG")

    Check it worked without printing the token:

    terminal
    echo "${#NETLIFY_AUTH_TOKEN} characters"    # 40, starting nfc_

    A personal access token works too, and is the better choice for a shared pipeline: User settings → Applications → New access token. It can be revoked on its own without logging your laptop out, and it is not tied to your CLI session.

    Put both into the repository: Settings → Secrets and variables → Actions → New repository secret, as NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID. That is what Lab 3 and Lab 4 expect.

    Once zel deploy lands, those two commands become one:

    terminal
    zel deploy netlify my-docs

    A custom domain

    Say you own example.com and want the site at docs.example.com.

    1. Find who actually serves your DNS

      terminal
      dig +short NS example.com

      This is the step people skip, and it is the one that costs an afternoon. Where you bought the domain is often not where its DNS is served. A record added in the registrar's panel while the nameservers point elsewhere is inert — it publishes nothing, and dig keeps returning nothing however long you wait.

      Whatever that command prints is where the next step happens.

    2. Add one CNAME, there

      FieldValue
      TypeCNAME
      Name / Hostdocs
      Points tomy-docs.netlify.app
      TTLwhatever it offers

      Use the exact *.netlify.app hostname from sites:create. A CNAME to a hostname that does not exist fails silently.

    3. Tell Netlify the name is yours

      Project → Domain management → Add a domaindocs.example.com.

      Netlify verifies the CNAME it can already see, then issues a Let's Encrypt certificate. Until you do this, the request arrives at Netlify and gets a 404 — it has no way to know which project the hostname belongs to.

    4. Confirm

      terminal
      dig +short docs.example.com && curl -sI https://docs.example.com | head -1
      my-docs.netlify.app.
      13.52.188.95
      HTTP/2 200
      

    B · A container, running locally

    Right when you need image optimisation, or when the site has to live wherever your other services live.

    terminal
    zel build my-docs --target image --tag my-docs:dev
    zel image run my-docs:dev
    ✓ Built my-docs:dev — 148 MB
    ✓ Serving http://localhost:3000
    

    That is a Node runtime serving your site on port 3000 — the same artefact you would push to Railway, Fly.io, Cloud Run or ECS.

    terminal
    zel image push my-docs:dev --registry ghcr.io/you

    You never wrote a Dockerfile. If your organisation requires one in the repository, zel image dockerfile prints the one zel would have used.

What you have

my-docs/
├── site.yaml
├── pages.yaml
├── menu.yaml
├── sections/home/
├── 01-docs/
└── assets/

A folder of text under version control, and a published site. No lockfile, no framework upgrade waiting for you, nothing that stops working because a dependency moved.

Next