{"id":469,"date":"2026-06-02T11:14:11","date_gmt":"2026-06-02T11:14:11","guid":{"rendered":"https:\/\/saveyourclicks.com\/blog\/build-diaries\/internal-linking-strategy-from-search-console-data-2\/"},"modified":"2026-06-02T11:14:11","modified_gmt":"2026-06-02T11:14:11","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 rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" media=\"print\" onload=\"this.media='all';this.onload=null\" href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;600;700;800&#038;display=swap\">\n\n<noscript><link rel=\"stylesheet\" href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;600;700;800&#038;display=swap\"><\/noscript>\n\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\n<link rel=\"stylesheet\" media=\"print\" onload=\"this.media='all';this.onload=null\" href=\"\/blog\/wp-content\/uploads\/build-diaries\/internal-linking-strategy-from-search-console-data.css?v=b8bb7505\">\n<noscript><link rel=\"stylesheet\" href=\"\/blog\/wp-content\/uploads\/build-diaries\/internal-linking-strategy-from-search-console-data.css?v=b8bb7505\"><\/noscript>\n\n<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\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    { \"@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.\"}},\n    { \"@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.\"}},\n    { \"@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.\"}},\n    { \"@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.\"}}\n  ]\n}\n<\/script>\n\n<article class=\"bd-article\">\n\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 class=\"bd-eyebrow\"><span class=\"bd-dot\" aria-hidden=\"true\"><\/span>Build Diary \u00b7 06 \u00b7 <time datetime=\"2026-06-02\">June 2, 2026<\/time><\/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\n      <dl class=\"bd-honesty\" aria-label=\"Build summary\">\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\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\n      <ol class=\"bd-rail\" aria-label=\"Build timeline\">\n\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\" src=\"\/blog\/wp-content\/uploads\/2026\/06\/build-diary-internal-linking-strategy-from-search-console-data-1.png\" alt=\"Google Search Console export of all blog posts with clicks, impressions, CTR and position columns\" loading=\"lazy\">\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\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&nbsp;KB 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\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>Each card&#8217;s image and summary are already in the page&#8217;s <code>&lt;head&gt;<\/code>.<\/h2>\n            <p class=\"bd-eureka-shift\">The shift: stop thinking &#8220;scrape the article,&#8221; start thinking &#8220;read the OpenGraph tags.&#8221;<\/p>\n            <p>Each card needs a cover image, a title, and a short summary. I almost reached for full-page fetching and HTML parsing across 119 posts. Then it clicked: every post already exposes <code>og:image<\/code> (the cover) and <code>og:description<\/code> (a clean, human-written summary) in its head. One <code>curl<\/code> per URL, grep two meta tags, run it twelve-wide in parallel \u2014 the whole catalog resolved in seconds instead of minutes, and the summaries read better than anything I&#8217;d have auto-generated.<\/p>\n          <\/div>\n        <\/li>\n\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\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\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\" src=\"\/blog\/wp-content\/uploads\/2026\/06\/build-diary-internal-linking-strategy-from-search-console-data-2.png\" alt=\"Final spreadsheet with seven landing-page tabs, each row showing title, summary, image URL, alt text, blog URL, clicks and impressions\" loading=\"lazy\">\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\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\n      <\/ol>\n    <\/div>\n  <\/main>\n\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\n      <dl class=\"bd-ledger\" aria-label=\"Time spent by phase\">\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\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\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\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\n<\/article>\n\n<script src=\"\/blog\/wp-content\/uploads\/build-diaries\/internal-linking-strategy-from-search-console-data.js?v=33ca7bfc\" defer><\/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":0,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/469\/revisions"}],"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}]}}