<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
 
 <title>DK Development</title>
 <link href="https://dkdevelopment.net/atom.xml" rel="self"/>
 <link href="https://dkdevelopment.net"/>
 <updated>2026-08-31T03:59:40+00:00</updated>
 <id>https://dkdevelopment.net</id>
 <author>
   <name>Damian Karzon</name>
   <email>dkarzon@tboda.com</email>
 </author>

 
 <entry>
   <title>I built my own Coding Agent harness and orchestrator — Part 1</title>
   <link href="https://dkdevelopment.net/building-local-agent-box-pt1//"/>
   <updated>2026-08-20T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/building-local-agent-box-pt1/</id>
   <content type="html">&lt;p&gt;Three things lined up over the past few months and pushed me into building this. Token costs kept creeping upwards the more I leaned on cloud coding agents. Open weight models got genuinely good — good enough that “just run it locally” stopped sounding like a compromise. And underneath both of those, I just wanted to know for myself what’s actually possible when you cut the cloud out entirely, rather than take someone else’s word for it.&lt;/p&gt;

&lt;p&gt;None of those three on their own would’ve been enough to get me off the couch. Together, they were.&lt;/p&gt;

&lt;p&gt;This is part one of a series on &lt;strong&gt;&lt;a href=&quot;https://github.com/dkarzon/localagent-box&quot;&gt;localagent-box&lt;/a&gt;&lt;/strong&gt; — something I built to answer that question. It’s a self-hosted orchestration layer that hooks &lt;a href=&quot;https://opencode.ai&quot;&gt;OpenCode&lt;/a&gt; up to &lt;a href=&quot;https://ollama.com&quot;&gt;Ollama&lt;/a&gt; and GitHub. You give it a repo and a prompt, and it goes off and clones the code, creates a branch, runs an AI coding session with a local model, then commits and pushes the result back to GitHub. All on your own hardware. No cloud APIs, no usage bills.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;the-three-things-that-pushed-me-here&quot;&gt;The three things that pushed me here&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The token bills kept climbing.&lt;/strong&gt; Not one shocking invoice — a slow drift. The more I folded cloud-hosted agents into my day-to-day, the more sessions I was running, and the more each session cost as I asked them to do bigger chunks of work. Nothing that broke the budget, but enough that I started mentally flinching before kicking off a bigger job. That’s not a great place to be if you want to use these tools freely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open weight models stopped being the fallback option.&lt;/strong&gt; A year ago, running a local model for coding felt like a novelty — something you’d do to prove a point, not to get real work done. Models like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qwen2.5-coder&lt;/code&gt; changed that calculus. They’re not frontier-model good, but they’re good enough for a real slice of the work I throw at an agent, and that slice is only growing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And I just wanted to see for myself.&lt;/strong&gt; I could read benchmarks and other people’s takes all day, but the only way to actually know what a 7B model running on my own GPU can and can’t handle unsupervised is to build the harness and let it loose on real repos. This whole project is as much an experiment as it is a tool.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;why-not-just-use-opencode-directly&quot;&gt;Why not just use OpenCode directly?&lt;/h3&gt;

&lt;p&gt;OpenCode is great — it’s a terminal-first AI coding tool. But it’s built for interactive sessions where you’re sitting there at the keyboard. I wanted something I could fire-and-forget: kick off a job via the web UI or an API call, go do something else, and come back to a branch ready to review.&lt;/p&gt;

&lt;p&gt;LocalAgent-box is the orchestration layer in the middle — it talks to OpenCode, handles the git plumbing, and keeps concurrent jobs from stepping on each other.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;how-it-works&quot;&gt;How it works&lt;/h3&gt;

&lt;p&gt;The core flow is straightforward:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Job comes in&lt;/strong&gt; — via the API or the web UI&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Clone the repo&lt;/strong&gt; — fresh workspace, checks out the base branch&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Create a feature branch&lt;/strong&gt; — so the agent has a clean place to work&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Run OpenCode&lt;/strong&gt; — pointed at the workspace, Ollama as the model provider, using whatever prompt you gave it&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Commit and push&lt;/strong&gt; — stages everything, commits with your message, pushes back to GitHub&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s the default “batch” mode — single prompt, single run, done. No back-and-forth, no follow-ups. Just a PR branch waiting for you when it’s finished.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;whats-under-the-hood&quot;&gt;What’s under the hood&lt;/h3&gt;

&lt;p&gt;The server is a Node.js/TypeScript HTTP API that manages the job queue, spawns worker processes for each agent run, and exposes a REST API for creating and monitoring jobs.&lt;/p&gt;

&lt;p&gt;The UI is a React app (Vite + Tailwind) that shows you what’s running, lets you kick off new jobs, and streams the worker logs so you can see what the agent is actually doing.&lt;/p&gt;

&lt;p&gt;The main bits:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://opencode.ai&quot;&gt;OpenCode CLI&lt;/a&gt;&lt;/strong&gt; — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opencode run&lt;/code&gt; does the actual coding work&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://ollama.com&quot;&gt;Ollama&lt;/a&gt;&lt;/strong&gt; — local model server; OpenCode talks to this instead of a cloud API&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;GitHub App&lt;/strong&gt; — generates short-lived installation tokens per repo so workers can read and write to private repos without PATs kicking around everywhere&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://open-codereview.ai/&quot;&gt;Open Code Review&lt;/a&gt;&lt;/strong&gt; - Open source code review harness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is worth a mention. I’ve done this kind of GitHub App integration before so it was the natural choice — the server authenticates as a GitHub App and mints a fresh scoped token for each job rather than sitting on a hardcoded PAT. A bit more setup the first time, but it works cleanly.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;what-a-job-actually-looks-like&quot;&gt;What a job actually looks like&lt;/h3&gt;

&lt;p&gt;Kick off a batch agent via the API:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; POST http://localhost:8081/api/v1/agents &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Authorization: Bearer &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TOKEN&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Content-Type: application/json&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{
    &quot;repoId&quot;: &quot;myorg-my-app&quot;,
    &quot;prompt&quot;: &quot;Add input validation to the login form&quot;,
    &quot;baseBranch&quot;: &quot;main&quot;,
    &quot;agentBranch&quot;: &quot;agent/login-validation&quot;,
    &quot;commitMessage&quot;: &quot;Agent: add input validation to login form&quot;,
    &quot;push&quot;: true
  }&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You get an agent ID back immediately. Then it’s just polling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /api/v1/agents/:id&lt;/code&gt; for status or tailing logs if you want to watch it run. Status goes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;queued&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;running&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;completed&lt;/code&gt; (or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;failed&lt;/code&gt; if something blows up).&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;so-did-it-work&quot;&gt;So, did it work?&lt;/h3&gt;

&lt;p&gt;Once the hardware’s paid for, each agent run on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qwen2.5-coder:7b&lt;/code&gt; costs basically nothing. No token counting, no surprise bills, no throttling at peak hours. That’s the cost side of the equation sorted — but the more interesting answer is how much real work it can actually do.&lt;/p&gt;

&lt;p&gt;Give it something specific and scoped — “add input validation to this form handler” — and it nails it, most of the time. Ask it to “improve the error handling in this service” with no further context and you get something that technically compiles but isn’t what you wanted. It’s a floor lower than GPT-5 or Claude, and you feel that floor the moment your prompt gets vague.&lt;/p&gt;

&lt;p&gt;There’s a bonus I wasn’t chasing but noticed anyway: privacy. Your code never leaves your network. If you’re working on anything sensitive, or just don’t love the idea of your whole codebase passing through a third-party API, running everything locally sidesteps that entirely.&lt;/p&gt;

&lt;p&gt;So the honest answer, a few weeks in: local models won’t replace a frontier model for the hard stuff, but they’ve earned a real spot in the rotation for well-scoped, repetitive work. Which, if I’m being honest about how I spend most of my agent budget, is most of it.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;whats-next-interactive-sessions&quot;&gt;What’s next: interactive sessions&lt;/h3&gt;

&lt;p&gt;Batch mode is great for well-scoped tasks where you know exactly what you want. But sometimes you want to poke at the agent while it’s running — redirect it, ask it to extend something, course-correct before it goes too far down the wrong path.&lt;/p&gt;

&lt;p&gt;Part 2 is about adding &lt;strong&gt;interactive mode&lt;/strong&gt;: instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opencode run&lt;/code&gt; and exit, the worker starts up an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opencode serve&lt;/code&gt; instance and keeps it running. You get a proper chat UI with a live transcript, can send follow-up messages, and hit &lt;strong&gt;Finish&lt;/strong&gt; when you’re happy — which is when the commit and push actually happens.&lt;/p&gt;

&lt;p&gt;The tricky part is that OpenCode’s server API wasn’t built with an orchestration wrapper in mind. There’s a bunch of work mapping its SSE event stream into something the UI can render properly, plus some XDG environment variable gymnastics to make sure concurrent agents don’t step on each other’s sessions.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;More implementation detail in the next post — worker architecture, the GitHub App setup, and diving into interactive sessions. The code is (or will shortly be) open source at &lt;a href=&quot;https://github.com/dkarzon/localagent-box&quot;&gt;github.com/dkarzon/localagent-box&lt;/a&gt;; I’m splitting this into a series to walk through the build, not because it’s still half-finished.&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Building Nomii Without the Builder</title>
   <link href="https://dkdevelopment.net/nomii-from-draftbit-to-cursor-expo//"/>
   <updated>2026-08-17T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/nomii-from-draftbit-to-cursor-expo/</id>
   <content type="html">&lt;p&gt;We’ve been building &lt;strong&gt;&lt;a href=&quot;https://www.nomii.com.au/&quot;&gt;Nomii&lt;/a&gt;&lt;/strong&gt;, a family memory keepsake app for parents, for a while now. How we build it has changed a lot in that time. We started in a low-code app builder. These days it’s React Native in a repo we own, with Cursor and a pile of AI tools doing most of the typing.&lt;/p&gt;

&lt;p&gt;It wasn’t a dramatic break, more a gradual shift. The product got clearer, our needs got more specific, and the tooling around us caught up to the point where the next step felt natural instead of risky.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;why-a-builder-was-the-right-place-to-start&quot;&gt;Why a builder was the right place to start&lt;/h3&gt;

&lt;p&gt;Nomii isn’t a project we can give forty hours a week. It’s something we build in the gaps: evenings, weekends, the occasional stolen hour between day jobs. At that stage, low-code made a lot of sense.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://draftbit.com/&quot;&gt;Draftbit&lt;/a&gt; gave us a visual way to lay out screens, wire up navigation, and iterate on UI without bootstrapping a full React Native project from scratch. For a small team with mobile experience in the room but no one living in that world day to day, that’s a reasonable trade. We weren’t avoiding code so much as avoiding &lt;em&gt;setup&lt;/em&gt;, while we figured out what Nomii actually was.&lt;/p&gt;

&lt;p&gt;And it worked. The first version came together faster than we expected — screens, flows, basic data wiring, all the stuff that usually eats the first week of a mobile project. Draftbit let us focus on the product idea: how parents capture milestones, stories, voice recordings, and the bits of family life that scatter across camera rolls and group chats.&lt;/p&gt;

&lt;p&gt;That early phase is exactly what a builder is good for. Get something in front of people, learn, move on.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;when-the-project-started-asking-for-more&quot;&gt;When the project started asking for more&lt;/h3&gt;

&lt;p&gt;As Nomii grew, so did the gap between what we wanted to build and what felt comfortable inside the builder.&lt;/p&gt;

&lt;p&gt;The product vision got more specific: richer memory types, templates and prompts, multi-user accounts, a legacy access system we’re designing for the long term. The flows got more interconnected too. The UI stopped being a collection of screens and started being a &lt;em&gt;system&lt;/em&gt;, with navigation, state, and media handling that needed to evolve together.&lt;/p&gt;

