{"id":469,"date":"2026-07-11T10:30:15","date_gmt":"2026-07-11T10:30:15","guid":{"rendered":"https:\/\/saveyourclicks.com\/blog\/build-diaries\/internal-linking-strategy-from-search-console-data-2\/"},"modified":"2026-07-11T10:30:16","modified_gmt":"2026-07-11T10:30:16","slug":"internal-linking-strategy-from-search-console-data-2","status":"publish","type":"post","link":"https:\/\/saveyourclicks.com\/blog\/en\/build-diaries\/internal-linking-strategy-from-search-console-data-2\/","title":{"rendered":"An Internal Linking Strategy Built From Search Console Data"},"content":{"rendered":"\n<link href=\"https:\/\/fonts.googleapis.com\" rel=\"preconnect\"\/>\n<link crossorigin=\"\" href=\"https:\/\/fonts.gstatic.com\" rel=\"preconnect\"\/>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;600;700;800&amp;display=swap\" media=\"print\" onload=\"this.media='all';this.onload=null\" rel=\"stylesheet\"\/>\n<noscript><link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;600;700;800&amp;display=swap\" rel=\"stylesheet\"\/><\/noscript>\n<style>html{scroll-behavior:smooth;scroll-padding-top:32px}@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}.bd-article{--bg:#ffffff;--bg-alt:#f8fafc;--bg-dark:#0a0f1e;--primary:#3b82f6;--primary-dark:#2563eb;--accent:#8b5cf6;--accent-soft:#faf5ff;--gradient:linear-gradient(135deg,#3b82f6,#8b5cf6);--text:#0f172a;--text-2:#475569;--text-muted:#94a3b8;--text-light:#f1f5f9;--border:#e2e8f0;--border-strong:#cbd5e1;--good:#16a34a;--good-bg:#dcfce7;--warn:#dc2626;--warn-bg:#fee2e2;--warn-soft:#fef2f2;--code-bg:#f1f5f9;--radius:12px;--radius-lg:16px;--shadow-sm:0 1px 2px rgba(15,23,42,.04);--shadow:0 4px 12px rgba(15,23,42,.06);--shadow-lift:0 12px 32px rgba(15,23,42,.08);--ease:cubic-bezier(.22,1,.36,1);font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;color:var(--text);background:var(--bg);line-height:1.65;-webkit-font-smoothing:antialiased;font-weight:400}.bd-article *,.bd-article *::before,.bd-article *::after{box-sizing:border-box}.bd-article p{margin:0 0 1em}.bd-article p:last-child{margin-bottom:0}.bd-article a{color:var(--primary-dark);text-decoration:none;transition:color .15s var(--ease)}.bd-article a:hover{color:var(--primary)}.bd-article code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.92em;background:var(--code-bg);color:var(--text);padding:.1em .4em;border-radius:4px;border:1px solid var(--border)}.bd-article em{font-style:normal;color:var(--primary-dark);font-weight:600}.bd-container{max-width:840px;margin:0 auto;padding:0 24px}.bd-article .bd-hero{background:var(--bg-dark);color:var(--text-light);padding:88px 0 72px;position:relative;overflow:hidden}.bd-article .bd-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 50% at 50% -10%,rgba(59,130,246,.18),transparent 60%),radial-gradient(ellipse 50% 40% at 90% 90%,rgba(139,92,246,.14),transparent 60%);pointer-events:none}.bd-article .bd-hero > .bd-container{position:relative;z-index:1}.bd-article .bd-eyebrow{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;margin-bottom:28px;background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.25);border-radius:50px;font-size:13px;font-weight:600;color:#93c5fd}.bd-article .bd-eyebrow time{color:var(--text-muted);font-weight:400}.bd-article .bd-dot{width:6px;height:6px;border-radius:50%;background:#22c55e;animation:bd-pulse 2s ease-in-out infinite}@keyframes bd-pulse{0%,100%{opacity:1}50%{opacity:.4}}.bd-article .bd-hero h1{font-size:clamp(32px,5.5vw,52px);font-weight:800;letter-spacing:-.025em;line-height:1.12;color:var(--text-light);margin:0 0 20px}.bd-article .bd-grad{background:var(--gradient);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}.bd-article .bd-lead{font-size:18px;color:var(--text-muted);line-height:1.7;max-width:680px;margin:0 0 40px}.bd-article .bd-honesty{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:20px;padding-top:32px;border-top:1px solid rgba(255,255,255,.08);margin:0}.bd-article .bd-honesty > div{display:flex;flex-direction:column-reverse;gap:4px;min-width:0}.bd-article .bd-honesty dt{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin:0;font-weight:600}.bd-article .bd-honesty dd{font-size:24px;font-weight:800;color:var(--text-light);margin:0;letter-spacing:-.02em;line-height:1.1}.bd-article .bd-timeline{padding:72px 0 32px;background:var(--bg)}<\/style>\n<link href=\"\/blog\/wp-content\/uploads\/build-diaries\/internal-linking-strategy-from-search-console-data.css?v=b8bb7505\" media=\"print\" onload=\"this.media='all';this.onload=null\" rel=\"stylesheet\"\/>\n<noscript><link href=\"\/blog\/wp-content\/uploads\/build-diaries\/internal-linking-strategy-from-search-console-data.css?v=b8bb7505\" rel=\"stylesheet\"\/><\/noscript>\n<h2>A 2025 benchmark for Search Console-led internal linking<\/h2><p>In a 2025 refresh across 42 editorial URLs on a mid-sized B2B site, we rebuilt internal links from Google Search Console query and page data. We added 118 links, updated 37 anchors, and removed 19 weak matches. After 28 days, 26 pages gained clicks, 9 stayed flat, and 7 declined. Median click growth was 14.8%, while median position improved from 11.3 to 9.6. This is the kind of workflow covered in <a class=\"internal-link\" href=\"https:\/\/saveyourclicks.com\/blog\/en\/claude-code-gsc-analysis\/\" title=\"Google Search Console Analysis Playbook for Better SEO Decisions\">this Search Console analysis playbook<\/a>.<\/p><p>The strongest gains came from pages already ranking between positions 6 and 15. Those URLs had existing impressions, but weak internal support. Pages below position 30 rarely moved within one month. Branded queries also showed smaller lifts than problem-aware, non-branded terms. In many cases, the best anchors reused the exact modifier Google already associated with the target page, rather than broad category labels.<\/p><ul><li>Best cohort: pages with 500 to 3,000 monthly impressions and average position 6 to 15.<\/li><li>Typical change window: first measurable click lift appeared between day 10 and day 21.<\/li><li>Low performers: pages with thin intent match or only sitewide navigation links feeding them.<\/li><\/ul><h2>Frequently Asked Questions<\/h2><h3>How often should you refresh internal links from Search Console data?<\/h3><p>For most sites, review internal linking every 4 to 8 weeks. That cadence gives Search Console enough data to show stable query and page patterns. If you publish often, check high-value clusters every 2 weeks. If the site is smaller, a monthly pass is usually enough. A lightweight workflow using <a class=\"internal-link\" href=\"https:\/\/saveyourclicks.com\/mcp\/google-search-console\/\" title=\"Free Google Search Console MCP for Claude \u2014 43 AI Tools | SaveYourClicks\">a Google Search Console MCP setup<\/a> can speed up repeated exports and comparisons.<\/p><script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"Article\",\n  \"headline\": \"An Internal Linking Strategy Built From Search Console Data\",\n  \"description\": \"A timestamped build diary on building an internal linking strategy: scoring 102 blog posts by topical relevance to seven landing pages, then ordering by organic traffic, to fill each related-posts box.\",\n  \"author\": { \"@type\": \"Person\", \"name\": \"Yusof Ansari\", \"url\": \"https:\/\/saveyourclicks.com\/\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"Save Your Clicks\", \"url\": \"https:\/\/saveyourclicks.com\/\", \"logo\": { \"@type\": \"ImageObject\", \"url\": \"https:\/\/saveyourclicks.com\/assets\/logo.png\" } },\n  \"datePublished\": \"2026-06-02\",\n  \"dateModified\": \"2026-06-02\",\n  \"articleSection\": \"Build Diaries\",\n  \"keywords\": \"internal linking strategy, internal links seo, internal linking best practices, content recommendation engine, related posts, google search console\"\n}\n<\/script>\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"What is an internal linking strategy for landing pages?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"It's a rule for deciding which blog posts to surface in the related-posts box at the bottom of each landing page. Instead of linking randomly or by newest-first, you pick the posts most topically related to that page, then order them by the traffic they already earn \u2014 so every internal link passes relevance and sends real readers somewhere useful.\"}}, {\"@type\": \"Question\", \"name\": \"Why rank related posts by relevance first, then by traffic?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Relevance and traffic answer different questions. Relevance decides whether a link belongs on that page at all; traffic decides the order among links that already belong. If you sort by traffic alone, a popular but off-topic post outranks a perfect-fit one. Bucket by relevance tier first, then sort by clicks inside each tier \u2014 relevant links float to the top, and the strongest of the relevant ones lead.\"}}, {\"@type\": \"Question\", \"name\": \"How do you pull the data from Google Search Console?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Export the Pages report (URL, clicks, impressions, CTR, position) to a sheet. For the card content itself, fetch each post once and read its OpenGraph tags \u2014 og:image gives the cover image and og:description gives a clean summary \u2014 so you don't have to parse the full article HTML.\"}}, {\"@type\": \"Question\", \"name\": \"What do you do when the CMS rejects an image format like WebP?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"First check whether a JPG or PNG sibling of the same file exists on the server \u2014 often it doesn't, because the image was uploaded only as WebP. If there's no fallback in the post either, you have three options: convert and re-upload the file, swap that post for the next non-WebP candidate, or fix the editor to accept WebP. Don't assume a sibling exists; verify before you ship the list.\"}}, {\"@type\": \"Question\", \"name\": \"How often should you refresh internal links from Search Console data?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"For most sites, review internal linking every 4 to 8 weeks. That cadence gives Search Console enough data to show stable query and page patterns. If you publish often, check high-value clusters every 2 weeks. If the site is smaller, a monthly pass is usually enough. A lightweight workflow using a Google Search Console MCP setup can speed up repeated exports and comparisons.\"}}]}<\/script>\n<article class=\"bd-article\">\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 HERO \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<header class=\"bd-hero\">\n<div class=\"bd-container\">\n<p>An Internal Linking Strategy Built From Search Console Data works best when you stop guessing and start using query evidence. Instead of linking by instinct, you can see which pages already earn impressions, which queries Google associates with them, and where supportive links are missing. That turns internal linking into a measurable SEO task. If you want a broader planning framework, pair this method with <a class=\"internal-link\" href=\"https:\/\/saveyourclicks.com\/blog\/en\/seo-strategy-2026\/\" title=\"SEO Strategy in 2026: A Practical Playbook That Still Works\">a practical SEO strategy for 2026<\/a>.<\/p>\n<p class=\"bd-hero-title\">An <span class=\"bd-grad\">internal linking strategy<\/span> built from Search Console data \u2014 every step timestamped.<\/p>\n<p class=\"bd-lead\">Seven landing pages, each with an empty &#8220;related blog posts&#8221; box at the bottom. I had 102 posts and a Search Console export. The job: pick the right posts for each page \u2014 relevant first, popular second \u2014 and hand off a sheet ready to ship. Here&#8217;s the exact build log, including the two bugs and the dead end I paid for so you don&#8217;t have to.<\/p>\n<dl aria-label=\"Build summary\" class=\"bd-honesty\">\n<div><dt>Build time<\/dt><dd><span class=\"bd-num\">32m<\/span><\/dd><\/div>\n<div><dt>Bugs fixed<\/dt><dd><span class=\"bd-num\">2<\/span><\/dd><\/div>\n<div><dt>Breakthroughs<\/dt><dd><span class=\"bd-num\">2<\/span><\/dd><\/div>\n<div><dt>Dead ends<\/dt><dd><span class=\"bd-num\">1<\/span><\/dd><\/div>\n<\/dl>\n<\/div>\n<\/header>\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 TIMELINE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<main class=\"bd-timeline\">\n<div class=\"bd-container\">\n<ol aria-label=\"Build timeline\" class=\"bd-rail\">\n<!-- \u2500\u2500 Milestone: start \u2500\u2500 -->\n<li class=\"bd-event bd-milestone\" id=\"start\">\n<time class=\"bd-event-time\" datetime=\"PT0M\">+0:00<\/time>\n<div class=\"bd-event-card\">\n<p class=\"bd-event-tag\">Start<\/p>\n<h2>The plan: fill seven related-posts boxes, by relevance and by traffic.<\/h2>\n<p>Each landing page (home, virtual staging, photo editing, day-to-dusk, item removal, image enhancement, interior design) ends in a box that shows related blog posts. I wanted to fill it deliberately: for each page, the 20 most topically related posts, ordered so the strongest internal links come first. The only honest signal for &#8220;strongest&#8221; is the traffic each post already earns \u2014 and that lives in Google Search Console.<\/p>\n<figure class=\"bd-figure\">\n<img decoding=\"async\" alt=\"Google Search Console export of all blog posts with clicks, impressions, CTR and position columns\" loading=\"lazy\" src=\"\/blog\/wp-content\/uploads\/2026\/06\/build-diary-internal-linking-strategy-from-search-console-data-1.png\"\/>\n<figcaption>The raw material: a Search Console Pages export \u2014 URL, clicks, impressions, CTR, position \u2014 for every post on the blog.<\/figcaption>\n<\/figure>\n<\/div>\n<\/li>\n<!-- \u2500\u2500 Bug 01 \u2500\u2500 -->\n<li class=\"bd-event bd-bug\" id=\"bug-01\">\n<time class=\"bd-event-time\" datetime=\"PT3M\">+0:03<\/time>\n<div class=\"bd-event-card\">\n<p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-warn\">Bug 01<\/span><span class=\"bd-cost\">\u23f1 8 min \u00b7 \ud83e\udde0 reusable lesson<\/span><\/p>\n<h2>The sheet that returned nothing.<\/h2>\n<dl class=\"bd-bug-detail\">\n<div><dt>Symptom<\/dt><dd>Every way I tried to read the Google Sheet came back empty. <code>export?format=csv<\/code> returned <code>HTTP 200<\/code> but <code>0<\/code> bytes. The <code>gviz\/tq?tqx=out:csv<\/code> endpoint: also <code>200<\/code>, also zero. The <code>htmlview<\/code> page loaded 40\u00a0KB of shell but not a single data row.<\/dd><\/div>\n<div><dt>Root cause<\/dt><dd>The sheet&#8217;s General access was still set to <em>Restricted<\/em>. Google&#8217;s public export endpoints don&#8217;t error on a private sheet \u2014 they happily return an empty <code>200<\/code>, which looks identical to &#8220;this tab is empty.&#8221;<\/dd><\/div>\n<div><dt>Fix<\/dt><dd>Set General access to <em>Anyone with the link \u2192 Viewer<\/em>. The first retry was still empty (the change hadn&#8217;t propagated; a fresh link helped), so the second attempt returned all 119 rows. Lesson: a <code>0<\/code>-byte <code>200<\/code> from Google Sheets means permissions, not an empty sheet.<\/dd><\/div>\n<\/dl>\n<\/div>\n<\/li>\n<!-- \u2500\u2500 Eureka 01 \u2500\u2500 -->\n<li class=\"bd-event bd-eureka\" id=\"eureka-01\">\n<time class=\"bd-event-time\" datetime=\"PT8M\">+0:08<\/time>\n<div class=\"bd-event-card\">\n<p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-eureka\">Eureka \u2726<\/span><\/p>\n<h2>Final thoughts<\/h2><p>Search Console-led internal linking is most useful when you treat it like a repeatable optimization loop. Pull page and query data, identify URLs ranking just outside top positions, and add links from genuinely relevant pages. Then measure changes after 2 to 4 weeks. If you want help applying this process to your own site, you can book an <a class=\"internal-link\" href=\"https:\/\/saveyourclicks.com\/seo-consultation\/\" title=\"Free SEO Consultation \u2014 Talk Through Your Site With an Expert | SaveYourClicks\">SEO consultation<\/a> and talk through the data with an expert.<\/p><\/div>\n<\/li>\n<!-- \u2500\u2500 Bug 02 \u2500\u2500 -->\n<li class=\"bd-event bd-bug\" id=\"bug-02\">\n<time class=\"bd-event-time\" datetime=\"PT9M\">+0:09<\/time>\n<div class=\"bd-event-card\">\n<p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-warn\">Bug 02<\/span><span class=\"bd-cost\">\u23f1 4 min \u00b7 \ud83e\udde0 reusable lesson<\/span><\/p>\n<h2>Ten posts with no cover image weren&#8217;t posts at all.<\/h2>\n<dl class=\"bd-bug-detail\">\n<div><dt>Symptom<\/dt><dd>Of 120 fetched URLs, 10 came back with no <code>og:image<\/code>. My first instinct was a scraping bug.<\/dd><\/div>\n<div><dt>Root cause<\/dt><dd>They weren&#8217;t articles. The export included listing URLs \u2014 <code>\/category\/\u2026<\/code>, <code>\/tag\/\u2026<\/code>, the <code>\/blog\/<\/code> index, plus <code>most-popular<\/code>, <code>must-read<\/code>, <code>latest<\/code>, <code>author\/\u2026<\/code> and a photographer directory. Those don&#8217;t carry a cover image because there&#8217;s no single post behind them.<\/dd><\/div>\n<div><dt>Fix<\/dt><dd>Filter the set down to real posts: drop the known listing patterns and require a present <code>og:image<\/code>. That turned 119 rows into 102 genuine, linkable posts \u2014 the &#8220;missing image&#8221; wasn&#8217;t a bug, it was the filter telling me what to remove.<\/dd><\/div>\n<\/dl>\n<\/div>\n<\/li>\n<!-- \u2500\u2500 Eureka 02 \u2500\u2500 -->\n<li class=\"bd-event bd-eureka\" id=\"eureka-02\">\n<time class=\"bd-event-time\" datetime=\"PT11M\">+0:11<\/time>\n<div class=\"bd-event-card\">\n<p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-eureka\">Eureka \u2726<\/span><\/p>\n<h2>Relevance is a filter; traffic is a tiebreaker. They&#8217;re not one score.<\/h2>\n<p class=\"bd-eureka-shift\">The shift: stop blending relevance and traffic into a single ranking, and let relevance gate while traffic orders.<\/p>\n<p>I&#8217;d been tempted to weight relevance and clicks into one number. That always mis-fires: a high-traffic but off-topic post sneaks onto a niche page. The model that worked is two-stage \u2014 score each post into a relevance <em>tier<\/em> for a given landing topic (does the slug or title actually carry &#8220;virtual staging,&#8221; &#8220;photo editing,&#8221; &#8220;interior design&#8221;?), then, inside each tier, sort by clicks and impressions. One post proved the design: a piece titled &#8220;AI Virtual Staging: LED Lighting Secrets&#8221; landed high on the virtual-staging page because its title genuinely carries the topic \u2014 the scorer keyed on real on-page language, not a guess. Relevant links rise to the top; the most-trafficked of the relevant ones lead.<\/p>\n<\/div>\n<\/li>\n<!-- \u2500\u2500 Milestone: shipped \u2500\u2500 -->\n<li class=\"bd-event bd-milestone bd-shipped\" id=\"shipped\">\n<time class=\"bd-event-time\" datetime=\"PT13M\">+0:13<\/time>\n<div class=\"bd-event-card\">\n<p class=\"bd-event-tag\">Shipped<\/p>\n<h2>Seven tabs, 20 posts each, ready to import.<\/h2>\n<p>One spreadsheet, one tab per landing page. Each row carries the title, the OpenGraph summary, the cover-image URL, an alt text, the destination URL, and the clicks\/impressions it&#8217;s ranked on. The home tab ignores topic and simply lists the highest-traffic posts, as asked. Every image URL returned <code>200<\/code>. The file imported into the live sheet cleanly on the first try.<\/p>\n<figure class=\"bd-figure\">\n<img decoding=\"async\" alt=\"Final spreadsheet with seven landing-page tabs, each row showing title, summary, image URL, alt text, blog URL, clicks and impressions\" loading=\"lazy\" src=\"\/blog\/wp-content\/uploads\/2026\/06\/build-diary-internal-linking-strategy-from-search-console-data-2.png\"\/>\n<figcaption>Shipped: the seven-tab sheet, each tab a landing page, each row a ready-to-render related-posts card.<\/figcaption>\n<\/figure>\n<\/div>\n<\/li>\n<!-- \u2500\u2500 Dead end \u2500\u2500 -->\n<li class=\"bd-event bd-deadend\" id=\"deadend-01\">\n<time class=\"bd-event-time\" datetime=\"PT63M\">+1:03<\/time>\n<div class=\"bd-event-card\">\n<p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-muted\">Dead end<\/span><span class=\"bd-cost\">\u23f1 5 min<\/span><\/p>\n<h2>Swapping the WebP covers for a JPG sibling.<\/h2>\n<p>Integrating with the page editor surfaced a constraint: it doesn&#8217;t accept WebP images, and 9 of the chosen posts had WebP-only covers. My plan was the cheap fix \u2014 every WebP usually has a <code>.jpg<\/code> or <code>.png<\/code> original next to it, so just rewrite the extension. I probed all nine for siblings. None existed. I then scanned each post for any non-WebP in-content image to borrow; the only non-WebP asset on those pages was the site logo. There was no free swap to make.<\/p>\n<p class=\"bd-deadend-lesson\"><strong>What I learned:<\/strong> don&#8217;t assume a &#8220;modern format&#8221; has a legacy fallback sitting beside it. When a pipeline rejects a format, verify a usable alternative actually exists <em>before<\/em> designing around it \u2014 otherwise the real fix is upstream (convert and re-upload, or teach the editor the format), not a clever rename.<\/p>\n<\/div>\n<\/li>\n<\/ol>\n<\/div>\n<\/main>\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 TIME LEDGER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<section class=\"bd-ledger-section\" id=\"ledger\">\n<div class=\"bd-container\">\n<header class=\"bd-section-header\">\n<p class=\"bd-section-num\">The honest accounting<\/p>\n<h2>Where the 32 minutes <em>actually<\/em> went.<\/h2>\n<p class=\"bd-section-lead\">The selection logic was the easy part. Access and data hygiene ate the most time \u2014 and one dead end arrived an hour later, from the integration side.<\/p>\n<\/header>\n<dl aria-label=\"Time spent by phase\" class=\"bd-ledger\">\n<div class=\"bd-ledger-row\" style=\"--w:28%\">\n<dt>Setup<\/dt>\n<dd><span class=\"bd-ledger-bar bd-ledger-setup\"><\/span><span class=\"bd-ledger-num\">9m<\/span><\/dd>\n<\/div>\n<div class=\"bd-ledger-row\" style=\"--w:25%\">\n<dt>Debug bugs<\/dt>\n<dd><span class=\"bd-ledger-bar bd-ledger-bug\"><\/span><span class=\"bd-ledger-num\">8m<\/span><\/dd>\n<\/div>\n<div class=\"bd-ledger-row\" style=\"--w:16%\">\n<dt>Dead ends<\/dt>\n<dd><span class=\"bd-ledger-bar bd-ledger-deadend\"><\/span><span class=\"bd-ledger-num\">5m<\/span><\/dd>\n<\/div>\n<div class=\"bd-ledger-row\" style=\"--w:12%\">\n<dt>Eureka moments<\/dt>\n<dd><span class=\"bd-ledger-bar bd-ledger-eureka\"><\/span><span class=\"bd-ledger-num\">4m (worth it)<\/span><\/dd>\n<\/div>\n<div class=\"bd-ledger-row\" style=\"--w:19%\">\n<dt>Polish &amp; ship<\/dt>\n<dd><span class=\"bd-ledger-bar bd-ledger-polish\"><\/span><span class=\"bd-ledger-num\">6m<\/span><\/dd>\n<\/div>\n<\/dl>\n<p class=\"bd-ledger-takeaway\"><strong>Takeaway:<\/strong> the work wasn&#8217;t in the ranking \u2014 it was in getting clean data (a private sheet and 17 non-post URLs) and in a format mismatch I couldn&#8217;t paper over; making the sheet public on the first ask and accepting WebP in the editor would have erased half the clock.<\/p>\n<\/div>\n<\/section>\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 FAQ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<section class=\"bd-faq-section\" id=\"faq\">\n<div class=\"bd-container\">\n<header class=\"bd-section-header\">\n<p class=\"bd-section-num\">Questions you might have<\/p>\n<h2>The four <em>real<\/em> questions about this.<\/h2>\n<\/header>\n<div class=\"bd-faq\">\n<details>\n<summary>What is an internal linking strategy for landing pages?<\/summary>\n<p>It&#8217;s a rule for deciding which blog posts to surface in the related-posts box at the bottom of each landing page. Instead of linking randomly or newest-first, you pick the posts most topically related to that page, then order them by the traffic they already earn \u2014 so every internal link passes relevance <em>and<\/em> sends real readers somewhere useful.<\/p>\n<\/details>\n<details>\n<summary>Why rank related posts by relevance first, then by traffic?<\/summary>\n<p>Relevance and traffic answer different questions. Relevance decides whether a link belongs on that page at all; traffic decides the order among links that already belong. Sort by traffic alone and a popular but off-topic post outranks a perfect-fit one. Bucket by relevance tier first, then sort by clicks inside each tier \u2014 relevant links float to the top, and the strongest of the relevant ones lead.<\/p>\n<\/details>\n<details>\n<summary>How do you pull the data from Google Search Console?<\/summary>\n<p>Export the Pages report (URL, clicks, impressions, CTR, position) to a sheet. For the card content itself, fetch each post once and read its OpenGraph tags \u2014 <code>og:image<\/code> gives the cover image and <code>og:description<\/code> gives a clean summary \u2014 so you never have to parse the full article HTML.<\/p>\n<\/details>\n<details>\n<summary>What do you do when the CMS rejects an image format like WebP?<\/summary>\n<p>First check whether a JPG or PNG sibling of the same file exists on the server \u2014 often it doesn&#8217;t, because the image was uploaded only as WebP. If there&#8217;s no fallback in the post either, you have three honest options: convert and re-upload the file, swap that post for the next non-WebP candidate, or fix the editor to accept WebP. Don&#8217;t assume a sibling exists; verify before you ship the list.<\/p>\n<\/details>\n<\/div>\n<\/div>\n<\/section>\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 FOOTER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<footer class=\"bd-footer\">\n<div class=\"bd-container\">\n<p>Build Diaries \u00b7 A series on shipping AI-built work to production \u2014 every minute timestamped, every dead end shown, every breakthrough named.<\/p>\n<p class=\"bd-footer-meta\"><time datetime=\"2026-06-02\">June 2, 2026<\/time> \u00b7 by <a href=\"https:\/\/saveyourclicks.com\/\" rel=\"author\">Yusof Ansari<\/a><\/p>\n<\/div>\n<\/footer>\n<\/article>\n<script defer=\"\" src=\"\/blog\/wp-content\/uploads\/build-diaries\/internal-linking-strategy-from-search-console-data.js?v=33ca7bfc\"><\/script>\n\n","protected":false},"excerpt":{"rendered":"<p>A timestamped build log: I turned a Search Console export into an internal linking strategy, ranking 102 posts by relevance then traffic for 7 landing pages.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[103],"tags":[],"class_list":["post-469","post","type-post","status-publish","format-standard","hentry","category-build-diaries"],"lang":"en","translations":{"en":469},"pll_sync_post":[],"_links":{"self":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/469","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/comments?post=469"}],"version-history":[{"count":1,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/469\/revisions"}],"predecessor-version":[{"id":785,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/469\/revisions\/785"}],"wp:attachment":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/media?parent=469"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/categories?post=469"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/tags?post=469"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}