[{"data":1,"prerenderedAt":520},["ShallowReactive",2],{"\u002Farticles\u002Fauditing-my-own-site-after-writing-about-seo":3,"article-series:none":457},{"id":4,"title":5,"body":6,"description":439,"draft":440,"extension":441,"meta":442,"navigation":443,"path":444,"published":445,"seo":446,"seoDescription":447,"series":448,"seriesOrder":448,"stem":449,"tags":450,"updated":448,"__hash__":456},"articles\u002Farticles\u002Fauditing-my-own-site-after-writing-about-seo.md","I Wrote About SEO, Then Audited My Own Site",{"type":7,"value":8,"toc":419},"minimark",[9,38,41,44,49,52,55,63,92,95,99,102,130,133,137,144,149,156,160,163,166,173,177,180,183,260,263,266,270,276,280,283,290,293,297,300,304,307,310,314,317,332,335,338,341,345,348,351,354,358,395,399],[10,11,12,19],"blockquote",{},[13,14,15],"p",{},[16,17,18],"strong",{},"In short",[20,21,22,32,35],"ul",{},[23,24,25,26,31],"li",{},"I published ",[27,28,30],"a",{"href":29},"\u002Farticles\u002Fsubsetting-material-symbols-icon-font\u002F","an article about guarding a performance fix"," on a site with no guards of its own.",[23,33,34],{},"One idle question afterwards turned up three SEO bugs, and the checks I added found a fourth. All of them were mine, and one had been live for months.",[23,36,37],{},"They are fixed now, with checks so they stay fixed. Some things are still not done, and they are listed at the end.",[13,39,40],{},"My last article ended on a neat loop: identify the problem, understand it, fix it, prove the fix, then guard it so it stays fixed. It reads well. I had not been applying it to my own site.",[13,42,43],{},"This is what happened when I checked.",[45,46,48],"h2",{"id":47},"i-did-not-plan-an-audit","I did not plan an audit",[13,50,51],{},"There was no plan here. The article went live, and a few minutes later I asked the AI assistant I had been writing with a lazy question: was SEO handled on that page?",[13,53,54],{},"I expected \"yes\". I had set the site up with a proper SEO module, and it generates titles, descriptions, social images, structured data and a sitemap without me thinking about it. That was the problem: I had stopped thinking about it.",[13,56,57,58,62],{},"The assistant fetched the live page and read its ",[59,60,61],"code",{},"\u003Chead>",". Most of it was fine. Three things were not:",[64,65,66,72,78],"ol",{},[23,67,68,71],{},[16,69,70],{},"The title was too long."," With my name on the end it was 81 characters. Google shows about 60, so the end was cut off. I had spent a long time polishing that article and never looked at it the way a search result would show it.",[23,73,74,77],{},[16,75,76],{},"The description was out of date."," The article had changed shape during editing, from a how-to into a case study. The search snippet still described the how-to.",[23,79,80,83,84,87,88,91],{},[16,81,82],{},"Every canonical URL on the site pointed at a redirect."," The site is generated as static pages, which Netlify serves at ",[59,85,86],{},"\u002Fpath\u002F"," with a trailing slash. The canonical tags said ",[59,89,90],{},"\u002Fpath"," without one, which Netlify redirects. So every page was telling search engines \"the real page is somewhere else\", and somewhere else bounced back.",[13,93,94],{},"None of these broke anything a visitor would see. That is exactly why they had survived.",[45,96,98],{"id":97},"the-fixes-were-the-easy-part","The fixes were the easy part",[13,100,101],{},"Each fix was small:",[20,103,104,118,124],{},[23,105,106,109,110,113,114,117],{},[16,107,108],{},"One setting"," (",[59,111,112],{},"trailingSlash: true",") made canonicals, ",[59,115,116],{},"og:url",", the sitemap and structured data use the URL Netlify actually serves, and one more made internal links match.",[23,119,120,123],{},[16,121,122],{},"A shorter title:"," \"Material Symbols Was 97% Dead Weight\". It is 52 characters with my name, keeps the search term, and is more accurate than the original.",[23,125,126,129],{},[16,127,128],{},"A new description"," that matched the article it had become.",[13,131,132],{},"If I had stopped there, I would have fixed today's bugs and left the conditions that produced them exactly as they were.",[45,134,136],{"id":135},"guarding-it-starting-low","Guarding it, starting low",[13,138,139,140,143],{},"The site had no tests and no CI. Netlify built whatever I pushed. So I added three checks that run on every pull request and every change to ",[59,141,142],{},"main",".",[145,146,148],"h3",{"id":147},"the-link-checker-made-to-mean-it","The link checker, made to mean it",[13,150,151,152,155],{},"The SEO module had been running a link checker on every build since I rebuilt the site in Nuxt. It printed a summary, and nothing acted on it. One setting (",[59,153,154],{},"failOnError: true",") means a broken internal link now fails the build instead of going live. It does not fetch external links, so someone else's outage cannot block my deploy.",[145,157,159],{"id":158},"tests-on-the-built-pages","Tests on the built pages",[13,161,162],{},"The bugs only existed in the generated HTML, so the tests read the generated HTML. They check titles, descriptions, canonicals, the sitemap, drafts and 404s.",[13,164,165],{},"Eight older articles of mine have titles over 60 characters. The honest options were to shorten them all now or admit they exist, so they sit in an exceptions list. A test fails if the list grows, and another fails if one gets shortened but stays on it, so the list can only shrink. I have not shortened any of them yet.",[13,167,168,169,172],{},"I meant to use Vitest. Adding it made the package manager re-resolve every loosely pinned dependency, and one of them demanded a version that did not exist yet. The underlying issue is that my dependencies are pinned to ",[59,170,171],{},"latest",", which is now on the list below. I used Node's built-in test runner instead, which needs no dependency at all.",[145,174,176],{"id":175},"lighthouse-with-thresholds-i-can-actually-keep","Lighthouse, with thresholds I can actually keep",[13,178,179],{},"Lighthouse scores four pages on mobile on every run. The scores are currently good, but I do not yet know how much they wobble between runs, and a check that fails at random gets switched off. So the thresholds started well below the scores and only go up.",[13,181,182],{},"They did not stay there long. Over the first five runs no score moved by more than 2 points, so the next day I raised them:",[184,185,186,206],"table",{},[187,188,189],"thead",{},[190,191,192,196,200,203],"tr",{},[193,194,195],"th",{},"Category",[193,197,199],{"align":198},"right","Starting threshold",[193,201,202],{"align":198},"Threshold now",[193,204,205],{"align":198},"Scores",[207,208,209,224,236,249],"tbody",{},[190,210,211,215,218,221],{},[212,213,214],"td",{},"Performance",[212,216,217],{"align":198},"80",[212,219,220],{"align":198},"90",[212,222,223],{"align":198},"94 to 98",[190,225,226,229,231,234],{},[212,227,228],{},"Accessibility",[212,230,220],{"align":198},[212,232,233],{"align":198},"100",[212,235,233],{"align":198},[190,237,238,241,243,246],{},[212,239,240],{},"Best practices",[212,242,220],{"align":198},[212,244,245],{"align":198},"95",[212,247,248],{"align":198},"96 to 100",[190,250,251,254,256,258],{},[212,252,253],{},"SEO",[212,255,220],{"align":198},[212,257,233],{"align":198},[212,259,233],{"align":198},[13,261,262],{},"SEO at 100 is deliberate: on a site where I write about SEO, any failed SEO audit should stop a deploy. Accessibility went up for a different reason, covered below.",[13,264,265],{},"Each run keeps its reports for 90 days, and a log in the repository records every threshold change with a date, so the history lives with the code.",[145,267,269],{"id":268},"nothing-skips-the-checks","Nothing skips the checks",[13,271,272,273,275],{},"Checks only help if they cannot be skipped, and I had pushed straight to ",[59,274,142],{}," twice that day. A ruleset on the repository now requires every change to arrive as a pull request and pass the quality checks before it can merge, and nobody, including me, can push around it without switching it off first. This article went live through that same route.",[45,277,279],{"id":278},"the-guard-found-an-older-bug","The guard found an older bug",[13,281,282],{},"Writing the 404 test turned up the most useful find of the day.",[13,284,285,286,289],{},"The site started as a single-page Vue app, and Netlify needed a catch-all rule to serve it: ",[59,287,288],{},"\u002F* \u002Findex.html 200",". When I rebuilt the site as static pages in July, that rule came along. So for about four months, every address that did not exist returned my homepage with a \"200 OK\". Search engines call that a soft 404, and it makes a site look as if it has endless duplicate pages.",[13,291,292],{},"Nobody told me, because nobody would. A personal site has no users filing bugs. The fix was deleting one line, after checking that every route from the old app still existed as a real page.",[45,294,296],{"id":295},"proving-the-checks-work","Proving the checks work",[13,298,299],{},"A check I have never seen fail is a check I am trusting on faith. So I broke the built site on purpose: a canonical without the slash, an overlong title and the old catch-all rule. Each failed with a message naming the page and the problem. I also raised the Lighthouse threshold above the real score to make sure a failure actually fails the run.",[45,301,303],{"id":302},"what-the-checks-cannot-see","What the checks cannot see",[13,305,306],{},"Automated checks catch broken links and bad canonicals. They do not catch \"amazinglky\".",[13,308,309],{},"Reading the site the way a hiring manager would, rather than the way a build tool does, turned up four typos on my About page that had been there since 2023, and a job in my work history that appeared to start six months after it ended. None of it would ever fail a test. All of it was on the page people read to decide whether I am careful. The fixes took minutes; the lesson is that a site needs a reader as well as a test suite.",[45,311,313],{"id":312},"built-for-people-read-by-agents","Built for people, read by agents",[13,315,316],{},"After raising the thresholds, I ran the live homepage through Google's PageSpeed Insights. It has a newer category, still in development, called agentic browsing: how well an AI agent can read and use a page. My site failed one of its two checks.",[13,318,319,320,323,324,327,328,331],{},"The culprit was my work history. Each job was a ",[59,321,322],{},"\u003Cdl>",", a description list, holding values with no labels: three ",[59,325,326],{},"\u003Cdd>"," elements and no ",[59,329,330],{},"\u003Cdt>",". It looked fine, but anything reading the structure rather than the pixels got values without meaning.",[13,333,334],{},"That was not news to my own checks. The same problem was half of the homepage's accessibility score of 92, which my Lighthouse run had reported every time. My starting threshold of 90 let it pass. I had set it low on purpose, to allow for noise, and a low threshold hides real problems as easily as it tolerates noise.",[13,336,337],{},"The fix was three hidden labels (Company, Role and Dates), visible only to screen readers and anything else reading the structure. The homepage went from 92 to 96. The other half was grey text too faint to read comfortably, which one change to a shared style fixed in both light and dark mode. My first pass at it only proved half of that: my laptop is set to dark mode, so my local runs tested dark, while CI tested light and failed. Lighthouse now runs in both. Every page I test now scores 100 for accessibility, and the threshold went up to match, so either problem would fail the build if it came back.",[13,339,340],{},"The lesson is that many AI agents read a page the way a screen reader does, through its accessibility tree. Getting accessibility right was already the right thing to do; now it is also how a site gets read by the tools people increasingly browse with.",[45,342,344],{"id":343},"the-ai-made-mistakes-too","The AI made mistakes too",[13,346,347],{},"Most of this was done in one long session with an AI coding assistant, and it was not flawless.",[13,349,350],{},"It was fast and it was thorough, but it also counted things wrong, made claims a little stronger than the evidence, and wrote a first title for this very article that was 66 characters long, breaking the 60-character rule it had just helped write. It caught that one itself, by measuring. It also told me a sentence on my About page was unfinished, when it had only read a truncated copy of the line; the sentence was fine, and it corrected itself once it read the whole file. Other mistakes I caught by reading carefully, and some I probably have not caught.",[13,352,353],{},"The useful part was never that the tools were right. It was that they answered quickly when asked, and I kept asking. Was SEO handled? Does the canonical match what is served? What happens at an address that does not exist? None of those questions came from the tools.",[45,355,357],{"id":356},"what-is-still-not-done","What is still not done",[20,359,360,366,371,381,389],{},[23,361,362,365],{},[16,363,364],{},"Search Console is not set up."," I decided it is worth having, since it shows what Google actually indexed, and have not set it up yet.",[23,367,368],{},[16,369,370],{},"The eight long titles are still long.",[23,372,373,376,377,380],{},[16,374,375],{},"The linter does not run."," ",[59,378,379],{},"eslint"," was never installed, which I found during this work and left for later.",[23,382,383,388],{},[16,384,385,386],{},"Dependencies are still pinned to ",[59,387,171],{},", which is how the test framework broke.",[23,390,391,394],{},[16,392,393],{},"My own checks do not include agentic browsing yet."," The Lighthouse version my CI runs does not have that category yet, so for now it is a manual check in PageSpeed Insights.",[45,396,398],{"id":397},"what-i-would-tell-myself","What I would tell myself",[20,400,401,404,407,410,413,416],{},[23,402,403],{},"Check your own work the way you would check a colleague's. I published an article about guard rails on a site with none.",[23,405,406],{},"Test what you ship, not just what you write. Every one of these bugs lived in the generated output.",[23,408,409],{},"Start checks where you can keep them, then ratchet up. A strict check that gets disabled protects nothing.",[23,411,412],{},"A threshold below today's score tolerates noise, and it also hides real problems.",[23,414,415],{},"Personal projects rot quietly because nobody reports the bugs. That is a reason to automate the checks, not to skip them.",[23,417,418],{},"AI tools make the work faster. Asking the right questions is still the job.",{"title":420,"searchDepth":421,"depth":421,"links":422},"",2,[423,424,425,432,433,434,435,436,437,438],{"id":47,"depth":421,"text":48},{"id":97,"depth":421,"text":98},{"id":135,"depth":421,"text":136,"children":426},[427,429,430,431],{"id":147,"depth":428,"text":148},3,{"id":158,"depth":428,"text":159},{"id":175,"depth":428,"text":176},{"id":268,"depth":428,"text":269},{"id":278,"depth":421,"text":279},{"id":295,"depth":421,"text":296},{"id":302,"depth":421,"text":303},{"id":312,"depth":421,"text":313},{"id":343,"depth":421,"text":344},{"id":356,"depth":421,"text":357},{"id":397,"depth":421,"text":398},"An honest account of publishing an article about guarding fixes, then finding SEO bugs on my own site that had been live for months, and the checks I added so they stay fixed.",false,"md",{},true,"\u002Farticles\u002Fauditing-my-own-site-after-writing-about-seo","2026-11-07",{"title":5,"description":439},"An honest look at the SEO bugs I found on my own site after writing about web vitals, how I fixed them, and what is still not done.",null,"articles\u002Fauditing-my-own-site-after-writing-about-seo",[451,452,453,454,455],"seo","web-vitals","testing","nuxt","ai","5O5_3ymMzL0Gb1DyfyGj0_qt7Lg2KhcAJ89dJY4ezRQ",[458,459,465,470,475,480,485,490,495,500,504,508,512,516],{"path":444,"title":5,"published":445,"series":448,"seriesOrder":448},{"path":460,"title":461,"published":462,"series":463,"seriesOrder":464},"\u002Farticles\u002Fembrace-the-framework-custom-collections-for-subscriptions","Embrace the Framework: Put Subscription Business Rules in a Custom Collection","2026-07-28","embrace-the-framework","3",{"path":466,"title":467,"published":468,"series":463,"seriesOrder":469},"\u002Farticles\u002Fembrace-the-framework-customer-eligibility-with-custom-collections","Embrace the Framework: Make Eligibility Rules Read Like Business Policy","2026-08-04","6",{"path":471,"title":472,"published":473,"series":463,"seriesOrder":474},"\u002Farticles\u002Fembrace-the-framework-operational-reports-with-collections","Embrace the Framework: Build Clear Operational Reports With Collections","2026-08-02","5",{"path":476,"title":477,"published":478,"series":463,"seriesOrder":479},"\u002Farticles\u002Fembrace-the-framework-order-fulfilment-with-collections","Embrace the Framework: Make Order Fulfilment Clearer With Collections","2026-07-25","2",{"path":481,"title":482,"published":483,"series":463,"seriesOrder":484},"\u002Farticles\u002Fembrace-the-framework-order-processing-pipeline-that-can-grow","Embrace the Framework: Build an Order Processing Pipeline That Can Grow","2026-08-06","7",{"path":486,"title":487,"published":488,"series":463,"seriesOrder":489},"\u002Farticles\u002Fembrace-the-framework-order-processing-with-pipelines","Embrace the Framework: Break Order Processing Into Steps With Pipelines","2026-07-31","4",{"path":491,"title":492,"published":493,"series":463,"seriesOrder":494},"\u002Farticles\u002Fembrace-the-framework-supplier-imports-with-collections-and-pipelines","Embrace the Framework: Combine Collections and Pipelines for Reliable Supplier Imports","2026-08-08","8",{"path":496,"title":497,"published":498,"series":463,"seriesOrder":499},"\u002Farticles\u002Fembrace-the-framework-why-working-with-laravel-beats-working-around-it","Embrace the Framework: Why Working With Laravel Beats Working Around It","2026-07-23","1",{"path":501,"title":502,"published":503,"series":448,"seriesOrder":448},"\u002Farticles\u002Fsimple-search-helper","A Simple Laravel Search Helper","2023-08-06",{"path":505,"title":506,"published":507,"series":448,"seriesOrder":448},"\u002Farticles\u002Fsubsetting-material-symbols-icon-font","Material Symbols Was 97% Dead Weight","2026-11-06",{"path":509,"title":510,"published":511,"series":448,"seriesOrder":448},"\u002Farticles\u002Fsvg-tidy-local-svg-optimiser","SVG Tidy: Clean and Optimise SVGs Locally","2026-07-21",{"path":513,"title":514,"published":515,"series":448,"seriesOrder":448},"\u002Farticles\u002Ftools-i-use","Tools I Use","2024-05-26",{"path":517,"title":518,"published":519,"series":448,"seriesOrder":448},"\u002Farticles\u002Fwelcome","Welcome","2023-08-05",1794008630901]