Skip to content
Case study · 18 / 19← All work

Sitios editables

A content editor without a CMS: a JSON schema draws the form, and publishing rebuilds the static page and commits it to git. Three client sites run on it.

Role
Architecture and full build
Stack
HTML, Vanilla JS, Node.js, Vercel Functions, GitHub Git Data API
The editor
The editorGit is the database
(01) The problem

Two coaches and a food engineer needed one-page sites they could change themselves: copy, photos, buttons, colors, which sections show. A hosted CMS meant a monthly bill, a login to another product and a page that fetches its content on every visit. Handing them the HTML meant a broken layout the first time someone pasted a quote.

My roleI split each site into content, schema and template, wrote the editor and the publish function, and carried the engine from one site to the next.
(02) ResultsCounted in the code
3client sites in production on the same engine
413editable fields across the three schemas, none of them HTML
89 KBfor Lole's page, down from a 710 KB design export
(03) Architecture

Three decisions that shaped it.

  1. 01

    Git is the database

    Why. contenido.json in the repo is the source of truth. Publishing sends it to a single Vercel function that writes the JSON, the rebuilt index.html and any new photos as blobs, builds one tree and one commit through the GitHub Git Data API, and moves the branch. One commit means one redeploy. Undo is a new commit with the previous commit's tree, and only runs if the last commit came from the editor, so it never reverts a code change.

    Trade-off. Every publish waits for a deploy, about a minute. The editor sends the whole content file, so two open tabs overwrite each other. Photos live in git history, so the editor resizes them to 1600 px in the browser before upload.

  2. 02

    Render at publish time, serve static HTML

    Why. render(template, content) is plain text in, text out, so the same function runs in node construir.mjs locally and inside the publish function. Visitors download one HTML file from the CDN and never call the function. Clients write plain text with three marks (*gold italic*, **bold**, Enter); everything else is escaped, and an empty field removes the block around it, so clearing a button's text removes the button.

    Trade-off. Nothing on the page is dynamic. Forms open WhatsApp or email with a prefilled message instead of posting anywhere, and the preview needs a round trip to the function.

  3. 03

    Copy the engine per site, don't package it

    Why. Each site has its own repo, Vercel project, password and fine-grained GitHub token scoped to that repo only. Alimentación Integral Hoy got the editor first, then Natalia, then Lole. Copying the files let each site grow what it needed: Alimentación has video and photo lists, Lole has one generic list type, section reordering and a palette that derives eleven colors from six.

    Trade-off. The three copies drifted. Natalia's editor has no search and no section order, and the empty-field checker only exists in two repos. A fix travels by hand.

(04) GalleryKeep scrolling →
The editor
The editor · Lole's editor running locally. Every section, label and hint comes from esquema.json. The palette checks WCAG contrast live before anything is published.
Mujer Despierta
Mujer Despierta · Lole's landing started as a design export with React, a custom runtime and base64 assets. It became a template with markers and a few KB of vanilla JS.
Alimentación Integral Hoy
Alimentación Integral Hoy · The first site on the engine, and the largest schema: programs, videos, free resources and a banner Julia changes herself.
Natalia Raffatella
Natalia Raffatella · The smallest of the three. A Next.js app was replaced by the static page and the editor.
(05) What I'd do next
01Pull the engine (editor, render and publish function) into one shared repo the three sites update from, so Lole's list type and section order reach Natalia.
02Send the commit the editor loaded with each publish and refuse it if the content changed since, so two tabs can't overwrite each other.
03Keep failed login attempts outside the function's memory. Today a new instance starts the counter at zero and only the growing delay holds.
Next project · 19 / 19
Trading analytics dashboard →Heatmaps, charts and live tables for traders
Case study · 18 / 19

Sitios editables

A content editor without a CMS: a JSON schema draws the form, and publishing rebuilds the static page and commits it to git. Three client sites run on it.

Role
Architecture and full build
Stack
HTML, Vanilla JS, Node.js, Vercel Functions, GitHub Git Data API
Sitios editables
(01) The problem

Two coaches and a food engineer needed one-page sites they could change themselves: copy, photos, buttons, colors, which sections show. A hosted CMS meant a monthly bill, a login to another product and a page that fetches its content on every visit. Handing them the HTML meant a broken layout the first time someone pasted a quote.

My roleI split each site into content, schema and template, wrote the editor and the publish function, and carried the engine from one site to the next.
(02) Results
3client sites in production on the same engine
413editable fields across the three schemas, none of them HTML
89 KBfor Lole's page, down from a 710 KB design export
(03) Architecture

Three decisions that shaped it.

01

Git is the database

Why. contenido.json in the repo is the source of truth. Publishing sends it to a single Vercel function that writes the JSON, the rebuilt index.html and any new photos as blobs, builds one tree and one commit through the GitHub Git Data API, and moves the branch. One commit means one redeploy. Undo is a new commit with the previous commit's tree, and only runs if the last commit came from the editor, so it never reverts a code change.

Trade-off. Every publish waits for a deploy, about a minute. The editor sends the whole content file, so two open tabs overwrite each other. Photos live in git history, so the editor resizes them to 1600 px in the browser before upload.

02

Render at publish time, serve static HTML

Why. render(template, content) is plain text in, text out, so the same function runs in node construir.mjs locally and inside the publish function. Visitors download one HTML file from the CDN and never call the function. Clients write plain text with three marks (*gold italic*, **bold**, Enter); everything else is escaped, and an empty field removes the block around it, so clearing a button's text removes the button.

Trade-off. Nothing on the page is dynamic. Forms open WhatsApp or email with a prefilled message instead of posting anywhere, and the preview needs a round trip to the function.

03

Copy the engine per site, don't package it

Why. Each site has its own repo, Vercel project, password and fine-grained GitHub token scoped to that repo only. Alimentación Integral Hoy got the editor first, then Natalia, then Lole. Copying the files let each site grow what it needed: Alimentación has video and photo lists, Lole has one generic list type, section reordering and a palette that derives eleven colors from six.

Trade-off. The three copies drifted. Natalia's editor has no search and no section order, and the empty-field checker only exists in two repos. A fix travels by hand.

(04) Gallery
The editor
The editor · Lole's editor running locally. Every section, label and hint comes from esquema.json. The palette checks WCAG contrast live before anything is published.
Mujer Despierta
Mujer Despierta · Lole's landing started as a design export with React, a custom runtime and base64 assets. It became a template with markers and a few KB of vanilla JS.
Alimentación Integral Hoy
Alimentación Integral Hoy · The first site on the engine, and the largest schema: programs, videos, free resources and a banner Julia changes herself.
Natalia Raffatella
Natalia Raffatella · The smallest of the three. A Next.js app was replaced by the static page and the editor.
(05) What I'd do next
01Pull the engine (editor, render and publish function) into one shared repo the three sites update from, so Lole's list type and section order reach Natalia.
02Send the commit the editor loaded with each publish and refuse it if the content changed since, so two tabs can't overwrite each other.
03Keep failed login attempts outside the function's memory. Today a new instance starts the counter at zero and only the growing delay holds.
Next projectTrading analytics dashboard →
Nahuel SantillánEN