|
| 1 | +const fs = require('fs'); |
| 2 | +const path = require('path'); |
| 3 | + |
| 4 | +const faviconFiles = [ |
| 5 | + 'posts/best-laptops-for-students-2026.html', |
| 6 | + 'posts/best-premium-laptop-for-work-2026.html', |
| 7 | + 'posts/best-remote-work-setup-2026.html', |
| 8 | + 'posts/budget-laptops-under-1000.html', |
| 9 | + 'posts/do-you-need-thunderbolt-dock.html', |
| 10 | + 'posts/is-a-4k-monitor-worth-it.html', |
| 11 | + 'posts/is-samsung-990-pro-worth-it.html', |
| 12 | + 'posts/samsung-odyssey-g8-vs-alienware-aw3423dwf.html' |
| 13 | +]; |
| 14 | + |
| 15 | +const headerFiles = [ |
| 16 | + 'posts/best-laptops-for-students-2026.html', |
| 17 | + 'posts/budget-laptops-under-1000.html', |
| 18 | + 'privacy-policy.html', |
| 19 | + 'terms-of-service.html' |
| 20 | +]; |
| 21 | + |
| 22 | +const missingScriptFiles = [ |
| 23 | + 'about.html', |
| 24 | + 'affiliate-disclosure.html', |
| 25 | + 'amazon-stack.html', |
| 26 | + 'blog.html', |
| 27 | + 'contact.html', |
| 28 | + 'index.html', |
| 29 | + 'privacy-policy.html', |
| 30 | + 'smart-tools.html', |
| 31 | + 'terms-of-service.html', |
| 32 | + 'thank-you.html' |
| 33 | +]; |
| 34 | + |
| 35 | +const mobileRiskFiles = [ |
| 36 | + 'affiliate-disclosure.html', 'amazon-stack.html', 'contact.html', 'index.html', |
| 37 | + 'posts/alienware-aw3423dwf-review.html', 'posts/alienware-aw3423dwf-vs-odyssey-g8.html', |
| 38 | + 'posts/apple-macbook-pro-m4-pro-review.html', 'posts/best-laptops-for-students-2026.html', |
| 39 | + 'posts/best-premium-laptop-for-work-2026.html', 'posts/budget-laptops-under-1000.html', |
| 40 | + 'posts/dell-xps-15-9530-review.html', 'posts/do-you-need-thunderbolt-dock.html', |
| 41 | + 'posts/is-a-4k-monitor-worth-it.html', 'posts/samsung-990-pro-ssd-review.html', |
| 42 | + 'posts/samsung-odyssey-g8-review.html', 'posts/shure-sm7b-review.html', |
| 43 | + 'posts/shure-sm7b-vs-sm7db.html', 'posts/shure-sm7db-review.html', |
| 44 | + 'posts/surface-laptop-studio-2-review.html', 'privacy-policy.html', |
| 45 | + 'smart-tools.html', 'terms-of-service.html', 'thank-you.html' |
| 46 | +]; |
| 47 | + |
| 48 | +function getHtmlFiles(dir, files_) { |
| 49 | + files_ = files_ || []; |
| 50 | + let files = fs.readdirSync(dir); |
| 51 | + for (let file of files) { |
| 52 | + let name = path.join(dir, file); |
| 53 | + if (fs.statSync(name).isDirectory()) { |
| 54 | + if (!['.git', 'node_modules', 'images', 'assets'].includes(file)) { |
| 55 | + getHtmlFiles(name, files_); |
| 56 | + } |
| 57 | + } else if (name.endsWith('.html') && !name.startsWith('old_')) { |
| 58 | + files_.push(name); |
| 59 | + } |
| 60 | + } |
| 61 | + return files_; |
| 62 | +} |
| 63 | + |
| 64 | +const indexContent = fs.readFileSync('index.html', 'utf8'); |
| 65 | + |
| 66 | +// 1. Favicon Fix |
| 67 | +const faviconBlockMatches = indexContent.match(/<link href="assets\/icons\/favicon-32\.png\?v=6"[\s\S]*?<link href="assets\/icons\/favicon\.ico\?v=6" rel="shortcut icon" \/>/); |
| 68 | +if (faviconBlockMatches) { |
| 69 | + const canonicalFaviconBlock = faviconBlockMatches[0]; |
| 70 | + faviconFiles.forEach(file => { |
| 71 | + let p = file; |
| 72 | + if (fs.existsSync(p)) { |
| 73 | + let content = fs.readFileSync(p, 'utf8'); |
| 74 | + |
| 75 | + let relativeFaviconBlock = file.includes('posts/') ? canonicalFaviconBlock.replace(/href="assets\//g, 'href="../assets/') : canonicalFaviconBlock; |
| 76 | + |
| 77 | + content = content.replace(/<link[^>]+favicon[^>]+>/gi, ''); |
| 78 | + content = content.replace(/<link[^>]+apple-touch-icon[^>]+>/gi, ''); |
| 79 | + content = content.replace(/(<\/head>)/i, `${relativeFaviconBlock}\n$1`); |
| 80 | + |
| 81 | + fs.writeFileSync(p, content); |
| 82 | + console.log(`[Favicon] Fixed ${p}`); |
| 83 | + } |
| 84 | + }); |
| 85 | +} |
| 86 | + |
| 87 | +// 2. Header Fix |
| 88 | +const headerMatches = indexContent.match(/<header class="glass-header">[\s\S]*?<\/header>/); |
| 89 | +if (headerMatches) { |
| 90 | + const canonicalHeaderBlock = headerMatches[0]; |
| 91 | + headerFiles.forEach(file => { |
| 92 | + let p = file; |
| 93 | + if (fs.existsSync(p)) { |
| 94 | + let content = fs.readFileSync(p, 'utf8'); |
| 95 | + |
| 96 | + let relativeHeaderBlock = canonicalHeaderBlock; |
| 97 | + if (file.includes('posts/')) { |
| 98 | + relativeHeaderBlock = relativeHeaderBlock.replace(/href="([a-zA-Z0-9-]+\.html)"/g, 'href="../$1"'); |
| 99 | + } |
| 100 | + |
| 101 | + let existingHeaderMatch = content.match(/<header[^>]*>[\s\S]*?<\/header>/i); |
| 102 | + if (existingHeaderMatch) { |
| 103 | + content = content.replace(existingHeaderMatch[0], relativeHeaderBlock); |
| 104 | + } else { |
| 105 | + content = content.replace(/(<body[^>]*>)/i, `$1\n ${relativeHeaderBlock}`); |
| 106 | + } |
| 107 | + |
| 108 | + fs.writeFileSync(p, content); |
| 109 | + console.log(`[Header] Fixed ${p}`); |
| 110 | + } |
| 111 | + }); |
| 112 | +} |
| 113 | + |
| 114 | +// 3. Scripts Fix |
| 115 | +missingScriptFiles.forEach(file => { |
| 116 | + let p = file; |
| 117 | + if (fs.existsSync(p)) { |
| 118 | + let content = fs.readFileSync(p, 'utf8'); |
| 119 | + if (!content.match(/<script[^>]+script\.js"/i)) { |
| 120 | + let scriptTag = file.includes('posts/') ? '<script src="../script.js"></script>' : '<script src="script.js"></script>'; |
| 121 | + content = content.replace(/(<\/body>)/i, ` ${scriptTag}\n$1`); |
| 122 | + fs.writeFileSync(p, content); |
| 123 | + console.log(`[Script] Added to ${p}`); |
| 124 | + } |
| 125 | + } |
| 126 | +}); |
| 127 | + |
| 128 | +// 4. Affiliate Links Fix & Tracker report |
| 129 | +let csvContent = "File,Old URL,New URL,Old Rel,New Rel,Old Target,New Target\n"; |
| 130 | +const allHtmlFiles = getHtmlFiles('.'); |
| 131 | + |
| 132 | +allHtmlFiles.forEach(file => { |
| 133 | + let content = fs.readFileSync(file, 'utf8'); |
| 134 | + let originalContent = content; |
| 135 | + |
| 136 | + // Custom parsing opening tags <a> |
| 137 | + let newContent = content.replace(/<a\s+([^>]+)>/gi, (match, innerProps) => { |
| 138 | + let lowerProps = innerProps.toLowerCase(); |
| 139 | + |
| 140 | + // Check if it's an amazon link |
| 141 | + if (!lowerProps.includes('amazon.com') && !lowerProps.includes('amzn.to')) { |
| 142 | + return match; |
| 143 | + } |
| 144 | + |
| 145 | + // Extract href |
| 146 | + let hrefMatch = innerProps.match(/href=["']([^"']+)["']/i); |
| 147 | + if (!hrefMatch) return match; |
| 148 | + let oldHref = hrefMatch[1]; |
| 149 | + let newHref = oldHref; |
| 150 | + |
| 151 | + // Modify HRREF |
| 152 | + if (!newHref.toLowerCase().includes('tag=techstackglob-20')) { |
| 153 | + if (newHref.includes('?')) { |
| 154 | + newHref = `${newHref}&tag=techstackglob-20`; |
| 155 | + } else { |
| 156 | + newHref = `${newHref}?tag=techstackglob-20`; |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + // Extract target |
| 161 | + let targetMatch = innerProps.match(/target=["']([^"']*)["']/i); |
| 162 | + let oldTarget = targetMatch ? targetMatch[1] : ''; |
| 163 | + let newTarget = '_blank'; |
| 164 | + |
| 165 | + // Extract rel |
| 166 | + let relMatch = innerProps.match(/rel=["']([^"']*)["']/i); |
| 167 | + let oldRel = relMatch ? relMatch[1] : ''; |
| 168 | + let relTokens = oldRel ? oldRel.split(' ').filter(x => x) : []; |
| 169 | + |
| 170 | + let requiredTokens = ['nofollow', 'noopener', 'sponsored']; |
| 171 | + requiredTokens.forEach(token => { |
| 172 | + if (!relTokens.map(t => t.toLowerCase()).includes(token)) { |
| 173 | + relTokens.push(token); |
| 174 | + } |
| 175 | + }); |
| 176 | + let newRel = relTokens.join(' '); |
| 177 | + |
| 178 | + // Reconstruct |
| 179 | + let remaining = innerProps |
| 180 | + .replace(/href=["'][^"']+["']/gi, '') |
| 181 | + .replace(/target=["'][^"']*["']/gi, '') |
| 182 | + .replace(/rel=["'][^"']*["']/gi, ''); |
| 183 | + |
| 184 | + // create clean tag |
| 185 | + let newTag = `<a ${remaining.trim()} href="${newHref}" target="${newTarget}" rel="${newRel}">`.replace(/\s+/g, ' ').replace(' >', '>'); |
| 186 | + |
| 187 | + csvContent += `"${file}","${oldHref}","${newHref}","${oldRel}","${newRel}","${oldTarget}","${newTarget}"\n`; |
| 188 | + |
| 189 | + return newTag; |
| 190 | + }); |
| 191 | + |
| 192 | + if (newContent !== originalContent) { |
| 193 | + fs.writeFileSync(file, newContent); |
| 194 | + console.log(`[Affiliate] Fixed in ${file}`); |
| 195 | + } |
| 196 | +}); |
| 197 | + |
| 198 | +fs.writeFileSync('affiliate_report.csv', csvContent); |
| 199 | + |
| 200 | +// 5. Mobile Overflow |
| 201 | +mobileRiskFiles.forEach(file => { |
| 202 | + let p = file; |
| 203 | + if (fs.existsSync(p)) { |
| 204 | + let content = fs.readFileSync(p, 'utf8'); |
| 205 | + let oContent = content; |
| 206 | + |
| 207 | + // Remove existing table-responsive div to rebuild them carefully |
| 208 | + content = content.replace(/<div class=["']table-responsive["']>\s*(<table[\s\S]*?<\/table>)\s*<\/div>/gi, '$1'); |
| 209 | + content = content.replace(/(<table[\s\S]*?<\/table>)/gi, '<div class="table-responsive">\n$1\n</div>'); |
| 210 | + |
| 211 | + // Images: max-width: 100%, height: auto, loading="lazy" |
| 212 | + // Also remove inline width="..." height="..." to let CSS logic handle it |
| 213 | + content = content.replace(/<img[^>]+>/gi, (match) => { |
| 214 | + let newImg = match; |
| 215 | + if (!newImg.includes('max-width: 100%') && !newImg.includes('max-width:100%')) { |
| 216 | + if (!newImg.includes('style=')) { |
| 217 | + newImg = newImg.replace(/<img/i, '<img style="max-width: 100%; height: auto;"'); |
| 218 | + } else { |
| 219 | + newImg = newImg.replace(/style=["']([^"']*)["']/i, 'style="max-width: 100%; height: auto; $1"'); |
| 220 | + } |
| 221 | + } |
| 222 | + if (!newImg.includes('loading=')) newImg = newImg.replace(/<img/i, '<img loading="lazy"'); |
| 223 | + return newImg; |
| 224 | + }); |
| 225 | + |
| 226 | + // Inline width fixes for extreme pixels e.g. width: 600px |
| 227 | + content = content.replace(/width:\s*[4-9][0-9]{2,}px;?/gi, 'max-width: 100%; height: auto;'); |
| 228 | + |
| 229 | + if (content !== oContent) { |
| 230 | + fs.writeFileSync(p, content); |
| 231 | + console.log(`[Mobile] Responsive fixes in ${p}`); |
| 232 | + } |
| 233 | + } |
| 234 | +}); |
| 235 | + |
| 236 | +// 6. Orphan Fix: samsung-odyssey-g8-vs-alienware-aw3423dwf.html |
| 237 | +// Attach link from best-ultrawide-monitors-2026.html |
| 238 | +let p1 = 'posts/best-ultrawide-monitors-2026.html'; |
| 239 | +if (fs.existsSync(p1)) { |
| 240 | + let c1 = fs.readFileSync(p1, 'utf8'); |
| 241 | + if (!c1.includes('samsung-odyssey-g8-vs-alienware-aw3423dwf.html')) { |
| 242 | + // Add inside a relevant text paragraph |
| 243 | + c1 = c1.replace(/Our full comparison of these top contenders/i, 'Our full <a href="samsung-odyssey-g8-vs-alienware-aw3423dwf.html">AW3423DWF vs Odyssey G8 comparison</a>'); |
| 244 | + if (c1 === fs.readFileSync(p1, 'utf8')) { |
| 245 | + // Find another anchor |
| 246 | + c1 = c1.replace(/Alienware AW3423DWF/i, '<a href="samsung-odyssey-g8-vs-alienware-aw3423dwf.html">AW3423DWF vs Odyssey G8 comparison</a> (also featuring the Alienware AW3423DWF)'); |
| 247 | + } |
| 248 | + fs.writeFileSync(p1, c1); |
| 249 | + console.log(`[Orphan] Added to ${p1}`); |
| 250 | + } |
| 251 | +} |
| 252 | + |
| 253 | +let p2 = 'posts/alienware-aw3423dwf-review.html'; |
| 254 | +if (fs.existsSync(p2)) { |
| 255 | + let c2 = fs.readFileSync(p2, 'utf8'); |
| 256 | + if (!c2.includes('samsung-odyssey-g8-vs-alienware-aw3423dwf.html')) { |
| 257 | + c2 = c2.replace(/(<\/article>)/i, `<p>If you're still on the fence, see our <a href="samsung-odyssey-g8-vs-alienware-aw3423dwf.html">AW3423DWF vs Odyssey G8 comparison</a> for a deep-dive against its biggest rival.</p>\n$1`); |
| 258 | + fs.writeFileSync(p2, c2); |
| 259 | + console.log(`[Orphan] Added to ${p2}`); |
| 260 | + } |
| 261 | +} |
| 262 | + |
| 263 | +console.log('All fixes applied successfully!'); |
0 commit comments