← Back to rendered page

title: Generating pages from an API
shell: standard
eyebrow: Examples
subtitle: Hacker News walkthrough

components:
  - type: header
    title: Generating pages from an API
    eyebrow: Pattern
    subtitle: Any script that writes a string can drive kazam. Here's a 20-line one that pulls the Hacker News top 30.

  - type: button_group
    buttons:
      - label: See the live output →
        href: hackernews-live.html
        variant: primary
      - label: HN API docs ↗
        href: https://github.com/HackerNews/API
        variant: secondary

  - type: section
    eyebrow: The idea
    heading: Why this works
    components:
      - type: markdown
        body: |
          kazam reads `.yaml` files off disk. It doesn't care whether a human, a
          cron job, an LLM, or a serverless function wrote them. If you can emit
          the YAML (or JSON — JSON is valid YAML), kazam will render it.

          That makes it trivial to stand up live-ish dashboards pulled from
          whatever source: GitHub issues, Linear, Google Sheets, an internal
          metrics endpoint, an RSS feed, or — in this example — the free
          [Hacker News API](https://github.com/HackerNews/API).

  - type: section
    eyebrow: Step 1
    heading: The generator
    components:
      - type: markdown
        body: |
          Python, stdlib-only. Two halves: a tiny block-style YAML emitter
          (20 lines, no `pyyaml` dependency), and the actual fetcher that
          turns each HN story into a `card` component.

          If you'd rather not ship an emitter, drop in `yaml.safe_dump` from
          PyYAML — or skip YAML entirely and `json.dump` the dict, since
          kazam parses JSON as valid YAML.

      - type: code
        language: python
        code: |
          #!/usr/bin/env python3
          import json, urllib.request, datetime

          def fetch(url):
              return json.loads(urllib.request.urlopen(url, timeout=10).read())

          # ── minimal block-YAML emitter ──────────────────────────
          def yaml_scalar(v):
              if v is None: return "null"
              if isinstance(v, bool): return "true" if v else "false"
              if isinstance(v, (int, float)): return str(v)
              return json.dumps(v, ensure_ascii=False)  # JSON strings are valid YAML

          def yaml_dump(obj, indent=0):
              pad, lines = "  " * indent, []
              if isinstance(obj, dict):
                  for k, v in obj.items():
                      if isinstance(v, (dict, list)) and v:
                          lines.append(f"{pad}{k}:")
                          lines.extend(yaml_dump(v, indent + 1))
                      else:
                          lines.append(f"{pad}{k}: {yaml_scalar(v)}")
              elif isinstance(obj, list):
                  for item in obj:
                      if isinstance(item, dict) and item:
                          sub = yaml_dump(item, indent + 1)
                          lines.append(f"{pad}- {sub[0].lstrip()}")
                          lines.extend(sub[1:])
                      else:
                          lines.append(f"{pad}- {yaml_scalar(item)}")
              return lines

          # ── fetch + shape ───────────────────────────────────────
          ids = fetch("https://hacker-news.firebaseio.com/v0/topstories.json")[:30]
          cards = []
          for id in ids:
              item = fetch(f"https://hacker-news.firebaseio.com/v0/item/{id}.json")
              if not item or not item.get("title"):
                  continue
              desc = f"{item['by']} · {item.get('score', 0)} points · {item.get('descendants', 0)} comments"
              links = []
              if item.get("url"):
                  links.append({"label": "Read →", "href": item["url"]})
              links.append({"label": "HN →", "href": f"https://news.ycombinator.com/item?id={id}"})
              cards.append({"title": item["title"], "description": desc, "links": links})

          page = {
              "title": "Hacker News — Top 30",
              "shell": "standard",
              "eyebrow": "Live",
              "subtitle": datetime.datetime.now(datetime.UTC).strftime("Pulled %b %d, %Y %H:%M UTC"),
              "components": [
                  {"type": "header", "title": "Hacker News Top 30",
                   "subtitle": f"{len(cards)} stories"},
                  {"type": "card_grid", "min_width": 280, "cards": cards},
              ],
          }

          with open("hackernews-live.yaml", "w") as f:
              f.write("\n".join(yaml_dump(page)))

  - type: section
    eyebrow: Step 2
    heading: Build
    components:
      - type: code
        language: bash
        code: |
          python3 generate.py          # writes hackernews-live.yaml
          kazam build . --out _site    # renders it to _site/hackernews-live.html

      - type: markdown
        body: |
          That's it. The page exists. No server, no database, no runtime
          dependencies — just a file kazam can render.

  - type: section
    eyebrow: Step 3
    heading: Keep it fresh
    components:
      - type: markdown
        body: |
          Pair the generator with `kazam dev` for a live-reloading dashboard.
          The watcher notices the new YAML, rebuilds, and your browser tab
          refreshes automatically.

      - type: tabs
        tabs:
          - label: Local dev
            components:
              - type: code
                language: bash
                code: |
                  # terminal 1 — refresh every 5 minutes
                  while true; do python3 generate.py; sleep 300; done

                  # terminal 2 — watch, serve, live-reload
                  kazam dev . --port 3000

          - label: Cron / CI
            components:
              - type: code
                language: bash
                code: |
                  # crontab: refresh + rebuild + deploy every 10 minutes
                  */10 * * * * cd /srv/site && python3 generate.py \
                      && kazam build . --out _site --release \
                      && rsync -a _site/ webhost:/var/www/site/

          - label: GitHub Actions
            components:
              - type: code
                language: yaml
                code: |
                  on:
                    schedule:
                      - cron: "*/30 * * * *"
                    workflow_dispatch:
                  jobs:
                    refresh:
                      runs-on: ubuntu-latest
                      steps:
                        - uses: actions/checkout@v4
                        - run: python3 generate.py
                        - run: curl -L kazam.dev/install.sh | sh
                        - run: kazam build . --out _site --release
                        - uses: actions/upload-pages-artifact@v3
                          with: { path: _site }

  - type: callout
    variant: success
    title: The general pattern
    body: |
      `script.py` → `something.yaml` → `kazam build`. That's the whole
      contract. Anything that can emit a JSON object can drive a kazam
      page — RSS feeds, GitHub issues, Linear, Google Sheets, HubSpot,
      Stripe, your own internal JSON endpoints, or an LLM-written one-off.

  - type: callout
    variant: info
    title: Try it yourself
    body: "The live snapshot on this docs site was generated by the script above — click **[See the live output](hackernews-live.html)** to see the rendered result."

  - type: callout
    variant: info
    title: Next up
    body: Other example pages — docs, KBs, decks, landing pages, meeting briefs, API references.
    links:
      - label: See all use cases
        href: ../about.html
        variant: primary
      - label: Components reference
        href: ../components/index.html
        variant: secondary