1import { getMetadata } from './lib-franklin.js'; 2import { buildAuthorPath } from './shared.js'; 3import { isSubjectToRegistrationGate } from './authentication/helpers.js'; 4 5/** Class name Google's paywalled-content cssSelector must match (registration wall). */ 6const REGISTRATION_GATE_CSS_CLASS = 'registration-gate'; 7 8function injectJsonLd(data) { 9 const payload = Array.isArray(data) ? data : [data]; 10 const clean = payload.map((obj) => { 11 const copy = { ...obj }; 12 Object.keys(copy).forEach((key) => copy[key] === undefined && delete copy[key]); 13 return copy; 14 }); 15 const jsonLdString = JSON.stringify(clean.length === 1 ? clean[0] : clean, null, 2); 16 const scriptElement = document.createElement('script'); 17 scriptElement.type = 'application/ld+json'; 18 scriptElement.textContent = jsonLdString; 19 document.head.appendChild(scriptElement); 20} 21 22/** 23 * Marks article body wrappers so `hasPart.cssSelector` matches the DOM (Google requirement). 24 */ 25function markPaywalledContent() { 26 document.querySelectorAll('main .content-section .default-content-wrapper').forEach((el) => { 27 el.classList.add(REGISTRATION_GATE_CSS_CLASS); 28 }); 29} 30 31function parseForceGateMetadata() { 32 const raw = getMetadata('forcegate') || getMetadata('forceGate') || ''; 33 const normalized = raw.toString().toLowerCase(); 34 return normalized === 'true' || normalized === 'on' || normalized === '1'; 35} 36 37/** 38 * Injects JSON-LD structured data for SEO. Runs in the eager phase so schemas 39 * are present in the initial page for crawlers (not delayed). 40 */ 41export default function addSEOStructuredData() { 42 const template = (getMetadata('template') || '').toLowerCase(); 43 const { origin } = window.location; 44 const pageUrl = origin + window.location.pathname; 45 46 if (template === 'homepage') { 47 const name = getMetadata('og:title') || 'CRN'; 48 const description = getMetadata('og:description') || 'Technology News for IT Channel Partners and Solution Providers'; 49 const logo = getMetadata('og:image') || `${origin}/icons/CRN-red.png`; 50 51 injectJsonLd([ 52 { 53 '@context': 'https://schema.org/', 54 '@type': 'WebSite', 55 name, 56 url: origin, 57 description: description || undefined, 58 potentialAction: { 59 '@type': 'SearchAction', 60 target: { 61 '@type': 'EntryPoint', 62 urlTemplate: `${origin}/search?q={search_term_string}`, 63 }, 64 'query-input': 'required name=search_term_string', 65 }, 66 }, 67 { 68 '@context': 'https://schema.org/', 69 '@type': 'Organization', 70 name: name || 'CRN', 71 url: origin, 72 logo: logo || undefined, 73 }, 74 ]); 75 return; 76 } 77 78 if (template === 'category') { 79 const name = getMetadata('og:title') || document.title || ''; 80 const description = getMetadata('og:description'); 81 const breadcrumbName = name || 'Category'; 82 83 injectJsonLd([ 84 { 85 '@context': 'https://schema.org/', 86 '@type': 'CollectionPage', 87 name: name || undefined, 88 url: pageUrl, 89 description: description || undefined, 90 }, 91 { 92 '@context': 'https://schema.org/', 93 '@type': 'BreadcrumbList', 94 itemListElement: [ 95 { 96 '@type': 'ListItem', 97 position: 1, 98 name: 'Home', 99 item: origin, 100 }, 101 { 102 '@type': 'ListItem', 103 position: 2, 104 name: breadcrumbName, 105 item: pageUrl, 106 }, 107 ], 108 }, 109 ]); 110 return; 111 } 112 113 if (template === 'company') { 114 const name = getMetadata('og:title') || document.title || ''; 115 const description = getMetadata('og:description'); 116 117 injectJsonLd({ 118 '@context': 'https://schema.org/', 119 '@type': 'Organization', 120 name: name || undefined, 121 url: pageUrl, 122 description: description || undefined, 123 }); 124 return; 125 } 126 127 // Article-style templates: NewsArticle 128 const pageTitle = getMetadata('og:title'); 129 const imageURL = getMetadata('og:image'); 130 const publishedDate = getMetadata('publisheddate'); 131 const modifiedDate = getMetadata('modifieddate') || publishedDate; 132 const author = getMetadata('author'); 133 const authorURL = author ? `https://www.crn.com${buildAuthorPath(author)}` : undefined; 134 const description = getMetadata('og:description'); 135 const articleSection = getMetadata('category'); 136 const publisherName = 'CRN';
137 const publisherLogo = `${origin}/icons/CRN-red.png`; 138 const articleType = getMetadata('type'); 139 let articleGated = getMetadata('gated'); 140 if (articleGated) { 141 articleGated = articleGated.trim().toLowerCase(); 142 } 143 const forceGate = parseForceGateMetadata(); 144 const registrationGated = isSubjectToRegistrationGate( 145 publishedDate, 146 articleType, 147 forceGate, 148 articleGated, 149 ); 150 151 const jsonLdData = { 152 '@context': 'https://schema.org/', 153 '@type': 'NewsArticle', 154 headline: pageTitle || undefined, 155 description: description || undefined, 156 image: imageURL ? [imageURL] : undefined, 157 datePublished: publishedDate || undefined, 158 dateModified: modifiedDate || undefined, 159 mainEntityOfPage: { 160 '@type': 'WebPage', 161 '@id': pageUrl, 162 }, 163 publisher: { 164 '@type': 'Organization', 165 name: publisherName, 166 logo: { 167 '@type': 'ImageObject', 168 url: publisherLogo, 169 }, 170 }, 171 }; 172 173 if (articleSection) { 174 jsonLdData.articleSection = articleSection; 175 } 176 177 if (author && authorURL) { 178 jsonLdData.author = [{ 179 '@type': 'Person', 180 name: author, 181 url: authorURL, 182 }]; 183 } 184 185 // Registration / login wall â Google paywalled-content structured data
186 // https://developers.google.com/search/docs/appearance/structured-data/paywalled-content 187 if (registrationGated) { 188 jsonLdData.isAccessibleForFree = false; 189 jsonLdData.hasPart = { 190 '@type': 'WebPageElement', 191 isAccessibleForFree: false, 192 cssSelector: `.${REGISTRATION_GATE_CSS_CLASS}`, 193 }; 194 markPaywalledContent(); 195 } 196 197 injectJsonLd(jsonLdData); 198}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.