&lt;p&gt;Around the same time, a few things shifted in our toolchain. &lt;strong&gt;Expo&lt;/strong&gt; had matured to the point where the old mobile setup tax (Xcode rabbit holes, SDK path drama, an evening lost to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pod install&lt;/code&gt;) was mostly gone. &lt;a href=&quot;https://cursor.com/&quot;&gt;Cursor&lt;/a&gt; and AI coding agents had gone from novelty to something we were already using seriously on other projects. &lt;a href=&quot;https://www.pen.dev/&quot;&gt;pen.dev&lt;/a&gt; gave us a way to bring our existing &lt;strong&gt;Figma&lt;/strong&gt; designs into Cursor as implementation artifacts, which was basically the visual handoff we liked about the builder, minus staying inside one.&lt;/p&gt;

&lt;p&gt;Draftbit’s move to v2 was part of that picture too, a reminder that the platform was heading in a different direction, with more emphasis on AI assistance and less on the no-code workflow we’d built the early Nomii prototype in. It wasn’t some villain origin story, it just sharpened a question we’d already been circling: &lt;em&gt;are we still in the phase where a builder is the best fit, or have we moved past it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We had.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;expo-made-the-switch-feel-possible&quot;&gt;Expo made the switch feel possible&lt;/h3&gt;

&lt;p&gt;The thing that kept us on a builder longest wasn’t fear of React Native. It was the &lt;strong&gt;feedback loop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We needed to change something, see it on a phone, show it to the rest of the team, and keep going, without a build pipeline getting in the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expo&lt;/strong&gt; flattened a lot of that. One project template, a dev server, a QR code on your phone, and you’re looking at the app in minutes. Hot reload. Over-the-air updates when we need them. A testing loop that matches how we actually work on Nomii now: implement, run in Expo Go, test on a device, iterate.&lt;/p&gt;

&lt;p&gt;That’s what made the switch possible. The setup cost we’d been paying the builder to absorb was suddenly cheap enough to carry ourselves, especially with AI handling a fair chunk of the boilerplate.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;cursor-agents-and-owning-the-codebase&quot;&gt;Cursor, agents, and owning the codebase&lt;/h3&gt;

&lt;p&gt;Moving off the builder didn’t mean going back to hand-coding every screen like it was 2017. The whole point was still speed. We just wanted speed &lt;strong&gt;with&lt;/strong&gt; source code we could read, diff, refactor, and grow as Nomii gets more ambitious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; is where Nomii gets built now. Same pattern we use on other projects: an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; with conventions, clear scope boundaries, and enough context that the agent isn’t guessing at architecture every session.&lt;/p&gt;

&lt;p&gt;In practice that looks like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Describing a screen or flow in plain language, then iterating on the generated React Native components&lt;/li&gt;
  &lt;li&gt;Using the agent for the tedious glue (navigation wiring, form validation, API client scaffolding) while we stay in the director’s seat on product decisions&lt;/li&gt;
  &lt;li&gt;Fixing the subtle misses in review: state that works on the happy path but breaks on back navigation, accessibility gaps, the UI that’s technically correct but not quite &lt;em&gt;Nomii&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output lives in our repo. We can run it locally, test it in Expo Go, rip out a component that isn’t working, and prompt our way to a better version. As the product gets more complex, that ownership matters more. Builders aren’t bad, they just don’t give you a home you control, and mature products tend to need one.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;from-figma-to-code-via-pendev&quot;&gt;From Figma to code via pen.dev&lt;/h3&gt;

&lt;p&gt;We were already designing Nomii in &lt;strong&gt;Figma&lt;/strong&gt;. That’s where the team iterates on layout, flow, and visual polish. What we didn’t have was a clean way to get those designs in front of Cursor as something the agent could actually work from.&lt;/p&gt;

&lt;p&gt;Describing UI in prose works fine for simple screens. For anything with real visual intent (spacing, hierarchy, component structure) it turns into a game of telephone. You end up reviewing generated code that &lt;em&gt;sort of&lt;/em&gt; matches the design, then spending as long fixing it as you would have spent just building it.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.pen.dev/&quot;&gt;pen.dev&lt;/a&gt; bridged that gap. We drop Figma designs in as artifacts alongside the prompt, and Cursor uses them as reference during implementation. The agent isn’t guessing at what “the memory capture screen” should look like; it can see the design we already agreed on.&lt;/p&gt;

&lt;p&gt;The workflow is design in Figma, reference in Pencil, implement in Cursor, run in Expo, test on a device. Each step has a clear handoff. We’re not asking the AI to be a designer and a developer at the same time — we’re giving it the same brief a human developer would get.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;what-changed-and-what-didnt&quot;&gt;What changed, and what didn’t&lt;/h3&gt;

&lt;p&gt;Different tools, different tradeoffs — not a strict upgrade in every dimension.&lt;/p&gt;

&lt;p&gt;We took on more responsibility. Responsive layouts, component libraries, navigation patterns, media handling — the builder had been absorbing some of that thinking for us. Going native-ish means we own those problems again, which is more work but also more flexibility as Nomii’s feature set grows.&lt;/p&gt;

&lt;p&gt;There’s a learning curve to prompting well. A builder UI is constrained; you can only do what the widgets allow, which is frustrating but also guard-railed. Cursor will happily build exactly what you asked for, including the parts you forgot to mention. Review becomes part of the craft.&lt;/p&gt;

&lt;p&gt;And cost isn’t zero: Cursor subscription, Expo tooling, our time reviewing generated code. But it’s a cost structure that fits where Nomii is now — pay for tools, own the output, no platform ceiling on what we can build next.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;what-wed-tell-ourselves-at-the-start&quot;&gt;What we’d tell ourselves at the start&lt;/h3&gt;

&lt;p&gt;If we were starting from scratch today, knowing what we know now, we’d probably still begin with a builder, or at least something equally fast and visual, and plan to graduate once the product vision solidified. That early phase was worth it. We didn’t waste time wrestling a toolchain; we spent it learning what parents actually need from a memory keepsake app.&lt;/p&gt;

&lt;p&gt;We’d just write the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; earlier when the switch happened. The first week of agent-assisted development involved a lot of re-explaining conventions that should have been captured once. Same lesson as every other AI-native project we’ve worked on: the context doc is the product.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;where-nomii-is-now&quot;&gt;Where Nomii is now&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://www.nomii.com.au/&quot;&gt;Nomii&lt;/a&gt; is still very much in progress — we’re on the waitlist phase while we build out the app. But the build loop feels right for this stage: fast iteration on real devices, a codebase that can grow with the product, and AI doing the parts of mobile development we were happy to delegate anyway.&lt;/p&gt;

&lt;p&gt;Draftbit got us moving when speed mattered most. Expo, Cursor, and Pencil are how we build now that Nomii knows what it wants to be.&lt;/p&gt;

&lt;p&gt;We’ll write more about Nomii itself as we get closer to launch. For now, we’re just enjoying building a mobile app with agents, on our terms, in a repo that’s finally ours.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>ScaleBop: From Vibe to Viable</title>
   <link href="https://dkdevelopment.net/from-vibe-to-viable-scalebop//"/>
   <updated>2026-07-07T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/from-vibe-to-viable-scalebop/</id>
   <content type="html">&lt;p&gt;I’ve been building &lt;strong&gt;ScaleBop&lt;/strong&gt; for a while now, and the more I talk to people who ship with AI coding tools, the clearer the problem gets: we’ve never had it easier to &lt;em&gt;generate&lt;/em&gt; an app, and we’ve never had more confusion about what happens &lt;em&gt;after&lt;/em&gt; the prototype works on localhost.&lt;/p&gt;

&lt;p&gt;Lovable, Bolt, Replit, Claude — pick your tool — they’ll get you a full-stack app in an afternoon. That part of the story is well told. What they don’t tell you is what comes next.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;the-gap-nobody-talks-about&quot;&gt;The gap nobody talks about&lt;/h3&gt;

&lt;p&gt;Hosting models. Which AWS services you actually need at your current scale. Infrastructure-as-code when you’ve never written infrastructure. CI/CD, secrets, environments — without copying a tutorial that doesn’t match your repo. Whether the generated code is safe to expose to the internet.&lt;/p&gt;

&lt;p&gt;That’s the gap between &lt;em&gt;“I have an app”&lt;/em&gt; and &lt;em&gt;“I have a production system”&lt;/em&gt;. Projects stall there. Founders burn weeks in AWS documentation rabbit holes. Agencies quietly eat margin doing work the AI tools implied was already done.&lt;/p&gt;

&lt;p&gt;ScaleBop is my attempt to close that gap — not by becoming another black-box host, but by meeting you at the codebase and giving you a credible first pass at architecture, infrastructure, and delivery that &lt;strong&gt;you own&lt;/strong&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;what-scalebop-does&quot;&gt;What ScaleBop does&lt;/h3&gt;

&lt;p&gt;The tagline is &lt;strong&gt;from vibe to viable&lt;/strong&gt;. In practice, that’s a short, repeatable workflow:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Import your project&lt;/strong&gt; — connect a GitHub repo, upload a ZIP, or point at an AI-generated project.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Detect the stack&lt;/strong&gt; — frameworks, languages, build tools, databases, deployment patterns. ScaleBop reads what’s actually in the repo, not what you think you built.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Synthesize an architecture&lt;/strong&gt; — a rule-based AWS deployment blueprint tuned for cost, scalability, and simplicity. Next.js maps differently than Express; a SQLite prototype gets different guidance than Postgres.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Generate delivery assets&lt;/strong&gt; — starter AWS CDK scaffolding and GitHub Actions workflows oriented toward OIDC and environment promotion. You merge these into your repo; ScaleBop doesn’t silently deploy into your account without you in the loop.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scan the source&lt;/strong&gt; — a static security pass on the same snapshot, so you can triage risky patterns before you wire things up to the public internet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output isn’t magic. It’s scaffolding — a serious starting point you can review, adapt, and grow. That’s intentional. Whether you’re an enterprise team or an indie founder, you need &lt;strong&gt;artifacts you control&lt;/strong&gt;, not another platform that holds the keys.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;the-education-problem&quot;&gt;The education problem&lt;/h3&gt;

&lt;p&gt;Here’s the part I find most interesting, and the reason ScaleBop isn’t just a codegen tool.&lt;/p&gt;

&lt;p&gt;A growing audience of &lt;strong&gt;non-technical builders&lt;/strong&gt; — founders, operators, designers, product people — are using AI to create real software. They can describe what they want, iterate on UI and flows, and get surprisingly far. Then someone mentions Lambda, OIDC, or a VPC, and the conversation turns opaque. They either pretend to understand, pay someone without context, or abandon the project.&lt;/p&gt;

&lt;p&gt;We can fix that — not by turning everyone into a cloud architect, but by &lt;strong&gt;translating&lt;/strong&gt; as we go.&lt;/p&gt;

&lt;p&gt;Every architecture recommendation comes with &lt;strong&gt;rationale&lt;/strong&gt;: why this backend shape, why this database choice, what tradeoff you’re making. AWS service names in the blueprint get paired with plain-language explainers. What is CloudFront? Why would a Next.js app use Lambda@Edge versus a container? What does OIDC mean in a GitHub Actions workflow, and why should you care? &lt;strong&gt;Next steps&lt;/strong&gt; land as a checklist, not a wall of jargon — connect AWS, bootstrap CDK, open a PR with generated files, run the pipeline.&lt;/p&gt;

&lt;p&gt;You already have a codebase you care about. ScaleBop connects each infrastructure decision back to &lt;em&gt;your&lt;/em&gt; stack. That’s a much better teaching surface than a generic AWS course.&lt;/p&gt;

&lt;p&gt;If you’re a non-technical builder: you don’t need to master every service. You need enough literacy to ask good questions, spot when something sounds wrong, and collaborate with a developer or consultant without flying blind.&lt;/p&gt;

&lt;p&gt;If you’re a developer: you still get speed. The blueprint and generated IaC are starting points, not gospel. But your stakeholder can finally see &lt;em&gt;why&lt;/em&gt; the architecture looks the way it does.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;problems-i-set-out-to-solve&quot;&gt;Problems I set out to solve&lt;/h3&gt;

