{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreibqxjm7xckqkuciykf47kdb5hy3rongr45vykr7gav7pkijs5ei6q",
    "uri": "at://did:plc:svkyjirwpd7ts4qgnzoqfcc2/app.bsky.feed.post/3mnhumar4qkjn"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreietduye6i7jln35ihrsv5doghdomgydfvacdzi46nstx77sza6x54"
    },
    "mimeType": "image/png",
    "size": 8382
  },
  "description": "The new wave of cloud AI tools promises convenience, but what about control? We explore a robust local workflow that keeps generation off the cloud, protecting your data and your budget.\"",
  "path": "/2026/05/18/local-hero/",
  "publishedAt": "2026-05-18T09:49:40.000Z",
  "site": "at://did:plc:svkyjirwpd7ts4qgnzoqfcc2/site.standard.publication/3mhpwfentz6lr",
  "tags": [
    "AI",
    "Core",
    "Plugin"
  ],
  "textContent": "This week WordPress 7.0 ships a built-in AI Client – a PHP API for sending prompts to AI providers and getting results back. No custom plumbing, and a standard interface every plugin can use. They are also shipping plugins that connect to specific providers, e.g. AI Provider for Anthropic developed by WordPress themselves. The “no generation in the cloud” principle I wrote a plugin called dgwltd-batch-ai. It does one thing well: takes a post, asks a local AI model to generate something for it, like an excerpt, then ships the answers to production as one-way NDJSON syncs. The local model runs via Ollama — an open-source tool for running LLMs on your own hardware. For this experiment I used gemma4:e4b, an open model from Google. At around 9.6 GB it’s not a small download, but it runs comfortably on a modern machine with enough RAM (mine is 32gb). Generation only ever happens locally. Production only ever imports. The reasons are straightforward: I’d rather use a capable local model for small tasks than reach for a cloud API. I don’t want a model API key sitting on prod. I want to see what’s about to ship before it ships. And I don’t want a $100 usage bill because I accidentally ran a full excerpt batch against a thousand posts instead of ten. Local generation also means no GPU somewhere burning energy on my behalf. My local box runs ollama on bare metal; prod runs the importer. That’s it. Connection refused The model lives behind dgwltd-ollama-model-provider, a fork of Jonathan Bossenger’s wp-ollama-model-provider. Firstly to strip the Ollama Cloud code paths because the dgwltd architecture is local-only. Secondly, to add a configurable base URL after localhost:11434 stopped working from inside DDEV, my local setup for building WordPress sites. The container’s loopback isn’t the host’s loopback. From inside a DDEV container, localhost:11434 resolves to the container itself (nothing listening there), not the Mac where ollama serve is actually running. http://host.docker.internal:11434 is the DNS name Docker exposes for the host. Set it as an option at Settings → Ollama AI Models, or override per environment via the wp_ai_client_ollama_base_url filter. The filter wins over the option, which is what you want for mu-plugin per-site overrides. This is the boring kind of gotcha you only find after wp dgwltd-batch run --task=excerpt errors with “connection refused” and you spend ten minutes wondering whether Ollama crashed. One task, one post To prove the flow I picked a single post that needed an excerpt. This post. Empty excerpt. Good candidate. ddev wp dgwltd-batch run --task=excerpt --ids=1064 A single round-trip to gemma4 running locally. The result: The new wave of cloud AI tools promises convenience, but what about control? We explore a robust local workflow that keeps generation off the cloud, protecting your data and your budget. Two sentences, no marketing voice, on-topic. The model read the body and picked the framing it thought was most likely to make someone click. Whether it’s “right” is a different question. For this POC the question is can I get it from local to prod safely. NDJSON in 491 bytes The export command writes line-by-line JSON. Line 1 is the envelope; subsequent lines are task results. ddev wp dgwltd-batch export --tasks=excerpt --ids=1064 --output=deploy/excerpt-poc.ndjson The full file: {\"_meta\":{\"plugin\":\"dgwltd-batch-ai\",\"version\":\"0.1.0\",\"source\":\"https://dev.dgw.ltd.ddev.site:8443\",\"exported_at\":\"2026-05-15T14:23:41Z\",\"schema\":2}} {\"post_id\":1064,\"slug\":\"local-hero\",\"post_type\":\"post\",\"task\":\"excerpt\",\"hash\":\"9206087186c08518d8daa617f6db0d04\",\"value\":\"The new wave of cloud AI tools promises convenience, but what about control? We explore a robust local workflow that keeps generation off the cloud, protecting your data and your budget.\",\"ts\":\"2026-05-15T14:23:41Z\"} Two things load-bearing here. The hash is what makes re-imports idempotent. If prod already has the same hash recorded against this destination, the importer skips. The slug + post_type fallback handles ID drift between environments. Post 483 on local doesn’t need to be post 483 on prod. The importer matches by ID first, falls back to slug if the IDs disagree. Ship and import scp deploy/excerpt-poc.ndjson <site-id>@ssh.example.com:excerpt-poc.ndjson ssh <site-id>@ssh.example.com wp dgwltd-batch import --file=excerpt-poc.ndjson --dry-run The dry-run is the whole point. It reports every write it would make without touching anything. If it shows the wrong destination, the wrong post, or no matches at all, abort and fix locally. Cost of pausing to check: nothing. Cost of skipping the check: an editor’s hand-written excerpt, gone. When the dry-run looks right: wp dgwltd-batch import --file=excerpt-poc.ndjson The importer is refuse-on-conflict by default. Destinations with non-empty existing content are skipped. You pass --overwrite if you actually want to clobber. Defaults that protect editorial work, opt-in destruction. Re-running the same import after success is a no-op. The hash matches, nothing changes. That’s what makes deploy failures recoverable. Half-finished sync? Run it again. No special handling needed. The post (excerpt) you’re reading shipped that way. Local model, local review, one scp. No cloud, no bill, no surprises. Beyond excerpts Excerpts were just the proof of concept. The same pipeline now handles SEO meta, taxonomy suggestions, image alt text – all generated locally, all shipped the same way. One pattern, a lot of surface area to cover.",
  "title": "Local hero",
  "updatedAt": "2026-05-23T01:09:13.000Z"
}