# Local JSON

> A product catalogue built entirely from JSON files in the repo — no API, no database.

Content that lives in the repo as JSON, turned into static pages by `sitelo/data`. No API, no database, and no client-side JavaScript. Full source in [examples/json](https://github.com/paul-browne/sitelo/tree/main/examples/json).

## What you get

- A home page listing every category and product
- `/products/[slug]` — one static page per file in `data/products/`
- `/categories/[slug]` — one page per key in `data/categories.json`
- Adding a JSON file adds a page; no route to register
- Zero JS shipped — the files are read in Node at build time

## Project layout

`project`

```bash
my-site/
  sitelo.config.js
  data/
    site.json              # site-wide settings
    categories.json        # one object, keyed by slug
    products/              # one file per product
      aeron-chair.json
      jarvis-desk.json
      tolomeo-lamp.json
  src/
    lib/
      catalogue.js         # the only module that touches sitelo/data
    index.ht.js            # / — categories and every product
    products/
      [slug].ht.js         # /products/:slug — one page per JSON file
    categories/
      [slug].ht.js         # /categories/:slug — one page per key
```

Data lives outside `src/`, so sitelo never treats it as pages or assets.

## Put the content in data/

One file per product. The filename is the slug, so `aeron-chair.json` becomes `/products/aeron-chair` — nothing in the file has to say so:

`data/products/aeron-chair.json`

```json
{
  "name": "Aeron Chair",
  "category": "seating",
  "price": 1395,
  "released": "2022-04-01",
  "summary": "Mesh task chair with adjustable lumbar support.",
  "inStock": true
}
```

Categories are a single file instead: an object keyed by slug, which `readJsonCollection` reads as a collection just the same.

`data/categories.json`

```json
{
  "seating": {
    "name": "Seating",
    "description": "Chairs and stools for long working days."
  },
  "desks": {
    "name": "Desks",
    "description": "Fixed-height and sit-stand work surfaces."
  }
}
```

## Read it in one place

A small server-only module wraps the reads. Nothing in the HTML references it, so it never ships to the browser — and because `sitelo/data` memoizes per file, every page calling these helpers still parses each JSON file once for the whole build.

`src/lib/catalogue.js`

```javascript
import { readJson, readJsonCollection } from 'sitelo/data'

/** Site-wide settings: one JSON file, parsed as-is. */
export function getSite() {
  return readJson('data/site.json')
}

/** One entry per file, slug taken from the filename. */
export function getProducts() {
  return readJsonCollection('data/products', { sort: 'name' })
}

/** One file holding an object keyed by slug — the key becomes the slug. */
export function getCategories() {
  return readJsonCollection('data/categories.json')
}

/** Products in one category, cheapest first. */
export async function getProductsInCategory(slug) {
  const products = await readJsonCollection('data/products', { sort: 'price' })
  return products.filter((product) => product.category === slug)
}
```

## List everything on the home page

`src/index.ht.js — Template literal`

```javascript
import { getCategories, getProducts, getSite } from './lib/catalogue.js'

export async function data() {
  return {
    site: await getSite(),
    categories: await getCategories(),
    products: await getProducts(),
  }
}

export default ({ data }) => `
  <html lang="en">
    <head>
      <title>${data.site.title}</title>
      <link rel="stylesheet" href="/css/styles.css">
    </head>
    <body>
      <h1>${data.site.title}</h1>
      <h2>Categories</h2>
      <ul>
        ${data.categories
          .map(
            (category) =>
              `<li><a href="/categories/${category.slug}">${category.name}</a></li>`,
          )
          .join('')}
      </ul>
      <h2>All products</h2>
      <ul>
        ${data.products
          .map(
            (product) =>
              `<li><a href="/products/${product.slug}">${product.name}</a></li>`,
          )
          .join('')}
      </ul>
    </body>
  </html>
`
```

`src/index.ht.js — ht.js (recommended)`

```javascript
import { html, head, title, link, body, h1, h2, ul, li, a } from 'javascript-to-html'
import { getCategories, getProducts, getSite } from './lib/catalogue.js'

export async function data() {
  return {
    site: await getSite(),
    categories: await getCategories(),
    products: await getProducts(),
  }
}

export default ({ data }) =>
  html({ lang: 'en' },
    head(
      title(data.site.title),
      link({ rel: 'stylesheet', href: '/css/styles.css' }),
    ),
    body(
      h1(data.site.title),
      h2('Categories'),
      ul(
        ...data.categories.map((category) =>
          li(a({ href: `/categories/${category.slug}` }, category.name)),
        ),
      ),
      h2('All products'),
      ul(
        ...data.products.map((product) =>
          li(a({ href: `/products/${product.slug}` }, product.name)),
        ),
      ),
    ),
  )
```

`src/index.ht.jsx — JSX`

```jsx
import { getCategories, getProducts, getSite } from './lib/catalogue.js'

export async function data() {
  return {
    site: await getSite(),
    categories: await getCategories(),
    products: await getProducts(),
  }
}

export default function Home({ data }) {
  return (
    <html lang="en">
      <head>
        <title>{data.site.title}</title>
        <link rel="stylesheet" href="/css/styles.css" />
      </head>
      <body>
        <h1>{data.site.title}</h1>
        <h2>Categories</h2>
        <ul>
          {data.categories.map((category) => (
            <li key={category.slug}>
              <a href={`/categories/${category.slug}`}>{category.name}</a>
            </li>
          ))}
        </ul>
        <h2>All products</h2>
        <ul>
          {data.products.map((product) => (
            <li key={product.slug}>
              <a href={`/products/${product.slug}`}>{product.name}</a>
            </li>
          ))}
        </ul>
      </body>
    </html>
  )
}
```

## One page per JSON file

`generateStaticParams` returns a slug per file at build time; `data()` loads the matching entry for each page.

`src/products/[slug].ht.js — Template literal`

```javascript
import { getProduct, getProducts } from '../lib/catalogue.js'

export async function generateStaticParams() {
  const products = await getProducts()
  return products.map((product) => ({ slug: product.slug }))
}

export async function data({ params }) {
  const product = await getProduct(params.slug)
  if (!product) throw new Error(`Unknown product: ${params.slug}`)
  return { product }
}

export default ({ data }) => `
  <html lang="en">
    <head>
      <title>${data.product.name}</title>
      <link rel="stylesheet" href="/css/styles.css">
    </head>
    <body>
      <h1>${data.product.name}</h1>
      <p>${data.product.summary}</p>
      <dl>
        <dt>Category</dt>
        <dd>${data.product.category}</dd>
        <dt>Released</dt>
        <dd>${data.product.released}</dd>
        <dt>Availability</dt>
        <dd>${data.product.inStock ? 'In stock' : 'Out of stock'}</dd>
      </dl>
    </body>
  </html>
`
```

`src/products/[slug].ht.js — ht.js (recommended)`

```javascript
import { html, head, title, link, body, h1, p, dl, dt, dd } from 'javascript-to-html'
import { getProduct, getProducts } from '../lib/catalogue.js'

export async function generateStaticParams() {
  const products = await getProducts()
  return products.map((product) => ({ slug: product.slug }))
}

export async function data({ params }) {
  const product = await getProduct(params.slug)
  if (!product) throw new Error(`Unknown product: ${params.slug}`)
  return { product }
}

export default ({ data }) =>
  html({ lang: 'en' },
    head(
      title(data.product.name),
      link({ rel: 'stylesheet', href: '/css/styles.css' }),
    ),
    body(
      h1(data.product.name),
      p(data.product.summary),
      dl(
        dt('Category'),
        dd(data.product.category),
        dt('Released'),
        dd(data.product.released),
        dt('Availability'),
        dd(data.product.inStock ? 'In stock' : 'Out of stock'),
      ),
    ),
  )
```

`src/products/[slug].ht.jsx — JSX`

```jsx
import { getProduct, getProducts } from '../lib/catalogue.js'

export async function generateStaticParams() {
  const products = await getProducts()
  return products.map((product) => ({ slug: product.slug }))
}

export async function data({ params }) {
  const product = await getProduct(params.slug)
  if (!product) throw new Error(`Unknown product: ${params.slug}`)
  return { product }
}

export default function Product({ data }) {
  return (
    <html lang="en">
      <head>
        <title>{data.product.name}</title>
        <link rel="stylesheet" href="/css/styles.css" />
      </head>
      <body>
        <h1>{data.product.name}</h1>
        <p>{data.product.summary}</p>
        <dl>
          <dt>Category</dt>
          <dd>{data.product.category}</dd>
          <dt>Released</dt>
          <dd>{data.product.released}</dd>
          <dt>Availability</dt>
          <dd>{data.product.inStock ? 'In stock' : 'Out of stock'}</dd>
        </dl>
      </body>
    </html>
  )
}
```

## Edit and watch

`terminal`

```bash
npm install
npm run build
```

Under `sitelo`, changing a price reloads the open page — the dev server watches the JSON files pages actually read. Duplicate slugs, missing files, and malformed JSON fail the build with the offending path named.

[Data loading docs](https://sitelo.dev/docs/data) · [Routing docs](https://sitelo.dev/docs/routing) · [Configuration docs](https://sitelo.dev/docs/configuration)