&lt;p&gt;When I started, I wrote down a few problems I wanted ScaleBop to address directly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;“I have code but no cloud plan”&lt;/strong&gt; — stack detection plus a deployment blueprint from the actual repo&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“AWS is powerful and intimidating”&lt;/strong&gt; — opinionated defaults with explanations, not a blank console&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“I don’t know what IaC to write”&lt;/strong&gt; — generated CDK bundle matched to detected deployables&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“CI/CD feels like a separate project”&lt;/strong&gt; — GitHub Actions workflow with OIDC-oriented setup docs&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“AI-generated code might be unsafe”&lt;/strong&gt; — advisory security scan on import, visible alongside analysis&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“I want to own my infra, not rent a black box”&lt;/strong&gt; — downloadable artifacts; customer-owned AWS account; no hidden deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What ScaleBop is &lt;strong&gt;not&lt;/strong&gt;, at least not today: a fully managed Vercel-style platform that runs your production for you. That’s a deliberate boundary. Trust, security review, and operational ownership matter — especially for anyone learning their way into this space.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;how-i-built-it&quot;&gt;How I built it&lt;/h3&gt;

&lt;p&gt;ScaleBop is a TypeScript monorepo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js&lt;/strong&gt; handles the web app — import flows, architecture views, IaC and pipeline previews, billing, admin. The API is &lt;strong&gt;Node.js&lt;/strong&gt; — raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node:http&lt;/code&gt; locally, the same handlers packaged as &lt;strong&gt;AWS Lambda&lt;/strong&gt; in production. No Express, no framework magic; one handler shape everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; for application data; &lt;strong&gt;S3&lt;/strong&gt; for code snapshots at scale. The real business logic lives in &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;packages/core&lt;/code&gt;&lt;/strong&gt;: analyzer, blueprint synthesis, IaC and pipeline generation, security findings normalisation. The API and web app stay thin. &lt;strong&gt;AWS CDK&lt;/strong&gt; defines the ScaleBop platform itself — API Gateway, Lambda, RDS, buckets, secrets.&lt;/p&gt;

&lt;p&gt;The analyzer and blueprint engine are &lt;strong&gt;deterministic&lt;/strong&gt; today — heuristics and rules, not an LLM guessing your architecture. That was a conscious MVP choice. Predictable output you can debug beats impressive-sounding hallucinations when someone is trying to learn.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;the-tools-that-made-it-possible&quot;&gt;The tools that made it possible&lt;/h3&gt;

&lt;p&gt;Building a product like this solo — or nearly solo — would have been unrealistic a few years ago. Three tools shaped how I work.&lt;/p&gt;

&lt;h4 id=&quot;cursor&quot;&gt;Cursor&lt;/h4&gt;

&lt;p&gt;Cursor is where almost all of the code gets written. Not as a replacement for thinking — as a force multiplier for implementation.&lt;/p&gt;

&lt;p&gt;The ScaleBop codebase has strict conventions: shared types in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;packages/core&lt;/code&gt;, handlers in domain folders, SQL in query modules, Zod validation on request bodies, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;console.log&lt;/code&gt; in the API. Cursor helps me move fast &lt;strong&gt;within&lt;/strong&gt; those guardrails — refactoring across workspaces, extending the blueprint rules, writing migrations, keeping tests aligned.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; file in the repo is effectively a briefing doc for the AI: stack, patterns, things not to do. That meta-layer — human rules plus AI execution — is how a single builder can maintain a monorepo spanning web, API, core logic, and CDK without drowning in context switching.&lt;/p&gt;

&lt;p&gt;Honest take: Cursor doesn’t remove the hard product decisions. It removes friction on the decisions already made.&lt;/p&gt;

&lt;h4 id=&quot;linear&quot;&gt;Linear&lt;/h4&gt;

&lt;p&gt;Linear is the source of truth for &lt;em&gt;what&lt;/em&gt; to build next. Issues are tracked as units of work — Customer-owned AWS connections, security scan UX, CDK bootstrap automation, billing paths, custom domains, and dozens of vertical slices in between.&lt;/p&gt;

&lt;p&gt;Why Linear and not just GitHub Issues? Speed and clarity. Linear’s workflow matches how I think about product work: a crisp issue, a plan doc when scope is fuzzy (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docs/plans/&lt;/code&gt; often links back to a Linear ticket), ship, close. For a builder wearing product, engineering, and ops hats, that lightweight structure keeps momentum without enterprise ceremony.&lt;/p&gt;

&lt;p&gt;When something breaks in production or a user report comes in, it becomes a ticket. When an idea needs exploration — AWS reseller models, deployment architecture tradeoffs — it gets a plan doc &lt;em&gt;and&lt;/em&gt; a Linear issue so it doesn’t live only in my head.&lt;/p&gt;

&lt;h4 id=&quot;github&quot;&gt;GitHub&lt;/h4&gt;

&lt;p&gt;GitHub shows up twice: as &lt;strong&gt;infrastructure for ScaleBop the product&lt;/strong&gt;, and as &lt;strong&gt;part of what ScaleBop generates for customers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the product: source control, code review, GitHub OAuth for repo imports, GitHub App flows for listing repositories and opening PRs with generated IaC and pipeline files, CI workflows on push and pull request.&lt;/p&gt;

&lt;p&gt;For customers: ScaleBop generates &lt;strong&gt;GitHub Actions&lt;/strong&gt; workflows designed around &lt;strong&gt;OIDC&lt;/strong&gt; — short-lived credentials, no long-lived AWS keys in the repo. That’s the modern standard, and it’s exactly the kind of thing non-technical builders never know they need until something goes wrong.&lt;/p&gt;

&lt;p&gt;Your code lives here, your pipeline runs here, your AWS account stays yours.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;what-ive-learned-so-far&quot;&gt;What I’ve learned so far&lt;/h3&gt;

&lt;p&gt;The prototype economy is real. AI app generation isn’t a fad; it’s a new on-ramp to software. The downstream tooling lag is the opportunity.&lt;/p&gt;

&lt;p&gt;Ownership beats convenience for the first deploy. People will tolerate more steps if they understand them and retain control. Black-box deploy is a later-stage feature, not an MVP wedge.&lt;/p&gt;

&lt;p&gt;Education is a feature, not a blog sidebar. Explainers in the UI convert better than explainers in a PDF. Builders stay when they understand &lt;em&gt;their&lt;/em&gt; architecture.&lt;/p&gt;

&lt;p&gt;Deterministic beats flashy for trust. A rule-based blueprint you can trace beats an LLM essay that sounds confident and wrong.&lt;/p&gt;

&lt;p&gt;Solo building is a workflow problem. Cursor plus Linear plus GitHub isn’t hype — it’s an operating system for one person shipping a credible SaaS.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;where-it-goes-next&quot;&gt;Where it goes next&lt;/h3&gt;

&lt;p&gt;ScaleBop today delivers the core loop: ingest, analyze, blueprint, generate, scan. The roadmap includes deeper AWS connection flows, richer cost visibility, better security findings UX, and continued work on making the “first deploy” path approachable without crossing the trust boundary.&lt;/p&gt;

&lt;p&gt;If you’re building with AI and stuck between “it works on my machine” and “it runs in prod,” that’s the problem space I’m obsessed with. &lt;a href=&quot;https://scalebop.com&quot;&gt;ScaleBop&lt;/a&gt; is my answer so far — free import and stack detection on the starter tier, enough to see if it understands your app before you commit to anything.&lt;/p&gt;

&lt;p&gt;I’m still figuring out what parts of the journey feel most opaque. If you’ve hit that wall, I’d like to hear about it.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>The Rise of the Engineering Agent Manager</title>
   <link href="https://dkdevelopment.net/rise-of-engineering-agent-manager//"/>
   <updated>2026-04-07T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/rise-of-engineering-agent-manager/</id>
   <content type="html">&lt;p&gt;There’s a conversation happening in every engineering org right now, whether it’s out loud or just quietly in people’s heads: &lt;em&gt;what exactly is my job now?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a long time, the path was well understood. Junior developers write code, senior developers write better code and review it, tech leads set patterns and mentor, and engineering managers coordinate people and delivery. Everyone had a lane. AI has blown that model up, not by replacing people, but by fundamentally changing where the value is.&lt;/p&gt;

&lt;p&gt;The engineers and leaders who are thriving right now aren’t the ones who are best at writing code. They’re the ones who are best at directing, reviewing, and shaping the work that AI agents are producing.&lt;/p&gt;

&lt;h3 id=&quot;from-producer-to-director&quot;&gt;From Producer to Director&lt;/h3&gt;

&lt;p&gt;Here’s the mental shift that took me a while to internalise: your job is no longer to be the best implementer. It’s to be the best decision-maker about what gets implemented and why.&lt;/p&gt;

&lt;p&gt;Think of it like the difference between being a craftsman and being an architect. A craftsman’s value is in their ability to produce the work with their hands. An architect’s value is in their ability to understand the brief, envision the outcome, and communicate that clearly enough for others (or now, agents) to execute it faithfully.&lt;/p&gt;

&lt;p&gt;The coding work itself hasn’t disappeared. There’s more of it than ever. But the constraint has shifted from &lt;em&gt;how fast can humans write code&lt;/em&gt; to &lt;em&gt;how well can humans define, review, and guide what agents produce&lt;/em&gt;. That changes everything about what a strong engineering leader looks like.&lt;/p&gt;

&lt;h3 id=&quot;architecture-strategy-vision-now-the-whole-job&quot;&gt;Architecture, Strategy, Vision: Now the Whole Job&lt;/h3&gt;

&lt;p&gt;When an agent can scaffold a feature in minutes, the bottleneck becomes the quality of what you gave it to work from. Vague requirements produce vague code. Poorly considered architecture decisions, when handed to an agent that will faithfully implement them at scale, produce large amounts of faithfully bad code.&lt;/p&gt;

&lt;p&gt;This is where Engineering Managers and tech leads are finding renewed purpose. Spending time deeply on:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;System design and architecture&lt;/strong&gt;: The decisions that are hard to reverse, that constrain everything downstream, that require experience and judgment.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Product strategy&lt;/strong&gt;: What should we actually be building? What’s the simplest version of this that delivers value?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Technical vision&lt;/strong&gt;: Where is the codebase going in 6 months? What patterns should agents be following? What should they never do?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Context documents&lt;/strong&gt;: More on this below, but writing the documentation that puts agents in the right headspace is now a core leadership skill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engineers who are leaning into this are finding that their impact is multiplied. One person with a clear vision and a well-configured agent workflow can ship what used to take a team.&lt;/p&gt;

&lt;h3 id=&quot;controlling-agents-through-context&quot;&gt;Controlling Agents Through Context&lt;/h3&gt;

&lt;p&gt;One of the more interesting things about working with AI coding agents (tools like GitHub Copilot, Cursor, and others) is that they’re heavily influenced by the context you give them. This has spawned a new type of document that’s becoming a standard part of engineering workflows: the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; file, custom instruction files, and project-scoped prompt documents.&lt;/p&gt;

&lt;p&gt;These documents are essentially your communication layer with your agents. A well-written &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; might define:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The coding style and conventions for the project&lt;/li&gt;
  &lt;li&gt;Which patterns to use and which to avoid&lt;/li&gt;
  &lt;li&gt;How to structure components, name things, and organise files&lt;/li&gt;
  &lt;li&gt;Testing expectations&lt;/li&gt;
  &lt;li&gt;Scope boundaries: what the agent should and shouldn’t touch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tech leads who are writing these documents well are discovering something important: it forces you to articulate things that usually live only in your head. The conventions that you’d catch in a code review but never wrote down anywhere. The architectural decisions that are obvious to you but invisible to a new team member, or an agent.&lt;/p&gt;

&lt;p&gt;There’s also a feedback loop here. When an agent produces something that misses the mark, the right response isn’t just to fix the output. It’s to update your context documents so the agent gets it right next time. This is a fundamentally different kind of engineering leadership work, and it’s a skill that takes practice.&lt;/p&gt;

&lt;h3 id=&quot;linear-and-the-agent-friendly-backlog&quot;&gt;Linear and the Agent-Friendly Backlog&lt;/h3&gt;

