PageSourceSearch

https://www.crn.com/scripts/seo-structured-data.js

js crn.com collected 2026-09-24 06:49:10 UTC 6,062 bytes, 198 lines download raw bytes

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.