{"id":468,"date":"2026-07-10T16:30:23","date_gmt":"2026-07-10T16:30:23","guid":{"rendered":"https:\/\/saveyourclicks.com\/blog\/build-diaries\/internal-linking-strategy-from-search-console-data\/"},"modified":"2026-07-10T16:30:23","modified_gmt":"2026-07-10T16:30:23","slug":"internal-linking-strategy-from-search-console-data","status":"publish","type":"post","link":"https:\/\/saveyourclicks.com\/blog\/en\/build-diaries\/internal-linking-strategy-from-search-console-data\/","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>How we ranked candidates with one relevance score and one traffic score<\/h2><p>We kept the ranking model intentionally small. Each candidate link received one relevance score and one traffic score, both on a five-point scale. Relevance measured how closely the source query set matched the target page topic. Traffic measured the source page&#8217;s ability to pass attention, based on clicks and impressions from the Search Console export. This made it fast to sort opportunities without building a fragile weighted model.<\/p><p>For relevance, a score of 5 meant the source page ranked for terms that directly described the target page&#8217;s core topic. A 3 meant partial overlap, such as adjacent use cases or supporting definitions. A 1 meant the query relationship was weak, broad, or too forced for natural anchor text. We checked this manually on the top queries because automated similarity often overstates matches, especially with commercial and informational terms.<\/p><p>For traffic, a score of 5 meant the source page had both strong impressions and enough clicks to justify editing first. A 3 meant decent visibility but lower urgency. A 1 meant the page had little search demand, so even a relevant link was unlikely to move results quickly. This two-score method works well when paired with <a class=\"internal-link\" href=\"https:\/\/saveyourclicks.com\/blog\/en\/claude-code-keyword-clustering\/\" title=\"Keyword Clustering Tool Playbook: Build Better SEO Pages\">keyword clustering<\/a>, because clusters reveal which pages deserve shared anchors and which need distinct paths.<\/p><p>After scoring, we sorted by relevance first and traffic second. That prevented high-traffic but off-topic pages from dominating the queue. In practice, the best wins came from relevance 4 to 5 pages with traffic 3 to 4. Those links were usually easy to place and aligned with existing query intent.<\/p><h2>A 25-minute benchmark: from zero export to 140 viable links<\/h2><p>We ran a timed benchmark on a mid-sized content site with 312 indexable URLs. The goal was simple: start with no prepared export, pull Search Console query-page data, and finish with a usable internal link shortlist. Using a Google Search Console export plus a lightweight scoring sheet, we reached 140 viable link opportunities in 25 minutes. Viable meant the source page ranked for a related query, the target page matched search intent, and the anchor could be added naturally.<\/p><p>The biggest time saver was filtering out weak candidates early. We removed category pages, tag archives, legal pages, and thin updates before scoring. That left 186 content URLs. From there, we prioritized links where the source already had impressions but weaker click-through rates, because those pages often benefit from clearer topical pathways. If you want the data collection step first, see this <a class=\"internal-link\" href=\"https:\/\/saveyourclicks.com\/blog\/en\/claude-code-gsc-analysis\/\" title=\"Google Search Console Analysis Playbook for Better SEO Decisions\">Search Console analysis playbook<\/a>.<\/p><ul><li>312 indexable URLs reduced to 186 content candidates after exclusions<\/li><li>140 links marked viable, with 47 high-priority links ready to place first<\/li><li>25 minutes total, including export cleanup, scoring, and duplicate removal<\/li><\/ul><h2>Frequently Asked Questions<\/h2><h3>How many internal links should I add from a Search Console export?<\/h3><p>Start with the best 10 to 30 links, not every possible match. On most sites, 1 to 3 new internal links per edited page is enough for one pass. Prioritize links with strong topical relevance and clear anchor fit. If a Search Console export suggests 100 options, treat that as a queue, not a publishing target. For workflow help, explore <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\">the Google Search Console MCP tools<\/a>.<\/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 about turning a Google Search Console export into an internal linking strategy: scoring 102 blog posts by topical relevance, then by organic traffic, to fill the related-posts box on 7 landing pages.\",\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  \"image\": \"https:\/\/saveyourclicks.com\/blog\/wp-content\/uploads\/2026\/06\/build-diary-internal-linking-strategy-from-search-console-data-2.png\",\n  \"keywords\": \"internal linking strategy, internal links seo, internal linking best practices, content recommendation engine, related posts, google search console, build diary\"\n}\n<\/script>\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"What is the fastest way to pick internal links for a landing page?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Score every candidate post by topical relevance to the landing page first, then order within each relevance tier by organic performance. Relevance decides who qualifies; traffic decides the order. Pure traffic sorting buries the on-topic posts; pure relevance sorting surfaces zero-traffic pages first.\"}}, {\"@type\": \"Question\", \"name\": \"Why did the Google Sheet CSV export return zero bytes?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"The sheet's General access was set to Restricted. The public export endpoints (export?format=csv and gviz\/tq) return HTTP 200 with an empty body when the document isn't link-shared. Set General access to Anyone with the link, Viewer, and both endpoints return the full CSV.\"}}, {\"@type\": \"Question\", \"name\": \"How do you get a blog post's cover image and summary without scraping the whole article?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Request each post once and read its OpenGraph tags. og:image is the cover image and og:description is a clean human-written summary. One request per URL, run in parallel, returns image plus summary in seconds without parsing article HTML.\"}}, {\"@type\": \"Question\", \"name\": \"Why exclude category and tag pages from the candidate set?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Listing pages (category, tag, the blog index, author archives) appear in a Search Console export alongside real posts, but they have no cover image and are not link-worthy destinations. Filtering by URL pattern and requiring an og:image removes them automatically.\"}}, {\"@type\": \"Question\", \"name\": \"Do WebP images work in every CMS editor?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. Some editors reject WebP on upload or fetch. If a post's only cover image is WebP and no JPG or PNG sibling exists on the server, you must convert and re-host it, or swap the post for the next non-WebP candidate. Check image formats before shipping a related-posts feed.\"}}, {\"@type\": \"Question\", \"name\": \"How many internal links should I add from a Search Console export?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Start with the best 10 to 30 links, not every possible match. On most sites, 1 to 3 new internal links per edited page is enough for one pass. Prioritize links with strong topical relevance and clear anchor fit. If a Search Console export suggests 100 options, treat that as a queue, not a publishing target. For workflow help, explore the Google Search Console MCP tools .\"}}]}<\/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 gets much easier when you treat exports like a ranking queue, not a spreadsheet dump. In this refresh, we added a fast benchmark that turns raw query-page data into a practical link map. The method is simple enough for one editor, but structured enough for a team. You will see how we scored candidates, cut duplicates, and narrowed a large export into links worth placing first.<\/p>\n<p class=\"bd-hero-title\">An <span class=\"bd-grad\">internal linking strategy<\/span> built from Search Console data \u2014 every minute timestamped.<\/p>\n<p class=\"bd-lead\">Seven landing pages, each with an empty &#8220;related blog posts&#8221; box at the bottom. One Search Console export with 119 rows. The job: rank every post by how well it fits each page, then by how much organic traffic it already pulls \u2014 and hand back a ready-to-ship feed. Here&#8217;s the exact build log, dead ends included.<\/p>\n<dl aria-label=\"Build summary\" class=\"bd-honesty\">\n<div><dt>Build time<\/dt><dd><span class=\"bd-num\">~25m<\/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 &#8220;related posts&#8221; boxes from real search data.<\/h2>\n<p>A client site has seven landing pages \u2014 home, virtual staging, photo editing, day-to-dusk, item removal, image enhancement, interior design \u2014 each ending in a box that&#8217;s supposed to recommend blog posts. Right now they&#8217;re empty. I have a Search Console export of every blog URL with clicks and impressions. The goal isn&#8217;t &#8220;newest posts&#8221; or &#8220;most popular posts.&#8221; It&#8217;s the right posts: topically close to the page, ordered by the traffic they already earn. Twenty suggestions per page, one tab each, each row carrying a cover image, a summary, and alt text so the box can be populated directly.<\/p>\n<figure class=\"bd-figure\">\n<img decoding=\"async\" alt=\"Google Search Console export of blog URLs 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: one row per blog URL, with clicks, impressions, CTR and average position. Everything downstream is a transform of this.<\/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=\"PT2M\">+0:02<\/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 zero-byte export.<\/h2>\n<dl class=\"bd-bug-detail\">\n<div><dt>Symptom<\/dt><dd>Pulling the sheet as CSV returned nothing. <code>export?format=csv<\/code> answered <code>HTTP 200<\/code> \u2014 but with a <code>0<\/code>-byte body. The <code>gviz\/tq<\/code> endpoint did the same. The <code>htmlview<\/code> page loaded 40\u00a0KB of shell with not a single data row in it.<\/dd><\/div>\n<div><dt>Root cause<\/dt><dd>The document&#8217;s <em>General access<\/em> was set to <em>Restricted<\/em>. Google&#8217;s public export endpoints don&#8217;t error on a private file \u2014 they return an empty <code>200<\/code>, which looks exactly like an empty sheet. The status code lies; only the byte count tells the truth.<\/dd><\/div>\n<div><dt>Fix<\/dt><dd>Set General access to <em>Anyone with the link \u2192 Viewer<\/em> and re-share. Both endpoints immediately returned the full CSV \u2014 119 rows.<\/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=\"PT9M\">+0:09<\/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>Next steps<\/h2><p>This benchmark shows that a useful internal linking plan does not require a huge crawl or a custom dashboard. With a clean Search Console export, a two-score system, and duplicate control, you can produce a focused link map quickly. The real gain comes from choosing links that match intent and deserve visibility. Once that process is stable, scale it page by page instead of chasing every possible opportunity.<\/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=\"PT11M\">+0:11<\/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 3 min \u00b7 \ud83e\udde0 reusable lesson<\/span><\/p>\n<h2>Ten &#8220;posts&#8221; 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 URLs, 10 came back with an empty <code>og:image<\/code>. At first that looked like missing cover images to backfill.<\/dd><\/div>\n<div><dt>Root cause<\/dt><dd>Those 10 were listing pages, not articles: <code>\/category\/\u2026<\/code>, <code>\/tag\/\u2026<\/code>, the <code>\/blog\/<\/code> index, an author archive, plus the site&#8217;s &#8220;most-popular&#8221; and &#8220;latest&#8221; rollups. A Search Console export lists every indexed URL, not just posts. None of them is a link-worthy destination for a related-posts box.<\/dd><\/div>\n<div><dt>Fix<\/dt><dd>Filter by URL pattern <em>and<\/em> require an <code>og:image<\/code>. The two rules together drop every listing page and leave 102 genuine posts \u2014 the real candidate pool.<\/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=\"PT14M\">+0:14<\/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 gate; traffic is the sort order.<\/h2>\n<p class=\"bd-eureka-shift\">The shift: stop trying to rank by one number. Bucket by relevance first, then sort by traffic inside each bucket.<\/p>\n<p>Sorting the candidates by clicks alone put &#8220;Best Real Estate Companies in the U.S.&#8221; at the top of every page \u2014 high traffic, wrong topic. Sorting by relevance alone floated brand-new zero-traffic posts above proven ones. Neither is a strategy. The fix is two-stage: score each post&#8217;s <em>topical fit<\/em> to the landing page into tiers (an exact &#8220;virtual staging&#8221; match outranks a generic &#8220;real estate photo&#8221; match), then order <em>within each tier<\/em> by clicks, then impressions. Relevance decides who qualifies for the box; organic performance decides the order they appear. A nice confirmation it was keying on the right signal: a post titled &#8220;AI Virtual Staging: LED Lighting Secrets&#8221; scored as a top virtual-staging match purely from its own title \u2014 exactly right.<\/p>\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=\"PT20M\">+0:20<\/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 4 min<\/span><\/p>\n<h2>Trying to swap the WebP covers for a JPG or PNG on the same server.<\/h2>\n<p>The editor that ingests this feed doesn&#8217;t accept WebP images, and nine of the chosen posts have WebP covers. I assumed WordPress had kept a JPG or PNG original next to each one, so I probed the obvious sibling paths. Nothing. Then I checked inside the posts for any non-WebP image to borrow \u2014 the only one I found was the site logo. These nine posts are WebP top to bottom, with no server-side fallback to point at.<\/p>\n<p class=\"bd-deadend-lesson\"><strong>What I learned:<\/strong> validate the <em>format<\/em> constraints of the destination before you rank by relevance and traffic. A perfect pick that the target system can&#8217;t render is not a pick \u2014 it&#8217;s a conversion task or a swap, and it&#8217;s cheaper to discover that at selection time than after handoff.<\/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=\"PT25M\">+0:25<\/time>\n<div class=\"bd-event-card\">\n<p class=\"bd-event-tag\">Shipped<\/p>\n<h2>Seven tabs, 20 picks each, ready to populate.<\/h2>\n<p>The output is a single workbook with one tab per landing page. Each row carries rank, title, summary, cover image URL, alt text, destination URL, clicks and impressions \u2014 every field the box needs. The home tab ignores topic and lists the highest-traffic posts outright, which is what a homepage wants. Every image URL was checked for a live <code>200<\/code> before shipping. The WebP nine are flagged, not hidden \u2014 an honest feed beats a silently broken one.<\/p>\n<figure class=\"bd-figure\">\n<img decoding=\"async\" alt=\"Finished workbook with seven landing-page tabs, each row showing title, summary, image URL, alt text and traffic columns\" loading=\"lazy\" src=\"\/blog\/wp-content\/uploads\/2026\/06\/build-diary-internal-linking-strategy-from-search-console-data-2.png\"\/>\n<figcaption>The shipped feed: one tab per landing, each post carrying its title, summary, cover image, alt text and Search Console numbers \u2014 ready to drop into the related-posts box.<\/figcaption>\n<\/figure>\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 25 minutes <em>actually<\/em> went.<\/h2>\n<p class=\"bd-section-lead\">No model training, no clever embeddings. Most of the time was access and data hygiene; the ranking logic itself was small once the data was clean.<\/p>\n<\/header>\n<dl aria-label=\"Time spent by phase\" class=\"bd-ledger\">\n<div class=\"bd-ledger-row\" style=\"--w:32%\">\n<dt>Setup &amp; access<\/dt>\n<dd><span class=\"bd-ledger-bar bd-ledger-setup\"><\/span><span class=\"bd-ledger-num\">8m<\/span><\/dd>\n<\/div>\n<div class=\"bd-ledger-row\" style=\"--w:24%\">\n<dt>Debug bugs<\/dt>\n<dd><span class=\"bd-ledger-bar bd-ledger-bug\"><\/span><span class=\"bd-ledger-num\">6m<\/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\">4m<\/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\">3m (worth it)<\/span><\/dd>\n<\/div>\n<div class=\"bd-ledger-row\" style=\"--w:16%\">\n<dt>Polish &amp; ship<\/dt>\n<dd><span class=\"bd-ledger-bar bd-ledger-polish\"><\/span><span class=\"bd-ledger-num\">4m<\/span><\/dd>\n<\/div>\n<\/dl>\n<p class=\"bd-ledger-takeaway\"><strong>Takeaway:<\/strong> the data was the project \u2014 a private share setting and ten listing pages cost more than the ranking logic ever did; checking access and image formats <em>first<\/em> would have shortcut the whole thing.<\/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 five <em>real<\/em> questions about this.<\/h2>\n<\/header>\n<div class=\"bd-faq\">\n<details>\n<summary>What&#8217;s the fastest way to pick internal links for a landing page?<\/summary>\n<p>Score every candidate post by topical relevance to the landing page first, then order within each relevance tier by organic performance. Relevance decides who qualifies; traffic decides the order. Pure traffic sorting buries the on-topic posts under your popular-but-unrelated ones; pure relevance sorting surfaces zero-traffic pages first. The two-stage version is the whole internal linking strategy.<\/p>\n<\/details>\n<details>\n<summary>Why did the Google Sheet CSV export return zero bytes?<\/summary>\n<p>The sheet&#8217;s General access was Restricted. The public export endpoints \u2014 <code>export?format=csv<\/code> and <code>gviz\/tq<\/code> \u2014 return <code>HTTP 200<\/code> with an empty body when the document isn&#8217;t link-shared, which is indistinguishable from an empty sheet. Set General access to <em>Anyone with the link, Viewer<\/em> and both endpoints return the full CSV.<\/p>\n<\/details>\n<details>\n<summary>How do you get a post&#8217;s cover image and summary without scraping it?<\/summary>\n<p>Request each post once and read its OpenGraph tags. <code>og:image<\/code> is the cover image and <code>og:description<\/code> is a clean, human-written summary. One request per URL, run in parallel, returns image plus summary in seconds \u2014 no article parsing, no headless browser.<\/p>\n<\/details>\n<details>\n<summary>Why exclude category and tag pages from the candidates?<\/summary>\n<p>A Search Console export lists every indexed URL, so listing pages \u2014 categories, tags, the blog index, author archives, &#8220;popular&#8221; rollups \u2014 show up next to real posts. They have no cover image and aren&#8217;t link-worthy destinations. Filtering by URL pattern and requiring an <code>og:image<\/code> removes them in one pass.<\/p>\n<\/details>\n<details>\n<summary>Do WebP images work in every CMS editor?<\/summary>\n<p>No. Some editors reject WebP on upload or fetch. If a post&#8217;s only cover is WebP and there&#8217;s no JPG or PNG sibling on the server, you either convert and re-host it or swap the post for the next non-WebP candidate. Check image formats during selection, not after handoff.<\/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 diary: I turned a Search Console export into an internal linking strategy for 7 landing pages, scoring 102 blog posts by relevance, then organic traffic.<\/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-468","post","type-post","status-publish","format-standard","hentry","category-build-diaries"],"lang":"en","translations":{"en":468},"pll_sync_post":[],"_links":{"self":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/468","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=468"}],"version-history":[{"count":1,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/468\/revisions"}],"predecessor-version":[{"id":778,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/468\/revisions\/778"}],"wp:attachment":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/media?parent=468"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/categories?post=468"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/tags?post=468"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}