&lt;p&gt;Project management tooling is catching up fast. Linear has become the tool of choice for a lot of engineering teams, and it’s easy to see why when you start working in an AI-native way.&lt;/p&gt;

&lt;p&gt;Where a traditional Jira ticket might be a title and two sentences, Linear has nudged teams toward writing richer, more structured issue descriptions. And now with AI agents that can be pointed at a Linear ticket to execute on, the quality of that ticket has a direct impact on the quality of what gets built.&lt;/p&gt;

&lt;p&gt;The mental model is shifting from &lt;em&gt;assign this to a developer&lt;/em&gt; to &lt;em&gt;can an agent execute on this ticket as written?&lt;/em&gt; If the answer is no (too vague, missing acceptance criteria, or lacking technical context), that’s a signal the issue needs more work before it goes anywhere.&lt;/p&gt;

&lt;p&gt;For engineering managers and leads, this creates a new responsibility around backlog hygiene. Grooming a ticket used to be about making sure a human developer had enough information to get started. Now it’s about making sure that information is structured clearly enough that an agent can run with it autonomously, with a human reviewing the output rather than doing the work line by line.&lt;/p&gt;

&lt;p&gt;Some teams are even going further, using agents to draft tickets from higher-level briefs, then having leads review and refine those before they’re executed. The human judgment is still in the loop, just at a different stage.&lt;/p&gt;

&lt;h3 id=&quot;the-review-layer-is-the-new-craft&quot;&gt;The Review Layer Is the New Craft&lt;/h3&gt;

&lt;p&gt;If you’re an engineering leader and you’re worried that AI is devaluing your expertise, I’d push back on that. What’s changing is &lt;em&gt;where&lt;/em&gt; you apply it.&lt;/p&gt;

&lt;p&gt;The code review is becoming more important, not less. When agents are generating code, the review is where human expertise catches the subtle misalignments: the technically correct implementation that misses the intent, the approach that works for this feature but creates problems for the next one, the security concern that the agent didn’t have enough context to see coming.&lt;/p&gt;

&lt;p&gt;This requires:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Deep familiarity with the codebase and its constraints&lt;/li&gt;
  &lt;li&gt;Clear communication about &lt;em&gt;why&lt;/em&gt; something isn’t right, not just that it isn’t&lt;/li&gt;
  &lt;li&gt;The ability to update context documents so the same mistake doesn’t happen again&lt;/li&gt;
  &lt;li&gt;Trust in your own judgment about when to approve and when to push back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the skills that separate a great engineering leader from a mediocre one, and they haven’t changed. They’ve just been relocated to a new part of the workflow.&lt;/p&gt;

&lt;h3 id=&quot;the-leaders-who-are-struggling&quot;&gt;The Leaders Who Are Struggling&lt;/h3&gt;

&lt;p&gt;It’s also worth being honest about where the friction is. Not everyone is finding this transition easy.&lt;/p&gt;

&lt;p&gt;The engineers who built their identity around being great implementers — who took pride in hammering out clean code quickly — are having to find a new source of professional satisfaction. That’s genuinely hard. The craft is real and it matters, and being asked to step back from it to direct others (or agents) doing it can feel like a demotion even when it isn’t.&lt;/p&gt;

&lt;p&gt;There’s also a learning curve to working effectively with agents that a lot of people underestimate. It’s not as simple as asking a question and accepting whatever comes back. It requires developing intuition for when to trust the output, when to scrutinise it, and how to give feedback that actually improves future results.&lt;/p&gt;

&lt;p&gt;The leaders who are investing time in building this intuition are pulling ahead. The ones who are treating AI tools as a productivity shortcut but not changing how they think about their role are finding the benefits much more limited.&lt;/p&gt;

&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;

&lt;p&gt;If you’re an Engineering Manager or tech lead who’s still finding your footing with this shift, a few practical starting points:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Write an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; for one of your projects.&lt;/strong&gt; Even if it’s imperfect, the act of writing down your conventions is valuable regardless of agents.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Try completing a full feature with an agent&lt;/strong&gt;, staying in the director seat. Define the work clearly, review what it produces, iterate through feedback. Notice where you struggle to articulate what you want.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Audit your Linear (or equivalent) backlog.&lt;/strong&gt; Would an agent have enough context to work from these tickets? Where are the gaps?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Share the experience with your team.&lt;/strong&gt; The best teams right now are learning together, sharing prompt patterns that work, updating shared context docs based on what agents get wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The shift is real and it’s happening fast. The good news is that the skills that make a great engineering leader (clear thinking, good judgment, strong communication, technical depth) are exactly what this new way of working demands. They just need to be applied in new places.&lt;/p&gt;

&lt;p&gt;The Engineering Agent Manager is already here. The question is whether you’re ready to step into that role.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Using Schema.org structured data to improve SEO</title>
   <link href="https://dkdevelopment.net/seo-structured-data-2025//"/>
   <updated>2025-12-03T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/seo-structured-data-2025/</id>
   <content type="html">&lt;p&gt;Search engine optimization has long been a cornerstone of the web, continually evolving as search engines and open‑web initiatives shape how information is discovered.&lt;/p&gt;

&lt;p&gt;Today, the rise of AI tools and AI‑powered search is accelerating that evolution, making structured data more critical than ever. In this post, I’ll explore how adding schema.org markup can enrich your content, improve visibility across both traditional search and AI‑driven platforms, and ultimately create a better experience for users.&lt;/p&gt;

&lt;blockquote&gt;
    Schema.org is a set of extensible schemas that enables webmasters to embed structured data on their web pages for use by search engines and other applications. For more details, see the &lt;a href=&quot;https://schema.org&quot;&gt;homepage&lt;/a&gt;.
&lt;/blockquote&gt;

&lt;h4 id=&quot;why-use-structured-data&quot;&gt;Why Use Structured Data?&lt;/h4&gt;

&lt;p&gt;Adding structured data to your website provides several benefits:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Rich Search Results&lt;/strong&gt;: Your content can appear with enhanced visuals in search results, including star ratings, images, prices, and availability information&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Better Click-Through Rates&lt;/strong&gt;: Rich snippets stand out in search results, leading to higher engagement&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Voice Search Optimization&lt;/strong&gt;: Search assistants use structured data to provide spoken answers&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Knowledge Graph Integration&lt;/strong&gt;: Your content can be featured in Google’s Knowledge Panel&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AI Agent Compatibility&lt;/strong&gt;: Modern AI systems can better understand and utilise your content (more on this below)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;how-to-implement-structured-data&quot;&gt;How to Implement Structured Data&lt;/h4&gt;

&lt;p&gt;There are 3 main ways to include structured data in a website’s markup:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Microdata&lt;/li&gt;
  &lt;li&gt;JSON-LD&lt;/li&gt;
  &lt;li&gt;RDFa&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Microdata&lt;/strong&gt; adds additional attributes to HTML elements to identify the data on the page. The data is embedded directly within your existing HTML.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;article&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemscope&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemtype=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://schema.org/BlogPosting&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemprop=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;headline&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;My Blog Post Title&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemprop=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;author&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemscope&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemtype=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://schema.org/Person&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemprop=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Damian Karzon&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;time&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemprop=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;datePublished&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;datetime=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;2025-12-04&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;December 4, 2025&lt;span class=&quot;nt&quot;&gt;&amp;lt;/time&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;itemprop=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;articleBody&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The content of the blog post goes here...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;JSON-LD&lt;/strong&gt; (JavaScript Object Notation for Linked Data) adds a JSON block to the page where the structured data is output in a single location. This is Google’s recommended format as it’s easier to implement and maintain.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;application/ld+json&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@context&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://schema.org&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;BlogPosting&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;headline&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;My Blog Post Title&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Damian Karzon&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;datePublished&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;2025-12-04&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;dateModified&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;2025-12-04&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://example.com/images/post-banner.jpg&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;publisher&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Organization&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;DK Development&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;logo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ImageObject&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://example.com/logo.png&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;RDFa&lt;/strong&gt; (Resource Description Framework in Attributes) like Microdata, adds additional attributes to HTML elements to identify the data on the page. It’s more verbose but offers greater flexibility.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;article&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;vocab=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://schema.org/&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;typeof=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;BlogPosting&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;property=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;headline&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;My Blog Post Title&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;property=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;author&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;typeof=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Person&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;property=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Damian Karzon&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;time&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;property=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;datePublished&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;datetime=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;2025-12-04&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;December 4, 2025&lt;span class=&quot;nt&quot;&gt;&amp;lt;/time&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;property=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;articleBody&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The content of the blog post goes here...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;which-format-should-you-use&quot;&gt;Which Format Should You Use?&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;JSON-LD is the recommended approach&lt;/strong&gt; for most implementations because:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;It’s easier to add and maintain (no need to modify existing HTML)&lt;/li&gt;
  &lt;li&gt;It keeps your markup cleaner&lt;/li&gt;
  &lt;li&gt;It’s Google’s preferred format&lt;/li&gt;
  &lt;li&gt;It can be dynamically generated server-side or via JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;popular-schema-types&quot;&gt;Popular Schema Types&lt;/h4&gt;

&lt;p&gt;A comprehensive list of schema types can be found at &lt;a href=&quot;https://schema.org/docs/schemas.html&quot;&gt;schema.org/docs/schemas.html&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;The vocabulary currently consists of 792 Types, 1447 Properties, 15 Datatypes, 83 Enumerations and 445 Enumeration members.&lt;/blockquote&gt;

&lt;p&gt;Here are some of the most commonly used schema types:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Schema Type&lt;/th&gt;
      &lt;th&gt;Use Case&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Article/BlogPosting&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Blog posts and news articles&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Product&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;E-commerce product pages&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;LocalBusiness&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Physical business locations&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Organization&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Company information&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Person&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Individual profiles&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Recipe&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Cooking recipes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Event&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Upcoming events&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;FAQPage&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Frequently asked questions&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;HowTo&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Step-by-step guides&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Review&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Product or service reviews&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;BreadcrumbList&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Site navigation breadcrumbs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;VideoObject&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Video content&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;ai-agents-and-structured-data&quot;&gt;AI Agents and Structured Data&lt;/h4&gt;

&lt;p&gt;With the rise of AI assistants like ChatGPT, Claude, Gemini, and others, structured data has taken on a new dimension of importance. Here’s how AI agents interact with schema.org markup:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enhanced Content Understanding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI agents parse structured data to better understand the context and meaning of web content. When an AI encounters a page with proper schema markup, it can:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Accurately identify the type of content (article, product, recipe, etc.)&lt;/li&gt;
  &lt;li&gt;Extract key information like authors, dates, prices, and ratings&lt;/li&gt;
  &lt;li&gt;Understand relationships between entities on the page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI-Powered Search and Assistants&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many AI systems now power search experiences and virtual assistants:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt;: AI systems that search and synthesise information benefit from structured data to provide accurate, well-attributed responses&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Voice Assistants&lt;/strong&gt;: Siri, Alexa, and Google Assistant use structured data to answer questions directly&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AI Search Engines&lt;/strong&gt;: Perplexity, Bing Copilot, and Google’s AI Overviews leverage schema markup to generate accurate summaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Agentic Web Browsing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As AI agents become more autonomous in browsing and interacting with websites:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Structured data helps agents understand page purpose and available actions&lt;/li&gt;
  &lt;li&gt;Schema types like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Action&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntryPoint&lt;/code&gt; can indicate how to interact with a service&lt;/li&gt;
  &lt;li&gt;Well-marked content is more likely to be correctly interpreted and cited&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best Practices for AI Compatibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To optimise your structured data for AI agents:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Be comprehensive&lt;/strong&gt;: Include as many relevant properties as possible&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Use accurate data&lt;/strong&gt;: AI systems may cross-reference your claims&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sameAs&lt;/code&gt; properties&lt;/strong&gt;: Link to authoritative sources (Wikipedia, social profiles)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;speakable&lt;/code&gt; schema&lt;/strong&gt;: Indicates content suitable for text-to-speech&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Implement &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WebPage&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WebSite&lt;/code&gt; schemas&lt;/strong&gt;: Helps AI understand your site structure&lt;/li&gt;
