[{"data":1,"prerenderedAt":1214},["ShallowReactive",2],{"post-\u002Fblog\u002Fthe-website-that-told-the-same-story-twice":3,"blog-all":176},{"id":4,"title":5,"author":6,"body":7,"date":159,"description":160,"draft":161,"extension":162,"image":163,"meta":164,"navigation":165,"path":166,"readingMinutes":167,"seo":168,"stem":169,"tags":170,"__hash__":175},"blog\u002Fblog\u002Fthe-website-that-told-the-same-story-twice.md","The Website That Told the Same Story Twice","EkoHacks",{"type":8,"value":9,"toc":153},"minimark",[10,14,17,20,25,28,31,34,38,41,77,80,111,114,118,121,124,127,130,134,137,140,143,146,149],[11,12,13],"p",{},"We had a feeling about our own website, and a feeling is a hard thing to act on.",[11,15,16],{},"The home page and the story page felt like the same story told twice. Read one, then the other, and you met Nigeria again, the open source tools again, the institute we are building again. Nothing was wrong in isolation. Together they sagged. But when you try to fix a thing by feel, every cut is an argument about taste, and taste is slow and personal and easy to defend against. We wanted something firmer than a feeling. We wanted evidence.",[11,18,19],{},"So we did what we would do with any tangle in a codebase. We stopped reading the words as words, and started treating them as data.",[21,22,24],"h2",{"id":23},"content-is-data-wearing-a-nice-outfit","Content is data wearing a nice outfit",[11,26,27],{},"A web page looks like prose. Underneath, it is structure. Our pages are built from blocks: a hero, a section of text, a grid, a timeline. Each block is really a small record. It has a type, a position, a heading, and a job to do. Seen that way, a page is not an essay. It is a list of rows.",[11,29,30],{},"That shift is the whole trick. Once content is data, you can ask questions of it that you cannot ask of prose. You can count. You can group. You can find the thing that appears where it should not. A vague sense of repetition becomes a query with a yes or no answer.",[11,32,33],{},"The unit we were missing had no name on the page. We called it a beat. A beat is a single idea the site is trying to land. Our roots in Nigeria are a beat. Our open source engine is a beat. The institute we are growing into is a beat. A section is just one way of presenting a beat, and the same beat can be presented on more than one page. That last sentence is where the trouble was hiding.",[21,35,37],{"id":36},"a-throwaway-database-gone-by-lunch","A throwaway database, gone by lunch",[11,39,40],{},"We sketched a tiny database. Four tables, fifteen lines of schema, the kind of thing you delete the moment it has answered you. One table for pages, one for beats, one for the sections on each page, and one join table to connect them. The join table is the quiet hero here. It records which beats each section is telling, and at what depth, lite or full.",[42,43,48],"pre",{"className":44,"code":45,"language":46,"meta":47,"style":47},"language-sql shiki shiki-themes github-light github-dark","CREATE TABLE page    (id, slug, job);\nCREATE TABLE beat    (id, name, owner_page);   -- where this idea is told in full\nCREATE TABLE section (id, page_id, position, heading);\nCREATE TABLE section_beat (section_id, beat_id, depth);  -- the join\n","sql","",[49,50,51,59,65,71],"code",{"__ignoreMap":47},[52,53,56],"span",{"class":54,"line":55},"line",1,[52,57,58],{},"CREATE TABLE page    (id, slug, job);\n",[52,60,62],{"class":54,"line":61},2,[52,63,64],{},"CREATE TABLE beat    (id, name, owner_page);   -- where this idea is told in full\n",[52,66,68],{"class":54,"line":67},3,[52,69,70],{},"CREATE TABLE section (id, page_id, position, heading);\n",[52,72,74],{"class":54,"line":73},4,[52,75,76],{},"CREATE TABLE section_beat (section_id, beat_id, depth);  -- the join\n",[11,78,79],{},"Then we typed our two pages into it, every section, every beat each section carried. Tedious for ten minutes. Worth it. Because now the question we had been circling for an hour was one short query.",[42,81,83],{"className":44,"code":82,"language":46,"meta":47,"style":47},"SELECT beat, count(distinct page_id) AS pages\nFROM section_beat JOIN section USING (section_id)\nWHERE depth = 'full'\nGROUP BY beat\nHAVING pages > 1;\n",[49,84,85,90,95,100,105],{"__ignoreMap":47},[52,86,87],{"class":54,"line":55},[52,88,89],{},"SELECT beat, count(distinct page_id) AS pages\n",[52,91,92],{"class":54,"line":61},[52,93,94],{},"FROM section_beat JOIN section USING (section_id)\n",[52,96,97],{"class":54,"line":67},[52,98,99],{},"WHERE depth = 'full'\n",[52,101,102],{"class":54,"line":73},[52,103,104],{},"GROUP BY beat\n",[52,106,108],{"class":54,"line":107},5,[52,109,110],{},"HAVING pages > 1;\n",[11,112,113],{},"In plain words: show me every idea we are telling in full on more than one page. The database did not have feelings about our website. It returned three rows. Our Nigerian roots. Our open source engine. The institute we are building. Each one told fully, twice. The repetition we had felt was now three named things on a screen, and you cannot argue with three named things the way you can argue with a feeling.",[21,115,117],{"id":116},"the-fix-has-an-old-name","The fix has an old name",[11,119,120],{},"What we had was a normalisation problem, the same one that haunts any database where the same fact is stored in two places. When a fact lives twice, the two copies drift, they contradict, and the reader, or the program, stops trusting either. The cure is old and simple. Store each fact once, in one home. Everywhere else, point to it.",[11,122,123],{},"So we gave every beat a single home. The roots, the engine, the longer arc of the institute belong to the story page, the place a reader goes when they want the full account. The home page keeps only a light touch of each, a single line and a link, and spends its space on the job only it can do, which is to say what we offer and show people the door they came for.",[11,125,126],{},"A second query, almost the same as the first, then told us which sections to cut. Any section telling a beat in full on a page that does not own it. Three sections on the home page lit up. We removed them. The home page went from nine sections to six and got faster to read and easier to act on. The story page kept the depth and stopped competing with the front door for the same words.",[11,128,129],{},"We did not design that outcome by argument. We let the structure show us, and we followed it.",[21,131,133],{"id":132},"why-a-school-would-bother","Why a school would bother",[11,135,136],{},"We could have moved some paragraphs around until it felt better. It would have taken the same afternoon. The reason we reached for code instead is the reason we exist as a school.",[11,138,139],{},"We teach a way of working, not a stack of facts. And the habit underneath this small job is one of the most useful a builder can own. Make the implicit explicit. When something is hard to reason about, change its form until it is easy. A wall of prose is hard to reason about. A table is easy. The skill is not knowing SQL. It is recognising that a question about words could become a question about rows, and being willing to spend ten honest minutes building the thing that answers it.",[11,141,142],{},"That habit settles arguments that taste cannot. It replaces the loudest voice in the room with a result anyone can reproduce. It is, quietly, a fairer way to make decisions, and we think a school that builds technology for social change should model fair decisions in the small things as well as the large ones.",[11,144,145],{},"We also do this in the open on purpose. This website is part of how we earn trust, from the communities we build with and from the people who fund the work. Showing the working, including the throwaway database we deleted before lunch, is more honest proof of how we think than any claim we could write about ourselves. We are building a coding institute, and we are building it where the working can be seen.",[11,147,148],{},"The website told the same story twice. We did not talk ourselves out of the feeling, and we did not act on it blind. We gave it a shape we could question, and then we listened to the answer.",[150,151,152],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":47,"searchDepth":61,"depth":61,"links":154},[155,156,157,158],{"id":23,"depth":61,"text":24},{"id":36,"depth":61,"text":37},{"id":116,"depth":61,"text":117},{"id":132,"depth":61,"text":133},"2026-06-06","Our home and story pages felt like one story stretched across two screens. Rather than argue about it by taste, we modelled our own words as data, asked a small database one question, and let it show us exactly what we were repeating.",false,"md",null,{},true,"\u002Fblog\u002Fthe-website-that-told-the-same-story-twice",6,{"title":5,"description":160},"blog\u002Fthe-website-that-told-the-same-story-twice",[171,172,173,174],"craft","content","data","teaching","D80cQNJzuJTRJFUB0VGN-Xe2eH6jzsa2yLnY1pFcg5U",[177,420,684,718,747,910,1012,1100],{"id":178,"title":179,"author":180,"body":181,"date":413,"description":414,"draft":161,"extension":162,"image":163,"meta":415,"navigation":165,"path":416,"readingMinutes":73,"seo":417,"stem":418,"tags":163,"__hash__":419},"blog\u002Fblog\u002Fbuilding-engineering-teams-that-learn-together.md","Building Engineering Teams That Learn Together","EkoHacks Team",{"type":8,"value":182,"toc":405},[183,186,189,194,197,200,208,212,215,225,228,235,242,249,252,259,262,266,269,272,275,287,290,294,297,300,303,311,314,318,321,328,331,334,341,344,351,354,361,364,371,374,378,381,395,402],[11,184,185],{},"There is a pattern we see in nearly every organisation we work with. The team has talented individuals. Smart people who care about their craft. But somehow, the sum is less than the parts. Decisions get stuck. Knowledge lives in one person's head. When that person goes on leave, everything slows down.",[11,187,188],{},"The issue is rarely about individual skill. It is about how the team learns, shares, and grows together.",[190,191,193],"h3",{"id":192},"the-problem-with-the-10x-developer-myth","The Problem with the 10x Developer Myth",[11,195,196],{},"The industry loves the idea of the exceptional individual who can turn a project around alone. These people exist, but building around them is fragile.",[11,198,199],{},"When one person holds all the knowledge, you haven’t built a team; you’ve built a dependency. And dependencies are risks.",[11,201,202,203,207],{},"The alternative is deliberate knowledge distribution: designing your ways of working so that no single departure can derail the project. This does ",[204,205,206],"em",{},"not"," happen by accident.",[190,209,211],{"id":210},"what-a-learning-team-looks-like","What a Learning Team Looks Like",[11,213,214],{},"Learning teams have clear, observable behaviours:",[216,217,218],"ol",{},[219,220,221],"li",{},[222,223,224],"strong",{},"They ask questions openly.",[11,226,227],{},"There is no social cost to saying “I don’t know” or “can you explain that?” Pretending to understand is treated as more dangerous than admitting confusion.",[216,229,230],{},[219,231,232],{},[222,233,234],{},"They share context, not just code.",[11,236,237,238,241],{},"Work doesn’t end at “PR merged.” People explain ",[204,239,240],{},"why"," they chose an approach, what alternatives they considered, and which trade‑offs they accepted.",[216,243,244],{},[219,245,246],{},[222,247,248],{},"They rotate responsibilities.",[11,250,251],{},"Nobody is the permanent owner of a domain. People move between areas of the codebase, between frontend and backend, between features and infrastructure. It’s uncomfortable at first and invaluable over time.",[216,253,254],{},[219,255,256],{},[222,257,258],{},"They reflect regularly.",[11,260,261],{},"Not just in formal retros that can become performative, but in ongoing, honest conversations about what is and isn’t working, often informally, as part of the daily rhythm.",[190,263,265],{"id":264},"pair-programming-as-knowledge-transfer","Pair Programming as Knowledge Transfer",[11,267,268],{},"Pair programming is one of the most effective, and most resisted, tools for building a learning culture.",[11,270,271],{},"It feels slow. It feels like being watched. That discomfort is the point.",[11,273,274],{},"When two people work together:",[276,277,278,281,284],"ul",{},[219,279,280],{},"They must articulate their thinking.",[219,282,283],{},"They can’t hide behind habit or instinct.",[219,285,286],{},"They expose gaps in their own reasoning.",[11,288,289],{},"An hour of pairing can transfer understanding that would take weeks of documentation and months of solo work to develop.",[190,291,293],{"id":292},"psychological-safety-as-a-precondition","Psychological Safety as a Precondition",[11,295,296],{},"None of this works without psychological safety.",[11,298,299],{},"If people fear looking foolish, they won’t ask questions. If mistakes are punished, they’ll be hidden. If seniority decides whose ideas matter, junior people will stop contributing.",[11,301,302],{},"Psychological safety is not about being nice. It’s about:",[276,304,305,308],{},[219,306,307],{},"Valuing intellectual honesty over being right.",[219,309,310],{},"Treating changing your mind as a strength.",[11,312,313],{},"This starts with leadership. When the most senior people ask questions, admit uncertainty, and visibly change their minds in response to better arguments, everyone else gets permission to do the same.",[190,315,317],{"id":316},"practical-steps-to-build-a-learning-team","Practical Steps to Build a Learning Team",[11,319,320],{},"This is not a one‑off initiative; it’s a continuous practice. Some concrete moves:",[216,322,323],{},[219,324,325],{},[222,326,327],{},"Start each week with a learning goal.",[11,329,330],{},"Not just “what will we ship?” but “what will we understand better by Friday?”",[11,332,333],{},"Example: “This week we want to understand how the payment system handles retries.”",[216,335,336],{},[219,337,338],{},[222,339,340],{},"Run internal tech talks.",[11,342,343],{},"Short, informal, frequent. Anyone can present. The goal is practice explaining ideas and exposing the team to different perspectives, not polished performance.",[216,345,346],{},[219,347,348],{},[222,349,350],{},"Make code review a conversation.",[11,352,353],{},"Whenever possible, review synchronously. Walk through the code together, discuss alternatives, explore edge cases. Treat review as a learning moment, not a gate.",[216,355,356],{},[219,357,358],{},[222,359,360],{},"Celebrate questions, not just answers.",[11,362,363],{},"Call out questions that change direction or reveal gaps in understanding. Signal that curiosity is a first‑class contribution.",[216,365,366],{},[219,367,368],{},[222,369,370],{},"Document decisions, not just code.",[11,372,373],{},"Use lightweight decision records: what was decided, why, what alternatives were rejected, and links to relevant discussions. This builds institutional memory that survives team changes.",[190,375,377],{"id":376},"playing-the-long-game","Playing the Long Game",[11,379,380],{},"You won’t see a dramatic productivity spike in the first month. But over 6, 24 months, the compounding effect is significant:",[276,382,383,386,389,392],{},[219,384,385],{},"Faster onboarding and less knowledge loss when people leave.",[219,387,388],{},"Better, more resilient designs because more minds understand the system.",[219,390,391],{},"Teams that adapt quickly to change instead of freezing when a key person is unavailable.",[219,393,394],{},"A working environment that is simply more enjoyable to be part of.",[11,396,397,398,401],{},"The strongest engineering teams are not the ones with the most individually talented people. They are the ones that have built the ",[204,399,400],{},"capacity to grow together",".",[11,403,404],{},"That capacity, shared learning, shared context, shared ownership, is the most valuable asset a technology organisation can build.",{"title":47,"searchDepth":61,"depth":61,"links":406},[407,408,409,410,411,412],{"id":192,"depth":67,"text":193},{"id":210,"depth":67,"text":211},{"id":264,"depth":67,"text":265},{"id":292,"depth":67,"text":293},{"id":316,"depth":67,"text":317},{"id":376,"depth":67,"text":377},"2026-02-05T09:00:00.000Z","The strongest engineering teams are not the ones with the most talented individuals. They are the ones that have learned how to learn together.",{},"\u002Fblog\u002Fbuilding-engineering-teams-that-learn-together",{"title":179,"description":414},"blog\u002Fbuilding-engineering-teams-that-learn-together","cOw6vPQkIEZvCptXR1l4LoqNmREbmm7mlTYj0WI4KQk",{"id":421,"title":422,"author":423,"body":424,"date":676,"description":677,"draft":161,"extension":162,"image":678,"meta":679,"navigation":165,"path":680,"readingMinutes":167,"seo":681,"stem":682,"tags":163,"__hash__":683},"blog\u002Fblog\u002Fdockerising-nestjs-nuxt-monorepo-with-pnpm.md","Dockerising a NestJS and Nuxt Monorepo with pnpm","Ogochukwu Okpala",{"type":8,"value":425,"toc":657},[426,429,432,449,453,456,459,462,466,469,484,488,491,494,498,515,518,521,525,529,532,535,539,542,545,549,552,556,559,569,581,584,591,594,600,604,607,610,613,617,620,626,632,638,644,648,651,654],[11,427,428],{},"If you run a monorepo with a NestJS backend and a Nuxt frontend, you have probably wondered how to Dockerise it properly. Development should feel local and fast. Production should be lean and predictable. And the whole thing should work the same way on every machine, every time.",[11,430,431],{},"This post walks through a practical approach to achieving exactly that. No gatekeeping, no unnecessary complexity. Just a clear setup that separates development from production and keeps your team moving.",[11,433,434],{},[204,435,436,437,448],{},"This article is adapted from ",[204,438,439],{},[440,441,445],"a",{"href":442,"rel":443},"https:\u002F\u002Fmedium.com\u002F@okpalaogochukwu76\u002Fhow-i-dockerized-a-nestjs-nuxt-monorepo-with-pnpm-without-losing-my-mind-572b9d215ddf",[444],"nofollow",[204,446,447],{},"the original post on Medium"," by Ogochukwu Okpala.",[21,450,452],{"id":451},"docker-is-just-another-linux-machine","Docker Is Just Another Linux Machine",[11,454,455],{},"A Docker container is not magic. It is simply a fresh Linux machine with its own filesystem, its own users, and its own network. It knows nothing about your laptop. Every file it needs must be explicitly copied in. Every tool it uses must be explicitly installed.",[11,457,458],{},"Once you accept that mental model, most Docker questions answer themselves. \"Why do I need to copy this file?\" Because the container does not have it. \"Why is my app not reachable?\" Because the container's network is isolated from yours.",[11,460,461],{},"This mindset makes Docker far less intimidating and far more logical.",[21,463,465],{"id":464},"the-monorepo-structure","The Monorepo Structure",[11,467,468],{},"The setup assumes a standard pnpm workspace monorepo. Your backend and frontend live as separate applications under a shared root, with a single lockfile and workspace configuration at the top level.",[11,470,471,472,475,476,479,480,483],{},"The key files at the root are your ",[49,473,474],{},"package.json",", your ",[49,477,478],{},"pnpm-workspace.yaml",", and your ",[49,481,482],{},"pnpm-lock.yaml",". Each app has its own dependencies, its own build process, and its own Dockerfile.",[21,485,487],{"id":486},"corepack-and-pnpm-reproducible-installs","Corepack and pnpm: Reproducible Installs",[11,489,490],{},"Node ships with Corepack, which is essentially a package manager manager. Since the project uses pnpm, you enable Corepack in your Dockerfile and lock pnpm to a specific major version.",[11,492,493],{},"This single step means every developer, every CI pipeline, and every container uses the exact same version of pnpm. No more \"it works on my machine\" surprises caused by version drift. Builds become reproducible by default.",[21,495,497],{"id":496},"multi-stage-dockerfiles","Multi Stage Dockerfiles",[11,499,500,501,504,505,504,508,511,512,401],{},"Both the backend and frontend Dockerfiles follow the same multi stage pattern with four stages: ",[222,502,503],{},"base",", ",[222,506,507],{},"dev",[222,509,510],{},"build",", and ",[222,513,514],{},"prod",[11,516,517],{},"This distinction matters more than most people realise. Development containers are built for humans. They include hot reloading, debugging tools, and full dependency trees. Production containers are built for machines. They contain only what is strictly needed to run the compiled application.",[11,519,520],{},"These two environments should never look the same. Mixing them leads to bloated production images and fragile development workflows.",[21,522,524],{"id":523},"the-backend-nestjs","The Backend: NestJS",[190,526,528],{"id":527},"development","Development",[11,530,531],{},"In development, the backend container installs all dependencies and runs NestJS in debug mode with hot reloading. File changes on your local machine sync into the container, so the development experience feels natural.",[11,533,534],{},"One important detail: the container creates a dedicated non root user. Running as root inside a container is a common shortcut, but it introduces security risks and can cause file permission issues that surface at the worst possible time. Creating a proper user from the start avoids these problems entirely.",[190,536,538],{"id":537},"building-for-production","Building for Production",[11,540,541],{},"The build stage installs dependencies, copies source code, and compiles the application. Then it uses pnpm's deploy command to create a clean, self contained production bundle.",[11,543,544],{},"The deploy command is the key piece. It creates a standalone folder that contains only production dependencies and the compiled output. Think of it as generating a deployment artifact that has everything the application needs and nothing it does not.",[190,546,548],{"id":547},"the-production-image","The Production Image",[11,550,551],{},"The production stage starts from a fresh Node image, copies in the deployment artifact, and runs the compiled application directly with Node. No source files. No development dependencies. No pnpm. Just the runtime and the code.",[21,553,555],{"id":554},"the-frontend-nuxt","The Frontend: Nuxt",[190,557,528],{"id":558},"development-1",[11,560,561,562,565,566,401],{},"Nuxt development in Docker requires one critical configuration: binding the dev server to ",[49,563,564],{},"0.0.0.0"," instead of the default ",[49,567,568],{},"localhost",[11,570,571,572,574,575,577,578,580],{},"Inside a container, ",[49,573,568],{}," refers to the container itself, not your host machine. If the Nuxt dev server only listens on ",[49,576,568],{},", your browser cannot reach it. Binding to ",[49,579,564],{}," tells the server to accept connections from any network interface, which includes traffic from outside the container. Without this, you will see a blank page and wonder what went wrong.",[190,582,538],{"id":583},"building-for-production-1",[11,585,586,587,590],{},"Nuxt compiles into a ",[49,588,589],{},".output"," folder that contains everything needed to run the application. The build stage installs dependencies, copies source code, runs the build, and then prunes away development dependencies.",[190,592,548],{"id":593},"the-production-image-1",[11,595,596,597,599],{},"Nuxt's production image is remarkably clean. You copy the ",[49,598,589],{}," directory and any static assets into a fresh Node image and run the server entry point. No build tools, no package manager, no source code. Just Node serving the compiled application.",[21,601,603],{"id":602},"docker-compose-bringing-it-together","Docker Compose: Bringing It Together",[11,605,606],{},"With both Dockerfiles in place, Docker Compose acts as the orchestrator. Using profiles, you can define separate configurations for development and production.",[11,608,609],{},"In development, both services run together with file watchers and hot reloading active. Changes to your source code reflect immediately. Changes to your dependency files trigger rebuilds. The experience mirrors local development, but inside containers that match your deployment environment.",[11,611,612],{},"In production, the same Compose file spins up clean, compiled containers with no watchers and no rebuild logic. Just the applications running as they would in a real deployment.",[21,614,616],{"id":615},"what-this-gets-you","What This Gets You",[11,618,619],{},"This approach gives your team several things that compound over time.",[11,621,622,625],{},[222,623,624],{},"Consistency."," Every environment, from a developer's laptop to CI to production, runs the same way. Environment specific bugs become rare.",[11,627,628,631],{},[222,629,630],{},"Clean separation."," Development and production never share the same concerns. Your development workflow stays fast and ergonomic. Your production images stay small and secure.",[11,633,634,637],{},[222,635,636],{},"Simplicity."," Despite involving multiple stages and two separate applications, the setup follows a predictable pattern. Once you understand the backend Dockerfile, the frontend Dockerfile feels familiar.",[11,639,640,643],{},[222,641,642],{},"Ownership."," Your team controls the full deployment pipeline. There is no mystery about what runs in production or how it got there.",[21,645,647],{"id":646},"final-thoughts","Final Thoughts",[11,649,650],{},"Dockerising a monorepo is not inherently difficult. It simply requires you to be deliberate about what each environment needs and disciplined about keeping them separate.",[11,652,653],{},"Docker is strict, not hard. pnpm is honest, not complicated. And monorepos work well when you structure them with intention.",[11,655,656],{},"Once everything fits together, the setup feels clean, predictable, and easy to reason about. That is the kind of foundation that serves a team well as a project grows.",{"title":47,"searchDepth":61,"depth":61,"links":658},[659,660,661,662,663,668,673,674,675],{"id":451,"depth":61,"text":452},{"id":464,"depth":61,"text":465},{"id":486,"depth":61,"text":487},{"id":496,"depth":61,"text":497},{"id":523,"depth":61,"text":524,"children":664},[665,666,667],{"id":527,"depth":67,"text":528},{"id":537,"depth":67,"text":538},{"id":547,"depth":67,"text":548},{"id":554,"depth":61,"text":555,"children":669},[670,671,672],{"id":558,"depth":67,"text":528},{"id":583,"depth":67,"text":538},{"id":593,"depth":67,"text":548},{"id":602,"depth":61,"text":603},{"id":615,"depth":61,"text":616},{"id":646,"depth":61,"text":647},"2026-02-09T09:00:00.000Z","A practical guide to Dockerising a NestJS and Nuxt monorepo using pnpm, with separate multi stage Dockerfiles for development and production and a single Docker Compose setup for consistent, reproducible environments.","\u002Fimages\u002Fblog\u002Fdocker-monorepo-hero.png",{},"\u002Fblog\u002Fdockerising-nestjs-nuxt-monorepo-with-pnpm",{"title":422,"description":677},"blog\u002Fdockerising-nestjs-nuxt-monorepo-with-pnpm","iDe3xykVCQQEbBv9fWTMz2o7nmeCZ8EqbK1GcfO1X0Y",{"id":685,"title":686,"author":6,"body":687,"date":163,"description":712,"draft":161,"extension":162,"image":163,"meta":713,"navigation":165,"path":714,"readingMinutes":55,"seo":715,"stem":716,"tags":163,"__hash__":717},"blog\u002Fblog\u002Fstage-then-transform-why-your-crawler-should-never-touch-production-tables.md","Stage, Then Transform: Why Your Crawler Should Never Touch Production Tables",{"type":8,"value":688,"toc":709},[689,693,699,702,705],[690,691,686],"h1",{"id":692},"stage-then-transform-why-your-crawler-should-never-touch-production-tables",[11,694,695,696],{},"When you pull data from an external source, a public register, a CSV feed, a scraped site, there is one decision that quietly determines whether the pipeline will still be working in six months. It is not “which library”, “which schedule”, or “which database”. It is: ",[222,697,698],{},"where does the data land first?",[11,700,701],{},"Most pipelines answer that question badly. They crawl a source, massage the rows in-memory, and write straight into the production table the app already reads from. It works on day one. It breaks on day ninety.",[11,703,704],{},"This post is the pattern I wish I had internalised before I wrote my first crawler, explained with the pipeline we are building right now at Propi: matching first-time buyers to UK conveyancers using public SRA and Law Society data.",[21,706,708],{"id":707},"the-shortcut-that-bites","The shortcut that bites",{"title":47,"searchDepth":61,"depth":61,"links":710},[711],{"id":707,"depth":61,"text":708},"When you pull data from an external source, the decision that quietly determines whether the pipeline will still be working in six months is where the data lands first. Most pipelines answer that question badly. Here is the pattern I wish I had internalised before I wrote my first crawler, explained with the pipeline we are building right now at Propi.",{},"\u002Fblog\u002Fstage-then-transform-why-your-crawler-should-never-touch-production-tables",{"title":686,"description":712},"blog\u002Fstage-then-transform-why-your-crawler-should-never-touch-production-tables","Pf2NUUp8Rfg13SL42WxDKM5J5609bgM0h_6RObeaWj4",{"id":719,"title":720,"author":180,"body":721,"date":740,"description":741,"draft":161,"extension":162,"image":163,"meta":742,"navigation":165,"path":743,"readingMinutes":61,"seo":744,"stem":745,"tags":163,"__hash__":746},"blog\u002Fblog\u002Ftest-driven-development-is-not-about-tests.md","Test Driven Development Is Not About Tests",{"type":8,"value":722,"toc":738},[723,726,729,732,735],[11,724,725],{},"TDD is best understood as a design discipline rather than a testing technique. The tests are a valuable byproduct, but the real benefit is how the red, green, refactor cycle shapes your thinking about interfaces, responsibilities, and simplicity.",[11,727,728],{},"By forcing you to define behaviour before implementation, TDD pushes you to design from the outside in: you clarify what a unit should do, how it should be used, and what its boundaries are, before worrying about how it works internally. This naturally leads to smaller, more focused functions, clearer contracts, and fewer hidden dependencies.",[11,730,731],{},"The discipline of writing only the simplest code to make a failing test pass counteracts the common tendency to over‑engineer and speculate about future requirements. Instead of building speculative abstractions, you evolve the design incrementally, guided by concrete, executable examples of desired behaviour. Refactoring with a safety net of tests then lets you continuously improve structure without fear of breaking existing behaviour.",[11,733,734],{},"Objections about TDD being slow or unsuitable for complex systems usually stem from treating it as “tests first” for coverage rather than as a design loop. At different scales you adjust the granularity of tests, from small unit tests to higher‑level acceptance or integration tests, but the core principle remains: clarify intent, then implement, then refine.",[11,736,737],{},"Practising TDD on small, well‑bounded problems is the fastest way to internalise this rhythm. Over time, the habit of thinking in terms of observable behaviour, clear interfaces, and minimal implementations carries over even when you are not formally doing TDD. That is why, ultimately, TDD is not about tests; it is about cultivating a deliberate way of thinking before you type.",{"title":47,"searchDepth":61,"depth":61,"links":739},[],"2026-02-01T09:00:00.000Z","TDD is widely misunderstood. It is not a testing strategy. It is a design tool that helps you write simpler, more focused code by forcing you to think before you type.",{},"\u002Fblog\u002Ftest-driven-development-is-not-about-tests",{"title":720,"description":741},"blog\u002Ftest-driven-development-is-not-about-tests","yHSx59T_j29Is1mTJpwo-KbsiK28Uy-pgg_hoFvnFaM",{"id":748,"title":749,"author":180,"body":750,"date":903,"description":904,"draft":161,"extension":162,"image":163,"meta":905,"navigation":165,"path":906,"readingMinutes":73,"seo":907,"stem":908,"tags":163,"__hash__":909},"blog\u002Fblog\u002Fthe-dojo-way-deliberate-practice-for-software-developers.md","The Dojo Way: Deliberate Practice for Software Developers",{"type":8,"value":751,"toc":895},[752,755,758,762,765,768,771,775,778,784,790,796,800,803,806,838,841,845,848,851,854,858,861,864,867,871,874,880,886,892],[11,753,754],{},"In martial arts, a dojo is not just a training hall. It is a philosophy. A place where you practise the fundamentals until they become second nature. Where the emphasis is not on learning new techniques every week, but on mastering the ones you already know.",[11,756,757],{},"Software development could learn a lot from this approach.",[21,759,761],{"id":760},"the-problem-with-learning-on-the-job","The Problem with Learning on the Job",[11,763,764],{},"Most developers learn primarily through their work. They pick up new frameworks when a project demands it. They learn about testing when a bug slips through. They discover the importance of clean architecture when a codebase becomes impossible to maintain.",[11,766,767],{},"This is learning through consequence, and while it works eventually, it is slow, painful, and expensive. You are essentially using production systems as your training ground.",[11,769,770],{},"Imagine a surgeon who only practised during real operations. Or a pilot who only trained during commercial flights. The idea sounds absurd, yet this is exactly how most software teams operate.",[21,772,774],{"id":773},"what-deliberate-practice-means","What Deliberate Practice Means",[11,776,777],{},"Deliberate practice is a concept popularised by psychologist Anders Ericsson. It has three key characteristics:",[11,779,780,783],{},[222,781,782],{},"It is focused."," You work on a specific skill, not everything at once. Today you practise writing clean functions. Tomorrow you practise refactoring legacy code. The day after, you work on test design.",[11,785,786,789],{},[222,787,788],{},"It involves feedback."," You need someone or something to tell you what you are doing well and what needs improvement. In a dojo, that is your instructor. In software, it can be a mentor, a pair programming partner, or a well designed code review process.",[11,791,792,795],{},[222,793,794],{},"It pushes you beyond your comfort zone."," If the exercise is easy, you are not learning. Deliberate practice should feel challenging. That discomfort is where growth happens.",[21,797,799],{"id":798},"how-the-dojo-model-works","How the Dojo Model Works",[11,801,802],{},"Our workshops are built around these principles. Each session has a clear skill focus. Participants write code, get immediate feedback, and iterate. There are no slides. There is no sitting and listening for hours.",[11,804,805],{},"A typical dojo session looks like this:",[216,807,808,814,820,826,832],{},[219,809,810,813],{},[222,811,812],{},"Introduction"," (10 minutes): The facilitator explains the concept and the exercise.",[219,815,816,819],{},[222,817,818],{},"First attempt"," (30 minutes): Participants work through the problem, usually in pairs.",[219,821,822,825],{},[222,823,824],{},"Review"," (15 minutes): The group discusses approaches, trade offs, and common mistakes.",[219,827,828,831],{},[222,829,830],{},"Second attempt"," (30 minutes): Armed with new understanding, participants try again.",[219,833,834,837],{},[222,835,836],{},"Reflection"," (15 minutes): What did you learn? What would you do differently?",[11,839,840],{},"The magic is in the second attempt. By the time you come back to the problem, you have heard other perspectives, identified your own blind spots, and formed a clearer mental model. The improvement between the first and second attempt is often dramatic.",[21,842,844],{"id":843},"repetition-is-not-boring","Repetition Is Not Boring",[11,846,847],{},"One objection we hear regularly is \"why would I solve the same problem twice?\" This reveals a misunderstanding about what repetition does for skill development.",[11,849,850],{},"A musician does not play a piece once and move on. They play it hundreds of times, each time refining their technique, their timing, their expression. The goal is not to get through the piece. The goal is to internalise the patterns so deeply that they become automatic.",[11,852,853],{},"The same applies to software. When you practise test driven development on a simple problem, you are not learning that specific problem. You are building the neural pathways for a way of thinking. You are training your instincts. So that when you face a complex, high pressure situation in production code, the approach comes naturally.",[21,855,857],{"id":856},"building-a-practice-culture","Building a Practice Culture",[11,859,860],{},"The most powerful thing about the dojo model is not what happens during the session. It is what happens afterwards. Teams that embrace deliberate practice start to see their daily work differently.",[11,862,863],{},"Code reviews become learning opportunities, not gatekeeping exercises. Pair programming becomes a natural way to share knowledge, not an awkward obligation. Retrospectives become genuine reflection, not a ritual to endure.",[11,865,866],{},"This cultural shift is what separates teams that grow from teams that simply accumulate experience. Experience without reflection is just repetition. Experience with deliberate reflection is how you build genuine expertise.",[21,868,870],{"id":869},"starting-small","Starting Small",[11,872,873],{},"You do not need a formal programme to start practising deliberately. Here are three things any team can do this week:",[11,875,876,879],{},[222,877,878],{},"Set aside one hour."," Pick a problem from an online kata catalogue. Work through it as a team, focusing on clean code rather than speed.",[11,881,882,885],{},[222,883,884],{},"Review your own code from six months ago."," What would you do differently? What patterns do you see now that you missed then?",[11,887,888,891],{},[222,889,890],{},"Pair on something unfamiliar."," Find the area of your codebase that nobody understands and explore it together. Ask questions. Draw diagrams. Build understanding.",[11,893,894],{},"The dojo is not a place. It is a mindset. And it starts with the decision to treat your craft seriously enough to practise it.",{"title":47,"searchDepth":61,"depth":61,"links":896},[897,898,899,900,901,902],{"id":760,"depth":61,"text":761},{"id":773,"depth":61,"text":774},{"id":798,"depth":61,"text":799},{"id":843,"depth":61,"text":844},{"id":856,"depth":61,"text":857},{"id":869,"depth":61,"text":870},"2026-01-22T09:00:00.000Z","Most developers learn on the job, but the job rarely teaches you the fundamentals. The Dojo approach borrows from martial arts to create a structured space for deliberate practice.",{},"\u002Fblog\u002Fthe-dojo-way-deliberate-practice-for-software-developers",{"title":749,"description":904},"blog\u002Fthe-dojo-way-deliberate-practice-for-software-developers","7_w2L20WoAIgq1ORJuuLAgZ-YMJt3sz_8yTyMkm2JLY",{"id":4,"title":5,"author":6,"body":911,"date":159,"description":160,"draft":161,"extension":162,"image":163,"meta":1009,"navigation":165,"path":166,"readingMinutes":167,"seo":1010,"stem":169,"tags":1011,"__hash__":175},{"type":8,"value":912,"toc":1003},[913,915,917,919,921,923,925,927,929,931,951,953,977,979,981,983,985,987,989,991,993,995,997,999,1001],[11,914,13],{},[11,916,16],{},[11,918,19],{},[21,920,24],{"id":23},[11,922,27],{},[11,924,30],{},[11,926,33],{},[21,928,37],{"id":36},[11,930,40],{},[42,932,933],{"className":44,"code":45,"language":46,"meta":47,"style":47},[49,934,935,939,943,947],{"__ignoreMap":47},[52,936,937],{"class":54,"line":55},[52,938,58],{},[52,940,941],{"class":54,"line":61},[52,942,64],{},[52,944,945],{"class":54,"line":67},[52,946,70],{},[52,948,949],{"class":54,"line":73},[52,950,76],{},[11,952,79],{},[42,954,955],{"className":44,"code":82,"language":46,"meta":47,"style":47},[49,956,957,961,965,969,973],{"__ignoreMap":47},[52,958,959],{"class":54,"line":55},[52,960,89],{},[52,962,963],{"class":54,"line":61},[52,964,94],{},[52,966,967],{"class":54,"line":67},[52,968,99],{},[52,970,971],{"class":54,"line":73},[52,972,104],{},[52,974,975],{"class":54,"line":107},[52,976,110],{},[11,978,113],{},[21,980,117],{"id":116},[11,982,120],{},[11,984,123],{},[11,986,126],{},[11,988,129],{},[21,990,133],{"id":132},[11,992,136],{},[11,994,139],{},[11,996,142],{},[11,998,145],{},[11,1000,148],{},[150,1002,152],{},{"title":47,"searchDepth":61,"depth":61,"links":1004},[1005,1006,1007,1008],{"id":23,"depth":61,"text":24},{"id":36,"depth":61,"text":37},{"id":116,"depth":61,"text":117},{"id":132,"depth":61,"text":133},{},{"title":5,"description":160},[171,172,173,174],{"id":1013,"title":1014,"author":6,"body":1015,"date":159,"description":1089,"draft":161,"extension":162,"image":1090,"meta":1091,"navigation":165,"path":1092,"readingMinutes":73,"seo":1093,"stem":1094,"tags":1095,"__hash__":1099},"blog\u002Fblog\u002Fthe-workflow-is-the-lesson.md","The Workflow Is the Lesson",{"type":8,"value":1016,"toc":1082},[1017,1020,1023,1026,1030,1033,1036,1040,1043,1046,1050,1053,1056,1059,1062,1066,1069,1072,1076,1079],[11,1018,1019],{},"Where your content lives looks like a technical decision. For a school, it is a teaching one.",[11,1021,1022],{},"We teach a way of working that is keyboard first and code first. A workflow you can drive without reaching for a mouse, without hunting through a web interface, without waiting on a screen to load. We do this partly for speed and focus, and partly for accessibility. A workflow built from text, files, and the keyboard is one more people can reach, automate, and own.",[11,1024,1025],{},"For a while, the words on this site lived somewhere that quietly worked against that. This is the story of bringing them home, and the plan we are following to do it without stopping the work.",[21,1027,1029],{"id":1028},"why-a-cloud-cms-stopped-serving-us","Why a cloud CMS stopped serving us",[11,1031,1032],{},"Our writing has been living in a hosted content service, kept apart from the code that renders it. On paper that is a clean separation. In practice, for us, it added friction at every turn. Edits did not appear on our local site without a refresh. Drafts we could not see. A second system to keep in step with git. A way of working that leaned on a pointer and a browser tab rather than the keyboard.",[11,1034,1035],{},"The tool itself is capable. It is built for teams of authors publishing constantly without ever touching code, and that is a real and common need. It was not ours. We are a small, code first team, and every edit we make is already a commit waiting to happen.",[21,1037,1039],{"id":1038},"what-we-actually-want","What we actually want",[11,1041,1042],{},"Content as plain files. Living in git, beside the code, with one history and one source of truth. Editable in the same editor we write software in, reloading the moment we save. A workflow a new teammate can learn in an afternoon, because it is the same workflow they already use for code. Write, review, commit, push.",[11,1044,1045],{},"This is the lesson hiding inside the tooling. The tools we teach should be the tools we use. If we ask students to own their work through git, we should hold our own words the same way.",[21,1047,1049],{"id":1048},"the-plan-in-two-moves","The plan, in two moves",[11,1051,1052],{},"We have a live site to build and a deadline that does not care about elegance. So we are doing this in two honest steps rather than one perfect leap.",[11,1054,1055],{},"First, Nuxt Content. Our site already runs on Nuxt, so we are moving our writing into markdown files that Nuxt reads directly. Same framework, same components, no rebuild of the house. We get files in git, hot reload, and a single source of truth now, rather than next quarter.",[11,1057,1058],{},"Then, bit by bit, Astro and Keystatic. Once the live site is standing, we migrate piece by piece toward the end we actually want. Astro for a content site that ships almost no JavaScript, and Keystatic for a calm editor that writes those same files straight back into git. Authors who do not live in a terminal still get a gentle interface. Nothing leaves the repository. Publishing stays a commit.",[11,1060,1061],{},"We are naming the order plainly, because the order is the point. The cheap, reversible step first. The deeper change second, in small pieces we can review.",[21,1063,1065],{"id":1064},"doing-it-in-the-open","Doing it in the open",[11,1067,1068],{},"This post is the first piece of writing to live in the new way. It is a markdown file in our repository, and publishing it is a commit. We are documenting the decision here, including the parts that are unfinished, because a school that learns in the open should show its working, not only its conclusions.",[11,1070,1071],{},"We will be wrong about some of this. We will find something markdown handles badly, or a step that is harder than it looked. When we do, we will write that down too.",[21,1073,1075],{"id":1074},"what-we-are-really-practising","What we are really practising",[11,1077,1078],{},"None of this is about a framework. It is about ownership. Holding your work somewhere you can read it, version it, and pass it on. It is about curiosity. Asking what a tool is really for before reaching for it. And it is about humility. Being willing to move off a choice we made a year ago because we have learned more since.",[11,1080,1081],{},"The workflow is not the dull setup you rush through before the real teaching begins. The workflow is the lesson.",{"title":47,"searchDepth":61,"depth":61,"links":1083},[1084,1085,1086,1087,1088],{"id":1028,"depth":61,"text":1029},{"id":1038,"depth":61,"text":1039},{"id":1048,"depth":61,"text":1049},{"id":1064,"depth":61,"text":1065},{"id":1074,"depth":61,"text":1075},"We are moving our content out of a cloud CMS and into git. Here is why a school that teaches a keyboard first, code first way of working should practise it in the open, and the path we are taking to get there.","\u002Fimages\u002Fblog\u002Fworkflow-lesson-hero.png",{},"\u002Fblog\u002Fthe-workflow-is-the-lesson",{"title":1014,"description":1089},"blog\u002Fthe-workflow-is-the-lesson",[1096,1097,174,1098],"workflow","git","accessibility","VfRTGPa_IYj7FigiRkdwq-CLIiEo9uj2gyEWsZq08t0",{"id":1101,"title":1102,"author":180,"body":1103,"date":1207,"description":1208,"draft":161,"extension":162,"image":163,"meta":1209,"navigation":165,"path":1210,"readingMinutes":73,"seo":1211,"stem":1212,"tags":163,"__hash__":1213},"blog\u002Fblog\u002Fwhy-good-software-starts-with-the-right-questions.md","Why Good Software Starts with the Right Questions",{"type":8,"value":1104,"toc":1200},[1105,1108,1111,1114,1118,1121,1124,1127,1131,1134,1137,1140,1144,1147,1153,1159,1165,1171,1177,1181,1184,1187,1191,1194,1197],[11,1106,1107],{},"Most software projects do not fail because of bad code. They fail because the team built the wrong thing. Or they built the right thing for the wrong reasons. Or they solved a problem nobody actually had.",[11,1109,1110],{},"This is not a new observation. Teams have been talking about the importance of requirements gathering for decades. And yet the pattern repeats. A client walks in with a solution already in mind. The team nods along, opens their laptops, and starts building. Six months later, everyone is frustrated.",[11,1112,1113],{},"So what goes wrong?",[21,1115,1117],{"id":1116},"the-problem-with-starting-at-the-solution","The Problem with Starting at the Solution",[11,1119,1120],{},"When someone says \"we need a mobile app\" or \"we need to migrate to microservices,\" they are usually describing a solution, not a problem. The real question is: what is the outcome you are trying to achieve?",[11,1122,1123],{},"A mobile app might be the answer. But it might not. Maybe the real issue is that field workers cannot access their data offline. Maybe the root cause is a poorly designed internal tool that nobody wants to use. Maybe the actual need is better notifications, not an entirely new platform.",[11,1125,1126],{},"You will never know unless you ask.",[21,1128,1130],{"id":1129},"what-good-discovery-looks-like","What Good Discovery Looks Like",[11,1132,1133],{},"Good discovery is not about filling out templates or running workshops for the sake of it. It is about creating the conditions for honest conversation.",[11,1135,1136],{},"That means sitting with the people who will actually use the software. Not just the stakeholders who commissioned it, but the people on the ground. The customer service team answering calls at midnight. The warehouse operator scanning barcodes in poor lighting. The finance team copying data between three different spreadsheets.",[11,1138,1139],{},"These conversations are where the real requirements live. Not in a boardroom, but in the day to day friction that people have learned to work around.",[21,1141,1143],{"id":1142},"questions-worth-asking","Questions Worth Asking",[11,1145,1146],{},"Here are some questions that consistently lead to better outcomes:",[11,1148,1149,1152],{},[222,1150,1151],{},"What does success look like in six months?"," This forces clarity. If the answer is vague, the project scope will be vague too.",[11,1154,1155,1158],{},[222,1156,1157],{},"What are you doing today that you wish you could stop doing?"," This reveals the actual pain points, not the imagined ones.",[11,1160,1161,1164],{},[222,1162,1163],{},"Who else is affected by this?"," Software rarely exists in isolation. Understanding the wider system is crucial.",[11,1166,1167,1170],{},[222,1168,1169],{},"What have you tried before?"," Past failures are full of lessons. Ignoring them means repeating them.",[11,1172,1173,1176],{},[222,1174,1175],{},"What would make this project not worth doing?"," This surfaces the constraints and deal breakers early, when they are cheapest to address.",[21,1178,1180],{"id":1179},"discovery-is-not-a-phase","Discovery Is Not a Phase",[11,1182,1183],{},"One of the most common mistakes is treating discovery as something you do once at the start and then move on from. In reality, discovery should be continuous. Every sprint, every review, every conversation with a user is an opportunity to learn something new.",[11,1185,1186],{},"The best teams we have worked with treat curiosity as a core discipline. They are not afraid to say \"we were wrong\" or \"we have learned something that changes our approach.\" They build in time to reflect, not just to build.",[21,1188,1190],{"id":1189},"why-this-matters-for-consulting","Why This Matters for Consulting",[11,1192,1193],{},"At EkoHacks, we have seen the difference that good discovery makes. Projects that start with genuine curiosity tend to deliver faster, cost less, and produce software that people actually want to use.",[11,1195,1196],{},"This is not about being slow or cautious. It is about being deliberate. The time you invest in understanding the problem properly is time you save later by not building the wrong thing.",[11,1198,1199],{},"If you take one thing away from this, let it be this: the quality of your software is directly proportional to the quality of your questions. Start there, and the rest follows.",{"title":47,"searchDepth":61,"depth":61,"links":1201},[1202,1203,1204,1205,1206],{"id":1116,"depth":61,"text":1117},{"id":1129,"depth":61,"text":1130},{"id":1142,"depth":61,"text":1143},{"id":1179,"depth":61,"text":1180},{"id":1189,"depth":61,"text":1190},"2026-01-15T09:00:00.000Z","Before writing a single line of code, the most impactful thing a software team can do is ask better questions. Discovery is not a phase you rush through. It is the foundation everything else rests on.",{},"\u002Fblog\u002Fwhy-good-software-starts-with-the-right-questions",{"title":1102,"description":1208},"blog\u002Fwhy-good-software-starts-with-the-right-questions","MzBsSAbA6tqnZLFgPeEE-ryCdy9EuqxTyT4K94KKHxo",1782301565120]