Skip to content

Site JSON API

The site's content is also JSON

Everything the pages render is served over public, cacheable endpoints. Reads are open with CORS enabled, so you can pull this data from any client. The same { error } shape backs every failure response.

Endpoints

GET/api/projects

Every project as a light record. Case studies are linked, not embedded.

GET/api/projects/saferoute

One project, full record including its case study

GET/api/journal

Every post with a derived word count. Content blocks are linked, not embedded.

GET/api/journal/serverless-incident-pipeline

One post, full record including content blocks

POST/api/contact

Contact form intake. JSON body: name, email, message. Rate limited.

GET/api/messages

Stored contact submissions. Requires an admin bearer token.

Read responses are cached at the edge for one hour and stay fresh for one more while revalidating.

Try it

# List every project
curl -s https://aliutijani.com/api/projects | jq .count

# Pull one case study
curl -s https://aliutijani.com/api/projects/saferoute | jq .caseStudy

# Journal posts, word counts included
curl -s https://aliutijani.com/api/journal | jq '.items[] | {slug, wordCount, readingMinutes}'

Response shapes

GET /api/projects
{
  "count": 8,
  "items": [
    { "slug": "saferoute", "name": "SafeRoute", "tags": [...],
      "period": "...", "featured": true, "hasCaseStudy": true, ... }
  ]
}

GET /api/projects/saferoute
{
  "slug": "saferoute",
  "name": "SafeRoute",
  "overview": ["...", "..."],
  "caseStudy": { "problem": [...], "implementation": [...], "outcome": "..." },
  "tech": [...],
  "links": {...}
}
Not found (any endpoint, 404)
{ "error": "Project not found. See /api/projects for the full list." }

Contact and messages

POST /api/contact

Accepts JSON and stores the submission in Postgres before attempting email delivery. If delivery fails, the message survives and I still see it. Five requests per ten minutes per IP, tracked in the database.

GET /api/messages

Reads stored submissions. Send Authorization: Bearer <token> where the token is the server's ADMIN_TOKEN. Unguarded requests get a 401; responses are never cached.