&lt;/ol&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;application/ld+json&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@context&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://schema.org&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;WebSite&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;DK Development&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://dkdevelopment.net&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;potentialAction&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;SearchAction&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://dkdevelopment.net/search?q={search_term_string}&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;query-input&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;required name=search_term_string&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;testing-your-structured-data&quot;&gt;Testing Your Structured Data&lt;/h4&gt;

&lt;p&gt;Before deploying structured data to production, validate it using these tools:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://search.google.com/test/rich-results&quot;&gt;Google Rich Results Test&lt;/a&gt;&lt;/strong&gt;: Tests if your page is eligible for rich results&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://validator.schema.org/&quot;&gt;Schema.org Validator&lt;/a&gt;&lt;/strong&gt;: Validates your markup against the schema.org specification&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.google.com/webmasters/markup-helper/&quot;&gt;Google Structured Data Markup Helper&lt;/a&gt;&lt;/strong&gt;: Helps generate markup for your pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;a-real-world-blog-example&quot;&gt;A Real-World Blog Example&lt;/h4&gt;

&lt;p&gt;Here’s how I’ve implemented structured data on this very blog using JSON-LD:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;application/ld+json&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@context&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://schema.org&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;BlogPosting&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;mainEntityOfPage&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;WebPage&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://dkdevelopment.net/seo-structured-data-2025/&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;headline&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Using Schema.org structured data to improve SEO&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;How to add Schema.org markup to your websites to improve users search experience.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://dkdevelopment.net/img/Banners/book.jpg&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Damian Karzon&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://dkdevelopment.net&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;publisher&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Organization&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;DK Development&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;logo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ImageObject&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://dkdevelopment.net/img/favicon.ico&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;datePublished&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;2025-12-04&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;dateModified&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;2025-12-04&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4 id=&quot;recap&quot;&gt;Recap&lt;/h4&gt;

&lt;p&gt;Structured data is no longer just about improving traditional search results—it’s becoming essential infrastructure for how AI systems understand and interact with web content. By implementing schema.org markup:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Choose JSON-LD&lt;/strong&gt; as your primary format for ease of implementation&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Start with the basics&lt;/strong&gt;: Add Article/BlogPosting schema to your blog posts&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Expand to business schemas&lt;/strong&gt;: Organization, LocalBusiness, and Person for your about pages&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test thoroughly&lt;/strong&gt; before deployment using Google’s validation tools&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Think about AI&lt;/strong&gt;: Your structured data helps AI agents accurately represent your content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As AI continues to transform how users discover and consume web content, well-structured data will become a competitive advantage—not just for SEO, but for ensuring your content is accurately understood and represented across an increasingly AI-mediated web.&lt;/p&gt;

&lt;h4 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://schema.org/Blog&quot;&gt;Schema.org Blog Type&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/docs/advanced/structured-data/article&quot;&gt;Google’s Article Structured Data Guide&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/philwareham/schema-microdata-examples/blob/main/blog.html&quot;&gt;Schema Microdata Examples on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>My Home Assistant Docker Compose file</title>
   <link href="https://dkdevelopment.net/my-home-assistant-docker-compose//"/>
   <updated>2022-05-19T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/my-home-assistant-docker-compose/</id>
   <content type="html">&lt;p&gt;Over the last few months I have been playing around with a Home Assistant instance in Docker to run all of my home automation and over time I have slowly added more and more to it (more integrations, more services, more devices, etc.).&lt;/p&gt;

&lt;p&gt;Some info about my setup before we get into it.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Host machine running Windows 10 Pro&lt;/li&gt;
  &lt;li&gt;Docker Desktop using WSL2 backend&lt;/li&gt;
  &lt;li&gt;.wslconfig configured with 30GB Ram and 5 Processors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since I don’t run Home Assistant Operating System I don’t have the ability to use the supervisor and install services through that which means I’ve had to setup my own docker containers for additional services.
The containers I have running here are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Home Assistant&lt;/strong&gt; - The standalone Home Assistant Core install&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; - Database storage for Home Assistant recorder (instead of using filesystem)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Node Red&lt;/strong&gt; - Low-code programming tool for wiring up events&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mosquitto&lt;/strong&gt; - An MQTT message broker (to allow devices to use a publish/subscribe model of messaging)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tasmoadmin&lt;/strong&gt; - Administration platform for Tasmota devices (to allow OTA updates of devices without direct internet access)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Chrony&lt;/strong&gt; - A Network Time Protocol (NTP) Server (to allow devices without internet access to synchronise time)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;InfluxDB&lt;/strong&gt; - A Scalable datastore for metrics, events, and real-time analytics (used for long term storage of sensor data)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Grafana&lt;/strong&gt; - Open source data visualisation platform (to create dashboards from the data in InfluxDB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let’s take a look at the docker-compose.yml file:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.1&apos;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;home-assistant&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;home-assistant&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;homeassistant/home-assistant:2022.5.4&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;always&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localnet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/config:/config&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/media:/media&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mosquitto&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;8123:8123&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;1400:1400&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;TZ&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Australia/Brisbane&quot;&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres:12.6&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;always&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localnet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;5432:5432&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;POSTGRES_DB&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;homeassistant&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;POSTGRES_USER&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ha_user&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CrazyStr0ngPa$$word&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;nodered&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nodered&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nodered/node-red&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;always&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localnet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;1880:1880&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/nodered:/data&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;home-assistant&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mosquitto&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;TZ&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Australia/Brisbane&quot;&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;mosquitto&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mosquitto&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;eclipse-mosquitto&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;always&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localnet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;1883:1883&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;1884:1884&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/mosquitto/config:/mosquitto/config&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/mosquitto/data:/mosquitto/data&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/mosquitto/log:/mosquitto/log&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;TZ&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Australia/Brisbane&quot;&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;tasmoadmin&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;tasmoadmin&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ghcr.io/tasmoadmin/tasmoadmin:v1.8.0&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;always&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localnet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;8124:80&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/tasmoadmin:/data/tasmoadmin&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;chrony&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;chrony&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cturra/ntp&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;always&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localnet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;NTP_SERVERS=time.windows.com&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;123:123&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;influxdb&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;influxdb&lt;/span&gt; 
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;influxdb:latest&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;always&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localnet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DOCKER_INFLUXDB_INIT_MODE=setup&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;INFLUXDB_DB=home_assistant&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DOCKER_INFLUXDB_INIT_USERNAME=influx_user&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DOCKER_INFLUXDB_INIT_PASSWORD=CrazyStr0ngPa$$word&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DOCKER_INFLUXDB_INIT_ORG=ha-org&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DOCKER_INFLUXDB_INIT_BUCKET=ha-bucket&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;8086:8086&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/influxdb:/var/lib/influxdb2&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;grafana&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;grafana&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;grafana/grafana:latest&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;always&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localnet&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;influxdb&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;GF_SECURITY_ADMIN_USER=grafana_user&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;GF_SECURITY_ADMIN_PASSWORD=CrazyStr0ngPa$$word&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;8125:3000&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/c/local-docker/home-assistant/grafana:/var/lib/grafana&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;localnet&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bridge&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Let’s start with a few of the common parts to it.&lt;/p&gt;

&lt;h3 id=&quot;commons&quot;&gt;Commons&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;container_name&lt;/strong&gt; - I’ve given all of the containers specific names instead of the default naming of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;folder name&amp;gt;_&amp;lt;service name&amp;gt;_1&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;networks&lt;/strong&gt; - All the containers have been added to a custom network called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localnet&lt;/code&gt; which is also defined at the top level with the driver set to bridge. This is to make sure all of the containers can properly talk to each other and get around some network oddities I was experiencing with my setup.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;restart&lt;/strong&gt; - All of the containers are also set to always restart so whenever docker starts up they will also startup. So if the host is every restarted the containers will come back automatically.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;depends_on&lt;/strong&gt; - I have a few containers that set the depends on to ensure other containers are successfully loaded before them but this isn’t necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now into the containers themselves…&lt;/p&gt;

&lt;h3 id=&quot;home-assistant&quot;&gt;home-assistant&lt;/h3&gt;
&lt;p&gt;The main Home Assistant Core image, here I have given it a specific version so that I can easily see which version I’m using and update when I need to. Using latest here would work as well but may need to make sure if pulls down latest instead of using the existing cached version.&lt;/p&gt;

&lt;p&gt;I also have setup some mapped volumes for this so I can keep my config files in a folder on the Windows host machine. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/c/local-docker/home-assistant/config:/config&lt;/code&gt; This one maps the Windows folder &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C:\local-docker\home-assistant\config&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/config&lt;/code&gt; directory within the container which is where the main config yaml files are kept. This allows me to more easily edit the config files from the host machine as well as persisting the config across rebuilds of the container. The other volume entry is for media which allows me to easily add media to the server for local playback on media players. The TZ environment variable here sets the timezone of the container to make sure it’s running in my local timezone.&lt;/p&gt;

&lt;h3 id=&quot;postgres&quot;&gt;postgres&lt;/h3&gt;
&lt;p&gt;I’ve gone with using a PostgreSQL instance here for the recorder because my home assistant container has it’s config folder as a mounted volume which on Windows has a bit of a performance hit and since I run these folders from my local C Drive I wanted to make sure my home assistant wasn’t slowing anything else running on the host machine too much. The recorder integration also supports MySQL, MariaDB and SQLite (default), more info here: &lt;a href=&quot;https://www.home-assistant.io/integrations/recorder/&quot;&gt;https://www.home-assistant.io/integrations/recorder/&lt;/a&gt;. The container I actually use is in a separate docker-compose file because I use it for other things as well but I included it here for completeness.&lt;/p&gt;

&lt;h3 id=&quot;nodered&quot;&gt;nodered&lt;/h3&gt;
&lt;p&gt;I am not setting a version for this image so it will just use the latest available when it is started (I haven’t had any issues with this so far). To connect the node red instance to home assistant you need to make sure you install the relevant home assistant packages:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;node-red-contrib-home-assistant&lt;/li&gt;
  &lt;li&gt;node-red-contrib-home-assistant-websocket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running node red in docker like this I found that installing packages from the UI didn’t install the latest from npm, because it installed them from the node red flows library (&lt;a href=&quot;https://flows.nodered.org/&quot;&gt;https://flows.nodered.org/&lt;/a&gt;). To install the latest versions here I had to run the npm install commands in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data&lt;/code&gt; directory on the container itself.
This container has a volume setup for the /data directory so that I can backup the config and the flow json files and state is again persisted between rebuilds of the container.&lt;/p&gt;

&lt;h3 id=&quot;mosquitto&quot;&gt;mosquitto&lt;/h3&gt;
&lt;p&gt;Using the eclipse-mosquitto broker for MQTT (latest version works fine here) because it seems to be the best supported one for home assistant. For this one I have 3 different volumes to map the config, data and log directories to local folders, mainly so I can backup the config and ensure state is persistent between container rebuilds.&lt;/p&gt;

&lt;h3 id=&quot;tasmoadmin&quot;&gt;tasmoadmin&lt;/h3&gt;
&lt;p&gt;The image for this is no longer being uploaded to docker hub which is why it is pulling from github packages (ghcr.io/tasmoadmin/tasmoadmin:v1.8.0). The mapped volume here lets me keep a backup of my config and devices as well as helping to persist state between container rebuilds.&lt;/p&gt;

&lt;h3 id=&quot;chrony&quot;&gt;chrony&lt;/h3&gt;
&lt;p&gt;Not too much to configure for this one, there is an environment variable to set the time server to use (NTP_SERVERS) this can be set to what ever you want. I went with time.windows.com because my host machine is running windows and wanted to have the 2 running against the same time server.&lt;/p&gt;

&lt;h3 id=&quot;influxdb&quot;&gt;influxdb&lt;/h3&gt;
&lt;p&gt;Took me a little bit to decipher the documentation to get the right config I needed for this one since I hadn’t used influxdb before but got all the environment variables setup for it to work. Most of these are pretty straight forward for things like username, password, org and bucket, DOCKER_INFLUXDB_INIT_MODE=setup is used to ensure the automated setup is run the first time it is started. I also have a volume mapped here so that it persists between container builds mainly so that I don’t have to go about reconfiguring the integration with home assistant which requires the organisation id (not the same as the org environment variable) and the API token (generated through the automated setup).&lt;/p&gt;

&lt;h3 id=&quot;grafana&quot;&gt;grafana&lt;/h3&gt;
&lt;p&gt;This one I haven’t used too much yet but so this config may change as I use it more. At the moment I just have a mapped volume for backups and persistent state.&lt;/p&gt;

&lt;h2 id=&quot;bonus-content&quot;&gt;Bonus Content!&lt;/h2&gt;

&lt;p&gt;Since these services are all installed manually via docker I miss some of the integration points that comes with installing them through the HA supervisor. One of those is adding them to the side menu in home assistant, to do this we can make use of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;panel_iframe&lt;/code&gt; integration in home assistant, here is an example of that integration in configuration.yaml.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;na&quot;&gt;panel_iframe&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;nodered&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Node&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Red&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;http://192.168.1.5:1880/&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;icon&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mdi:file-tree&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;tasmoadmin&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;TasmoAdmin&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;http://192.168.1.5:8124/&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;icon&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mdi:home-automation&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
</content>
 </entry>
 
 <entry>
   <title>Basic remote PC monitoring through Home Assistant</title>
   <link href="https://dkdevelopment.net/home-assistant-basic-pc-monitoring//"/>
   <updated>2021-12-15T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/home-assistant-basic-pc-monitoring/</id>
   <content type="html">&lt;p&gt;I’ve had a server running at home for a while to run various things for work and personal use and recently I put Home Assistant on it to have a play with.
If you haven’t heard of Home Assistant before it is an Open source home automation that puts local control and privacy first.&lt;/p&gt;

&lt;p&gt;One thing that I wanted to do was be able to monitor the server status itself, things like storage, memory usage and temps then feed that into Home Assistant so I could try out a notification pipeline. The problem was since my HA instance runs as a docker container it doesn’t have direct access to those APIs in windows to give out that kind of info.&lt;/p&gt;

&lt;p&gt;To get around this limitation I figured I had a look at a few tools that could surface that info as a Rest API that HA could consume but none did quite what I wanted easily so I decided to write my own light weight api to surface this data (also because it was fun). Then using the built in rest integration within HA I could set it up to read this data in as sensors.&lt;/p&gt;

&lt;p&gt;I did this in .net core using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;System.IO.DriveInfo.GetDrives()&lt;/code&gt; method which gets me all the details I need about the attached hard drives. Once I had that I filtered out to only should fixed drives (ie. non-removable storage) and mapped it to a model object so that I could output it as JSON without circular reference issues. I also outputted it as a dictionary where the drive letter was the key, this is to make it easier to pick up the specific drives later in HA.&lt;/p&gt;

&lt;p&gt;Here is the code for the controller:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Microsoft.AspNetCore.Mvc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;System.IO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;System.Linq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;WinSysMon.Controllers&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HomeController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Controller&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;drives&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IActionResult&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Drives&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;DriveInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drives&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DriveInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetDrives&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

            &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;driveModels&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drives&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DriveType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DriveType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Fixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DriveInfoModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ToDictionary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;:&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;First&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;driveModels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DriveInfoModel&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AvailableFreeSpace&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DriveFormat&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TotalFreeSpace&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TotalSize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VolumeLabel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DriveInfoModel&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DriveInfo&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DriveInfoModel&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;AvailableFreeSpace&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AvailableFreeSpace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;DriveFormat&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DriveFormat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;TotalFreeSpace&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TotalFreeSpace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;TotalSize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TotalSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;VolumeLabel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VolumeLabel&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Once that was done and the API was working I needed to setup the sensors in HA using the rest integration. I setup 3 sensors per drive: availableFreeSpace, totalSize and volumeLabel which gave me enough info to do what I needed.
The rest integration can be configured via the yaml file in HA and it allows you to setup multiple sensors from a single endpoint it also lets you set the interval times and authentication.&lt;/p&gt;

&lt;p&gt;Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;host.docker.internal&lt;/code&gt; as the url so that my HA instance running in docker can reference the host PC. Also doing some calculations on the values here since the API outputs the values in bytes so that I can just read them in as GB with 2 decimal places which is enough for me but can be changed easily.
Here is my yaml configuration for the rest sensors setting&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-liquid&quot; data-lang=&quot;liquid&quot;&gt;rest:
  - scan_interval: 600
    resource: http://host.docker.internal:55555/drives
    sensor:
      - name: &quot;Server Drive C availableFreeSpace&quot;
        value_template: &quot;&lt;span class=&quot;p&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;availableFreeSpace&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;)&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;round(2)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&quot;
        unit_of_measurement: GB
        
      - name: &quot;Server Drive C totalSize&quot;
        value_template: &quot;&lt;span class=&quot;p&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;totalSize&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;)&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;round(2)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&quot;
        unit_of_measurement: GB
        
      - name: &quot;Server Drive C volumeLabel&quot;
        value_template: &quot;&lt;span class=&quot;p&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;volumeLabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&quot;
        

      - name: &quot;Server Drive D availableFreeSpace&quot;
        value_template: &quot;&lt;span class=&quot;p&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;availableFreeSpace&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;)&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;round(2)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&quot;
        unit_of_measurement: GB
        
      - name: &quot;Server Drive D totalSize&quot;
        value_template: &quot;&lt;span class=&quot;p&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;totalSize&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;)&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;round(2)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&quot;
        unit_of_measurement: GB
        
      - name: &quot;Server Drive D volumeLabel&quot;
        value_template: &quot;&lt;span class=&quot;p&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;volumeLabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This will add some new entities to Home Assistant for us which look something like this in the dev tools.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2021/ha-deventities.jpg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now that we have the sensor data in HA we can start using it, for now I have put it into a lovelace dashboard but it can also be used in automation rules and setup as triggers etc.
