DEV Community

Lucas Fernández Díaz
Lucas Fernández Díaz

Posted on Originally published at contento.solutions

How I run a one-person company's website like a production system

A version of this story first appeared on Contento Solutions.

My company's website runs on Squarespace. It also has 33 checks that run against it every morning, 118 scripts, a git history of 684 commits since September 1, two independent backups a day and a decision log with more than 900 rows.

I'm the only person in the company. That's exactly why it's built this way.

This is the architecture, what each part is for, and the two places where the platform said no and I had to find another way in.

Why treat a small site like production?

Because nobody else is going to notice when it breaks. On a team, someone sees the broken button on their phone. When you're alone, a regression can sit on the site for weeks. The fix isn't paying more attention. It's building something that pays attention for you.

The site has 41 public pages: an Academy with 11 guides, a Blog with 7 posts, six landing pages for specific fields, plus the usual home, services, pricing and so on. Every page carries custom code. That's a lot of surface for one person to eyeball.

The front: Squarespace plus custom code

Squarespace handles hosting, the CMS and the editor. Everything that makes the site look like itself is mine: custom HTML blocks and per-page code injections, all wrapped in one root class so the CSS is scoped and never leaks into the platform's own styles. One palette, three typefaces, the same button sizes everywhere.

Writing to Squarespace is where it got interesting. There's no public API for page content, so I watched what the editor's own Save button sends and replayed those same requests from a logged-in session. Two rules came out of that:

  1. Read the object, change one key, send the whole thing back. Rebuilding a payload by hand, or cloning a section with fresh IDs, gets you a 400 with no error body. The same request on the object you just read gets a 200.
  2. The error message is the documentation. When the layout grid rejects a change, the 400 tells you the exact limit it wanted. I raised the values until it stopped complaining.

The code injection editor was the other wall. Setting its value programmatically looks perfect: the editor updates, the Save button lights up, you save. Reload the page and the change is gone. It only persists what arrives as real input. So the process is: put the cursor where it belongs, enter the text as a person would, and never call anything saved until a cold reload outside the editor proves it.

site.json: one source for every script

The scripts don't keep their own lists. Everything structural lives in one machine-readable file, site.json: the list of public URLs, the site IDs, every surface where custom code lives (with a SHA-256 of its last known content), and the baseline value for every gate.

That rule exists because of a bug. At one point the list of URLs had been copied into six different scripts, and one gate spent several sprints checking 29 of the 33 pages that existed then. Nothing failed. It just wasn't looking. Now there's one list, and every script imports it.

The baselines get rewritten only on purpose, with an explicit update mode, when a change is intended. Never as a side effect of a run.

33 checks before coffee

ct-gates.py runs all the gates against the public URLs, anonymously, with no session. Each check compares a number against its baseline in site.json and prints PASS or FAIL. The exit code is 0 if everything passes, 1 if something fails, 2 if the network did.

The daily run reads the served HTML. Its 33 checks are grouped into families:

Family What it catches
HTTP and sitemap URLs that don't answer, public pages missing from the list
Structured data ld+json blocks that don't parse
Links broken links, orphan pages that aren't orphans on purpose
Typography a banned font sneaking back in, the wrong family on eyebrows
Page weight pages outside their normalized size band
Footer and CTAs missing global footer, drift in call to action labels
FAQ questions on a page without matching FAQ schema
llms.txt site pieces missing from the file AI assistants read

Once a week, a heavier run renders the pages in headless Chrome and checks what HTML can't tell you: computed visibility after JavaScript, real keyboard focus order through the DevTools protocol, and alignment at three screen widths.

Most of these families exist because something got through once. A banned font lived on the site for months in 183 declarations until I spotted it by accident on a form. Now it's a gate, and it can't come back without a FAIL.

The watcher: did something change that I didn't touch?

A gate tells you when a number leaves its band. It doesn't tell you when a number moves inside the band, or when someone updated the baseline without looking. So a second script, the watcher, runs the gates on a schedule and compares every result against the previous run, not only against the baseline. It also records the git HEAD of each run:

  • number changed and HEAD changed: I did that. Report it, don't alarm.
  • number changed and HEAD is the same: it moved on its own. That's the one to look at.

That logic caught the strangest bug on the site. One page appeared to double in weight in a few days, with nothing published. I saved both versions and diffed them. The content was identical byte for byte. The difference was runs of blank space the platform injects depending on which cached copy you get, more than half the page. Raw page weight on Squarespace turned out to be useless, so the weight gate now measures the page with whitespace collapsed.

The watcher runs from Windows Task Scheduler at 09:00 every day, with the full rendered run on Sundays. The runner holds a lock so two scheduled tasks firing at once after a cold boot don't overwrite each other's log, and it retries once after 90 seconds, because the first request after waking a laptop fails often.

The honest gap: if the laptop is off at 09:00, nothing runs. Moving the watcher to a small always-on server is next on the list.

Backups that don't depend on remembering

ct-respaldo.py runs at the end of the same morning round and makes two copies of every repo:

  1. A push to a private GitHub remote. It never forces, and it never commits human work. The only files it commits on its own are the ones the checkers write.
  2. A git bundle with the full history in a cloud-synced folder: one latest copy that gets overwritten and one per ISO week, keeping the last two.

Anything it couldn't back up, uncommitted changes or unpushed commits, gets written to a status file, and the run exits 1. Restoring is one command: clone the bundle.

A decision log instead of a memory

Every decision goes into a markdown table: ID, evidence, decision, status. It's past 900 rows. The ones that went wrong stay in, with what was learned.

It's grepped by ID, never read whole. That matters for the agents too: they read the same record, so they don't propose something I already rejected last week. A daily brief, HOY.md, is composed by a script from the gates, the backups and the queue. No AI writes it by hand, and every number in it says where it came from.

llms.txt and IndexNow, kept in sync by scripts

llms.txt was written by hand once, and a few weeks later an audit found it listed 7 of 12 pieces because nobody remembered to add the new ones. Now ct-llms.py regenerates it from the sitemap. It keeps the hand-written sections intact, rewrites only the Academy and Blog lists in sitemap order, keeps the curated line for any piece already there, and builds new lines from each page's title and meta description. A check mode exits 1 if the live file is missing anything, and that check is one of the daily gates.

For IndexNow, the problem was that Squarespace won't let you put a file at the domain root, and that's where the key has to live. The workaround: upload the key as a site file and add a URL mapping that redirects the root path to it. ct-indexnow.py fetches the key from the root before every ping and refuses to send anything if it doesn't match, then notifies the IndexNow endpoints and reports which ones accepted.

Agents split by function

I work with AI agents, and I split them the way you'd split a team. Several do research. One evaluates what all the others produce. One documents. One keeps improving the system itself. Others work on branding, others on code. Some days 22 run at once.

What they share is the system above: the same site.json, the same gates, the same decision log. The AI executes. The judgment stays with me.

What I'd tell anyone running a site alone

Write the decision down before you build. Put every list in one file. Make each bug you find into a check that runs forever. Compare against yesterday, not only against the rule. And never call anything saved until a cold reload says so.

This is what I build for businesses: the site, the measurement, the content and the automation behind it, end to end, with a strategy and a plan.

Bring me a digital problem.

Top comments (0)