I created a simple gauge card to test it setting severity with red as under 10%, orange as under 50% and green over 50%.&lt;/p&gt;

&lt;table style=&quot;width: 100%;&quot;&gt;
  &lt;tr&gt;
    &lt;td&gt;
      
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-liquid&quot; data-lang=&quot;liquid&quot;&gt;type: gauge
entity: sensor.server_drive_c_availablefreespace
min: 0
max: 233
severity:
  green: 115
  yellow: 23
  red: 0
needle: true
name: &apos;C: Drive Usage&apos;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

    &lt;/td&gt;
    &lt;td style=&quot;text-align: center;&quot;&gt;
      &lt;img src=&quot;/img/posts/2021/ha-drives.png&quot; /&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;From here next steps would be to look at integrating more sensors into the pipeline such as memory and temperature which can be done by querying with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;System.Manamgent.ManagementObjectSearcher&lt;/code&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Google Lighthouse CI Server Automation</title>
   <link href="https://dkdevelopment.net/lighthouse-ci-server-automation//"/>
   <updated>2021-09-28T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/lighthouse-ci-server-automation/</id>
   <content type="html">&lt;p&gt;Website performance is one of those things that is “important” to everyone but it is sometime hard to test and keep track of the results in a way that is easy to navigate and doesn’t require too much effort. Recently it has become even more important for SEO (Search Engine Optimization) with the big search engines starting to use these metrics more to affect ranking (See: &lt;a href=&quot;https://developers.google.com/search/docs/advanced/experience/page-experience&quot;&gt;Understanding page experience in Google Search results&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Google Lighthouse is a well known performance testing tool for websites that is built into Google Chrome and used by &lt;a href=&quot;https://developers.google.com/speed/pagespeed/insights&quot;&gt; Google PageSpeed Insights&lt;/a&gt;. But there is also &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci&quot;&gt;Lighthouse CI&lt;/a&gt; which is an open source CLI tool for automating running lighthouse for a CI/CD environment.&lt;/p&gt;

&lt;p&gt;Lighthouse CI also has a server component that let’s you save reports and compare them which can be very useful.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2021/lhci1.gif&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To setup the Lighthouse CI server through docker there are 3 parts to it.&lt;/p&gt;

&lt;h5 id=&quot;dockerfile&quot;&gt;Dockerfile&lt;/h5&gt;

&lt;p&gt;Setup the docker image to run the Lighthouse CI server with the appropriate config.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;err&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;node:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;-buster-slim&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;/usr/src/lhci&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;package.json&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;RUN&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;npm&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;lighthouserc.json&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;EXPOSE&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8000&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;npm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h5 id=&quot;packagejson&quot;&gt;package.json&lt;/h5&gt;

&lt;p&gt;The package.json file used to install the libraries and configure the server start.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lhci&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;0.0.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lhci server --config=./lighthouserc.json&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;@lhci/cli&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^0.8.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;@lhci/server&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^0.8.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mysql2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^2.1.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;pg&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^7.12.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;pg-hstore&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^2.3.3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;sqlite3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^4.0.6&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h5 id=&quot;lighthousercjson&quot;&gt;lighthouserc.json&lt;/h5&gt;

&lt;p&gt;The configuration file for the server, using PostgreSQL storage and daily CRON task to collect reports from PageSpeed Insights.
Check out the &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/server.md&quot;&gt;Lighthouse CI Server docs&lt;/a&gt; for more info on this.
To enable the PageSpeed Insights cron task you will need to get an API Key from the &lt;a href=&quot;https://developers.google.com/speed/docs/insights/v5/get-started&quot;&gt;official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;ci&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;server&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;port&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;storage&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;storageMethod&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sql&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;sqlDialect&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;postgres&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;sqlConnectionUrl&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;postgres://lhci_user:&amp;lt;password&amp;gt;@host.docker.internal:5432/lighthouseci_db&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;basicAuth&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;username&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;damian&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;PASSWORD&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;psiCollectCron&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;psiApiKey&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;psiApiKey&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;sites&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;urls&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://dkdevelopment.net/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://dkdevelopment.net/2019/05/25/working-remotely/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://dkdevelopment.net/resume/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;schedule&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;0 0 * * *&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;projectSlug&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dkdevelopment.net&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Once the server is up and running you then need to create a project on the server, to do this run: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lhci wizard&lt;/code&gt; to create a new project on the server and answer the questions it asks.
Save the tokens it generates (you will need these later).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2021/lhci-wizard.jpg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;With the project setup and the PSI CRON tasks running you will then be able to see the daily reports coming in with the ability to compare reports over time.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/2021/lhci-compare.jpg&quot; /&gt;&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Tips and tricks for Github Pages</title>
   <link href="https://dkdevelopment.net/tips-and-tricks-for-github-pages//"/>
   <updated>2020-07-26T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/tips-and-tricks-for-github-pages/</id>
   <content type="html">&lt;p&gt;This blog has been hosted on Github Pages for a long time, a very very long time, according to my git history since Feb 2012 (before that it was on Wordpress).&lt;/p&gt;

&lt;p&gt;I decided to make the change for a few reasons:&lt;br /&gt;
-To learn something new (Jekyll/Liquid were new to me)&lt;br /&gt;
-Have it feel more like a dev blog (writing posts in a dev IDE pushing to git etc.)&lt;br /&gt;
-Also the Pretzel Code52 project had just kicked off and I was interested in getting involved in that (&lt;a href=&quot;https://github.com/Code52/pretzel&quot;&gt;https://github.com/Code52/pretzel&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Now, 8 years later I am trying to get into blogging again so I am breathing new life into my Github Pages blog as well as building a new website for a podcast using Github Page so I thought I’d share some of the interesting things it can do.&lt;/p&gt;

&lt;h4&gt;Docker&lt;/h4&gt;
&lt;p&gt;I’m not a ruby developer and I didn’t want to have to install all of the ruby dev tools just to be able to add blog posts. Previously I was using a prebuilt version of pretzel locally which worked pretty well, there were a few things that worked differently or were not supported but I got by. After doing a fair bit of work with docker I figured I should just be able to run jekyll on docker, run the same version that runs on Gihub Pages without all the overhead.&lt;/p&gt;

&lt;p&gt;Luckily there are already github pages images setup so I didn’t need to do much to get this working, the one I went with is &lt;a href=&quot;https://github.com/Starefossen/docker-github-pages&quot;&gt;Starefossen/docker-github-pages&lt;/a&gt;. After finding that it was as easy as creating a powershell script to run it, which also mounts the directory to the docker container so it will auto update when changes are made.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;n&quot;&gt;docker&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;${PWD}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;/usr/src/app&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1000:4000&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;starefossen/github-pages&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;jekyll&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;serve&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;/_site&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--drafts&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--watch&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--force_polling&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;0.0.0.0&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-P&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;4000&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4&gt;For each loops&lt;/h4&gt;
&lt;p&gt;Github pages and Jekyll use the &lt;a href=&quot;https://jekyllrb.com/docs/liquid/&quot;&gt;Liquid templating language&lt;/a&gt; which lets you do some fun things, one of them is for loops. The below code loops through the posts on the site (getting the first 12) to display links for each. &lt;code&gt;{% for post in site.posts limit: 12 %}&lt;/code&gt; - &lt;code&gt;site.posts&lt;/code&gt; being the collection to iterate over. &lt;code&gt;for post in&lt;/code&gt; defines a post as the individual item being iterated. &lt;code&gt;limit: 12&lt;/code&gt; sets the loop to only iterate over the first 12 items, this can also be used with &lt;code&gt;offset: 12&lt;/code&gt; which sets the iteration to start at item 12 in the collection.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;{% for post in site.posts limit: 12 %}
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;{{ post.url }}&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        {{ post.title }}
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
{% endfor %}&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4&gt;Combine and Minify CSS&lt;/h4&gt;
&lt;p&gt;This one is a bit of a hack but it allows you to combine and (somewhat) minify CSS files using some of the liquid templating. Starting with the &lt;a href=&quot;https://www.rubydoc.info/github/Shopify/liquid/Liquid/Capture&quot;&gt;&lt;strong&gt;capture block&lt;/strong&gt;&lt;/a&gt; which lets you store a result in a variable without rendering it. Using the capture we then include the relevant css files inside the capture here using the &lt;a href=&quot;https://jekyllrb.com/docs/includes/&quot;&gt;&lt;strong&gt;include_relative&lt;/strong&gt;&lt;/a&gt; tag. Finally we render the newly captured variable making sure to strip out the new lines and multiple spaces.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span class=&quot;nt&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;permalink&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;dkdev&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.min.css&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%-&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;capture&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;sitecss&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;-%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;include_relative&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;bootstrap.min.css&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;include_relative&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;fontawesome-free.min.css&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;include_relative&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;clean-blog.css&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%-&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;endcapture&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;-%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;{-&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;sitecss&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;strip_newlines&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;  &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4&gt;Data files&lt;/h4&gt;
&lt;p&gt;Jekyll allows you to add other &lt;a href=&quot;https://jekyllrb.com/docs/datafiles/&quot;&gt;data files&lt;/a&gt; that can be used by various pages. These are just yml, json or csv files that can be put in the _data folder that can be accessed from any page or layout using site.data.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;
    &lt;pre&gt;
&lt;code class=&quot;language-yml&quot; data-lang=&quot;yml&quot;&gt;&lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Phili J Fry&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Delivery Boy&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&lt;/span&gt;

&lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Bender&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Company Chef&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&lt;/span&gt;

&lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Turanga Leela&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Captain&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&lt;/span&gt;

&lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;John Zoidberg&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Staff doctor&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;

&lt;p&gt;This data file can then be used by a for loop to display all the items:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;{% for character in site.data.characters %}
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
        {{ character.name }} - {{ character.title }}
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
{% endfor %}&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h4&gt;Syntax Highlighting&lt;/h4&gt;
&lt;p&gt;Github Pages has built in syntax highlighting that uses the &lt;a href=&quot;https://github.com/rouge-ruby/rouge&quot;&gt;Rouge Ruby syntax highlighter&lt;/a&gt; library.
It let’s you wrap your code with &lt;code&gt;{% highlight %}&lt;/code&gt; and &lt;code&gt;{% endhighlight %}&lt;/code&gt; and it will do some formatting with it which allows you to easily style it.
All you need to do is include a rouge or pygments compatible stylesheet.&lt;/p&gt;

&lt;p&gt;Here is what mine looks like for reference: &lt;a href=&quot;https://dkdevelopment.net/css/pygments_style.css&quot;&gt;https://dkdevelopment.net/css/pygments_style.css&lt;/a&gt;&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Getting started with Singer and Docker</title>
   <link href="https://dkdevelopment.net/2020/06/14/singer-io-with-docker//"/>
   <updated>2020-06-14T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/2020/06/14/singer-io-with-docker/</id>
   <content type="html">&lt;p&gt;I’ve been doing a lot of work with data recently and I wanted a way to be able to join some of our platform data to our traffic data from Google Analytics.&lt;/p&gt;

&lt;p&gt;After trying a few methods to get data out of Google Analytics into our PostgreSQL database with not much success I discovered &lt;a href=&quot;https://singer.io&quot;&gt;singer.io&lt;/a&gt; a “Simple, Composable, Open Source ETL” tool. So I started playing around with the Google Analytics tap and the PostgreSQL target (&lt;a href=&quot;https://www.singer.io/tap/google-analytics/postgresql/&quot;&gt;https://www.singer.io/tap/google-analytics/postgresql/&lt;/a&gt;). After a lot of digging through various documentations and source code I was able to get it working!&lt;/p&gt;

&lt;p&gt;Now that I have it running it was time to try and automate it somehow, to do this I tried to set it up as a docker container that way it can be run in the cloud (In our case AWS) on a schedule to pull in data daily from Google Analytics.&lt;/p&gt;

&lt;p&gt;Here is the Docker file that puts it all together.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-docker&quot; data-lang=&quot;docker&quot;&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; python:3.7.6-stretch&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;pip &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;singer-target-postgres

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ./tap-google-analytics-master/ ./external/tap-google-analytics-master/&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /external/tap-google-analytics-master&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;pip &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; .

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ./singer/tap_google-analytics_config.json ./tap_google-analytics_config.json&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ./singer/client_secrets.json ./client_secrets.json&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ./singer/tap_google-analytics_reports.json ./tap_google-analytics_reports.json&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ./singer/target_postgres_config.json ./target_postgres_config.json&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; tap-google-analytics -c ./tap_google-analytics_config.json | target-postgres -c ./target_postgres_config.json &amp;gt;&amp;gt; state.json&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;A few things here to call out to get this working. Installing the PostgreSQL target was straight forward but for the Google Analytics tap I used an unofficial library and built it from the source at &lt;a href=&quot;https://gitlab.com/meltano/tap-google-analytics&quot;&gt;https://gitlab.com/meltano/tap-google-analytics&lt;/a&gt;. After that we have the various config files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tap_google-analytics_config.json&lt;/strong&gt; This is the config for the Google Analytics tap, key file location is name of the config file that details the service account credentials. The reports property is a json file that defines the reports to pull from Google Analytics. The start date and end date here define the date range to pull the report data for.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;key_file_location&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;client_secrets.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;view_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;111111111&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;reports&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;tap_google-analytics_reports.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start_date&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2020-06-01T00:00:00Z&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end_date&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2020-06-15T00:00:00Z&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;client_secrets.json&lt;/strong&gt; this file contains the service account credentials, to get these follow the instructions defined in &lt;a href=&quot;https://gitlab.com/meltano/tap-google-analytics#creating-service-account-credentials&quot;&gt;https://gitlab.com/meltano/tap-google-analytics#creating-service-account-credentials&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tap_google-analytics_reports.json&lt;/strong&gt; this file defines the report data being pulled from Google Analytics. A full list of the available dimensions and metrics can be found here: &lt;a href=&quot;https://ga-dev-tools.appspot.com/dimensions-metrics-explorer/&quot;&gt;https://ga-dev-tools.appspot.com/dimensions-metrics-explorer/&lt;/a&gt;. Here is an example of a report for pageviews.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ga_pageviews&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;dimensions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ga:date&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ga:pagePath&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ga:source&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ga:medium&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ga:sourceMedium&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;metrics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ga:pageviews&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;target_postgres_config.json&lt;/strong&gt; This file is the config for the PostgreSQL target, mostly straight forward connection config.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postgres_host&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;localhost&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postgres_port&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5432&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postgres_database&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ga_data&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postgres_username&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;db_user&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postgres_password&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;db_treasure&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postgres_schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;public&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;after_run_sql&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now that we have everything setup to run in docker to truly automate this all that is left to do is being able to update the start and end date properties in the Google Analytics config file dynamically. This can be done with a bit of bash.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Working Remotely (and how)</title>
   <link href="https://dkdevelopment.net/2019/05/25/working-remotely//"/>
   <updated>2019-05-25T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/2019/05/25/working-remotely/</id>
   <content type="html">&lt;p&gt;I work remotely and have been full time for over 6 months now and I wanted to take some time to talk about my experience with it. I quite often joke about this, for example here is a post from my Instagram after my 2nd week at another fully remote workplace.&lt;/p&gt;

&lt;blockquote class=&quot;instagram-media2&quot; style=&quot; background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; margin-bottom: 12px; max-width:540px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);&quot;&gt;
    &lt;div style=&quot;padding:16px;&quot;&gt;
        &lt;div style=&quot; display: flex; flex-direction: row; align-items: center;&quot;&gt;
            &lt;div style=&quot;background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;&quot;&gt;
                &lt;a href=&quot;https://www.instagram.com/damiankarzon/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
                    &lt;img style=&quot;border-radius: 50%;&quot; src=&quot;https://pbs.twimg.com/profile_images/721583338671058944/M83ceTNZ_200x200.jpg&quot; alt=&quot;damiankarzon&quot; /&gt;
                &lt;/a&gt;
            &lt;/div&gt;
            &lt;div style=&quot;display: flex; flex-direction: column; flex-grow: 1; justify-content: center;&quot;&gt;
                &lt;div style=&quot;border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 12px; width: 100px;&quot;&gt;
                    &lt;a style=&quot;color: #000; font-weight: 500; font-size: 14px;&quot; href=&quot;https://www.instagram.com/damiankarzon/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
                        damiankarzon
                    &lt;/a&gt;
                &lt;/div&gt;
                &lt;div style=&quot;border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;&quot;&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;div style=&quot;padding-top: 8px;&quot;&gt;
            &lt;a href=&quot;https://www.instagram.com/p/BuxJFmmBHOi/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
                &lt;img src=&quot;/img/posts/2019-05/work-remotely.png&quot; alt=&quot;Damian working from home&quot; /&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div style=&quot;display: flex; flex-direction: row; align-items: center;&quot;&gt;
            &lt;a href=&quot;https://www.instagram.com/p/BuxJFmmBHOi/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
                &lt;div style=&quot;background-color: #EEEEEE; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);&quot;&gt;
                &lt;/div&gt;
                &lt;div style=&quot;background-color: #EEEEEE; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;&quot;&gt;
                &lt;/div&gt;
                &lt;div style=&quot;background-color: #EEEEEE; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);&quot;&gt;
                &lt;/div&gt;
            &lt;/a&gt;
            &lt;a href=&quot;https://www.instagram.com/p/BuxJFmmBHOi/&quot; target=&quot;_blank&quot; style=&quot;margin-left: 8px;&quot; rel=&quot;noopener&quot;&gt;
                &lt;div style=&quot; background-color: #EEEEEE; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;&quot;&gt;
                &lt;/div&gt;
                &lt;div style=&quot; width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #EEEEEE; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)&quot;&gt;
                &lt;/div&gt;
            &lt;/a&gt;
            &lt;a href=&quot;https://www.instagram.com/p/BuxJFmmBHOi/&quot; target=&quot;_blank&quot; style=&quot;margin-left: auto;&quot; rel=&quot;noopener&quot;&gt;
                &lt;div style=&quot; width: 0px; border-top: 8px solid #EEEEEE; border-right: 8px solid transparent; transform: translateY(16px);&quot;&gt;
                &lt;/div&gt;
                &lt;div style=&quot; background-color: #EEEEEE; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);&quot;&gt;
                &lt;/div&gt;
                &lt;div style=&quot; width: 0; height: 0; border-top: 8px solid #EEEEEE; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);&quot;&gt;
                &lt;/div&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;p style=&quot;font-family:Arial,sans-serif; font-size:14px; line-height:16px; margin-bottom:0; margin-top:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;&quot;&gt;
            &lt;a href=&quot;https://www.instagram.com/damiankarzon/&quot; style=&quot;font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:16px;&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; Damian Karzon&lt;/a&gt; (@damiankarzon) on &lt;time style=&quot; font-family:Arial,sans-serif; font-size:14px; line-height:17px;&quot; datetime=&quot;2019-03-09T00:42:34+00:00&quot;&gt;Mar 8, 2019 at 4:42pm PST&lt;/time&gt;&lt;/p&gt;
        &lt;p style=&quot; margin:0;&quot;&gt;
            &lt;a href=&quot;https://www.instagram.com/p/BuxJFmmBHOi/&quot; style=&quot; color:#000; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:16px; text-decoration:none; word-wrap:break-word;&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;What people think of when I say I work from home vs. what it&amp;#39;s actually like.&lt;/a&gt;
        &lt;/p&gt;
    &lt;/div&gt;
&lt;/blockquote&gt;

&lt;p&gt;But this is actually something I take very seriously and I am constantly asked about it. When I tell people I work from home they often ask me a question like &lt;em&gt;“How often do you have to go into the office?”&lt;/em&gt; or &lt;em&gt;“Where is their head office?”&lt;/em&gt;. My current workplace&lt;strong&gt;&lt;em&gt;has no office&lt;/em&gt;&lt;/strong&gt;, none at all. The entire company works remotely which is sometimes a strange concept for people.&lt;/p&gt;

&lt;p&gt;When I first started working remotely there were a few things that I was worried about:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Distractions&lt;/li&gt;
  &lt;li&gt;Cabin fever&lt;/li&gt;
  &lt;li&gt;Isolation&lt;/li&gt;
  &lt;li&gt;Work-life balance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, this post is specifically about my experience with remote work and, as I have learned throughout this time, everyone is different and depending on personalities are affected in different ways when it comes to this sort of thing.&lt;/p&gt;

&lt;h4 id=&quot;distractions&quot;&gt;Distractions&lt;/h4&gt;
&lt;p&gt;When I first started I thought the distractions would be a big problem for me (from past experience working from home in a non-full time way) so I set out to make sure I could minimise it. The main thing I did for this was to separate my home and work life both physically and mentally. I had a spare room at my place which I have converted into my home office that I use exclusively for working, I also have a dedicated work computer that I don’t use for anything else (ie. no games on it). To separate my home and work life mentally I try to go for a walk every day when I finish work, this helps to break up the work day from the non-work day (it’s also good to get some exercise in).&lt;/p&gt;

&lt;h4 id=&quot;cabin-fever&quot;&gt;Cabin fever&lt;/h4&gt;
&lt;p&gt;This wasn’t something I had actually considered when I started since I am quite comfortable staying at home for long periods of time. However the problem came up often when I found myself being stuck in a problem that I couldn’t solve. I would start to get irritated very easily by my surroundings and not being able to solve the problem in front of me so I would end up walking up and down the hallway for no reason. In this situation now I quite often turn to someone else on the team and jump of a quick call just to talk things out or I leave the house and go for a walk to get some fresh air.&lt;/p&gt;

&lt;h4 id=&quot;isolation&quot;&gt;Isolation&lt;/h4&gt;
&lt;p&gt;I often hear people talk about isolation being their biggest problem when it comes to working remotely but this hasn’t been a massive problem for me, probably due to living alone for the last 6 years. This is also helped by the virtual meetings we have which are all video calls and always start with a bit of a chat before getting into business. A great example of this is our weekly whole company meetings where we start by everyone having a chance to share something personal (maybe they did something exciting on the weekend or during the week) or they can share a learning from the previous week.&lt;/p&gt;

&lt;h4 id=&quot;work-life-balance&quot;&gt;Work-life balance&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;“Do you find yourself working more since it’s all just there?”&lt;/em&gt; or &lt;em&gt;“Do you sometimes just go and watch tv instead of working?”&lt;/em&gt; other questions I sometimes get asked. Both are on opposite sides of the work-life balance, on one hand you have the potential to over work since it is so close and on the other you could do no work at all because you are not confined to an office. My answer to both of these questions is normally &lt;em&gt;“Yes but…”&lt;/em&gt;, yes sometimes I do find myself working more, yes sometimes I do take a random break during the day if I’m feeling drained or stuck. It’s all about having the balance between the 2, it is much easier when working from home to over work or to under work but if you are motivated to do the work and have the discipline to keep working when you need to then you won’t have a problem. Motivation can be hard to find sometimes so it’s important to have work that you enjoy and are passionate about and to have a team that work well together. It is also important to remember that no job is awesome all the time and work will sometimes get you down but you need to keep at it anyway and work on making it better where you can.&lt;/p&gt;

&lt;h4 id=&quot;environment&quot;&gt;Environment&lt;/h4&gt;

&lt;p&gt;I have put a lot a lot of time, effort and investment into getting my home office setup and it’s at a point where I am more productive there than any office I have ever worked at.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Desk: SKARSTA sit/stand desk from IKEA (120x70 cm)&lt;/li&gt;
  &lt;li&gt;Chair: DX Racer Classic Series&lt;/li&gt;
  &lt;li&gt;Laptop: Microsoft Surface Book 2 (13” i7, 16GB Ram, 512GB SSD) with Surface dock&lt;/li&gt;
  &lt;li&gt;Monitor: Dell P2715Q 27” 4K&lt;/li&gt;
  &lt;li&gt;Audio: Steelseries Arctis 3 Bluetooth headset &amp;amp; JBL Charge 2 Bluetooth speaker&lt;/li&gt;
  &lt;li&gt;Keyboard: Steelseries Apex 350&lt;/li&gt;
  &lt;li&gt;Mouse: Steelseries Rival 110&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;tools&quot;&gt;Tools&lt;/h4&gt;

&lt;p&gt;It is also super important to find the right tools and team process to be able to work remotely. The majority of our communication is text based and for that we use Slack mostly (some emails here and there when dealing with external parties).
Sometimes it’s harder to communicate things over text, when this happens we tend to jump on a quick call using either a phone call, Slack call or a Zoom video call.
We currently run a “scrum-like” development process which includes daily virtual stand-ups, for this we use Zoom. We also use video calls for larger team meetings such as the weekly whole company or product team meetings, this makes discussions much easier and allows people to share screens if required.&lt;/p&gt;

&lt;p&gt;There are also times when the team (or at least part of the team) will meet up in person, for example the Brisbane dev team try to get together once a month and spend the day working together, this is either at a co-working space or a shared space (such as a library). There are also times when all of the workers in Brisbane will get together for a team lunch, this greatly heaps everyone get to know each other more and to feel more comfortable with each other.&lt;/p&gt;

&lt;p&gt;Well that’s enough ramblings from me about working remotely, let me know your thoughts on it or if you have any questions feel free to reach out to me (&lt;a href=&quot;https://twitter.com/dkarzon&quot;&gt;@dkarzon&lt;/a&gt; on Twitter).&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a Discord voice bot with Discord.NET</title>
   <link href="https://dkdevelopment.net/2019/02/18/discord-voice-bot//"/>
   <updated>2019-02-18T00:00:00+00:00</updated>
   <id>https://dkdevelopment.net/2019/02/18/discord-voice-bot/</id>
   <content type="html">&lt;p&gt;
    I&apos;ve been playing around with the Discord api recently, I wanted to see what could actually be done with it.
    I set a goal to make a discord bot that slightly annoys people by joining voice channels and randomly say Zapp Brannigan quotes.
&lt;/p&gt;

&lt;p&gt;
    For those who don&apos;t know Discord is an: &lt;blockquote&gt;All-in-one voice and text chat for gamers that&apos;s free, secure, and works on both your desktop and phone. - &lt;a href=&quot;https://discordapp.com/&quot;&gt;https://discordapp.com/&lt;/a&gt;&lt;/blockquote&gt;
&lt;/p&gt;

&lt;p&gt;
    I got started with &lt;a href=&quot;https://github.com/discord-net/Discord.Net&quot;&gt;Discord.NET&lt;/a&gt; and was able to get the bot connected very quickly. Discord.NET is an unofficial .NET API wrapper for discord which seem to be the most popular and widely used library for it so I went with it. Be sure to check out the &lt;a href=&quot;https://discord.foxbot.me/docs/&quot;&gt;documentation&lt;/a&gt; for details. For this to work with voice connections there are a few native dlls that you need to download and put with your application. These are simple enough to get from the &lt;a href=&quot;https://discord.foxbot.me/docs/guides/voice/sending-voice.html&quot;&gt;voice documentation&lt;/a&gt; (or check out my full source link at the end).
&lt;/p&gt;
&lt;p&gt;
    First thing you need to do is get an instance of &lt;strong&gt;DiscordSocketClient&lt;/strong&gt; for my project I am doing a .NET Core 2.2 console app with the built in dependency injection.
&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;&lt;span class=&quot;n&quot;&gt;_discord&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_services&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetRequiredService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DiscordSocketClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_discord&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;LoginAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TokenType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Bot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_botToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_discord&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;StartAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That is all the code you need to have your bot connect to discord!&lt;/p&gt;

&lt;p&gt;
    I wanted my bot to detect when a user joined a voice channel and connect to that channel (if it wasn&apos;t already). To do that I used the &lt;strong&gt;UserVoiceStateUpdated&lt;/strong&gt; event on the DiscordSocketClient which can tell us when a users voice state updates (join/leave/move voice channels).
&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// Add this before calling StartAsync&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;_discord&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UserVoiceStateUpdated&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OnVoiceStateUpdated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The code to handle the event then looks like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;OnVoiceStateUpdated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SocketUser&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SocketVoiceState&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SocketVoiceState&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Check if this was a non-bot user joining a voice channel&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;IsBot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VoiceChannel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VoiceChannel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;ConnectToVoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VoiceChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ConnectToVoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SocketVoiceChannel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;voiceChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;voiceChannel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;$&quot;Connecting to channel &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;voiceChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;voiceChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ConnectAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;$&quot;Connected to channel &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;voiceChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;blocknote&quot;&gt;Important to note here is that we don&apos;t await ConnectToVoice, this is because if we await that call from inside the UserVoiceStateUpdated event it will cause a deadlock and never return from connecting to the voice channel.&lt;/div&gt;

&lt;p&gt;Now we are connecting to a voice channel when we detect a user joins it so lets look at having the bot actually send audio through. To do this the documentation for the library has an example using ffmpeg to read the sound files and pipes that into a stream that then gets sent to Discord.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;psi&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ProcessStartInfo&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;FileName&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ffmpeg&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Arguments&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;$@&quot;-i &quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot; -ac 2 -f s16le -ar 48000 pipe:1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;RedirectStandardOutput&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;UseShellExecute&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ffmpeg&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;psi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ffmpeg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;StandardOutput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BaseStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;discord&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;CreatePCMStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AudioApplication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Voice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;CopyToAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;discord&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;discord&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FlushAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;
    Here we start a new process passing in start info that opens up ffmpeg.exe (also included in the project) passing in a relative path to out sound file and a few other command line args to play the sound and pipe the output which we then copy into a stream into discord.
&lt;/p&gt;

&lt;p&gt;
    This is the basics of creating a voice capable discord bot with Discord.NET, for a more advanced implementation take a look at the full source for this project at &lt;a href=&quot;https://github.com/dkarzon/DiscordZapBot&quot;&gt;https://github.com/dkarzon/DiscordZapBot&lt;/a&gt;.
&lt;/p&gt;
</content>
 </entry>
 
 
</feed>