Skip to content

Instantly share code, notes, and snippets.

@NightScript370
Last active August 15, 2022 19:26
Show Gist options
  • Select an option

  • Save NightScript370/177098b4ba5f2dab2909dc8dbee7f623 to your computer and use it in GitHub Desktop.

Select an option

Save NightScript370/177098b4ba5f2dab2909dc8dbee7f623 to your computer and use it in GitHub Desktop.
import { JSDOM } from 'jsdom';
import {Thing, WithContext, LocalBusiness, FAQPage} from '../../typings/schema.d.ts';
import config from '../data/siteConfig.json' assert { type: "json" }
import * as path from "path";
export default class schemaHandler {
document: Document;
schema: WithContext<Exclude<Thing, string>>;
constructor(htmlDocument:string) {
this.document = (new JSDOM(htmlDocument)).window.document;
this.schema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
'name': config.meta.name,
'url': config.meta.url
};
}
generateSchema() {
// No Reason to log "SiteNavigationElement"
// https://support.google.com/webmasters/thread/11476324?hl=en&msgid=11521744
const itemType = this.document.querySelector('html')?.getAttribute('itemtype')
// First one up is LocalBusiness, but within LocalBusiness, there's a lot of subcategories.
// We cannot go ahead and extract the names of the types from LocalBusiness, but the file is local
const localBusinessSubEntries = Deno.readTextFileSync(path.join(Deno.cwd(), 'typings', 'schema.d.ts'))
.split('\n')
.find(line => line.startsWith('export declare type LocalBusiness = '))!
.replace('export declare type LocalBusiness = ', '')
.replace(' | string', '')
.split(' | ');
localBusinessSubEntries.push('LocalBusiness')
const localBusinessTypeFind = localBusinessSubEntries.find(type => itemType?.endsWith(type))
if (localBusinessTypeFind) {
let localSchema = Object.assign(this.schema, {"additionalType": localBusinessTypeFind}) as unknown as WithContext<Exclude<LocalBusiness, string>>;
// Reviews are not welcome
// https://developers.google.com/search/blog/2019/09/making-review-rich-results-more-helpful#self-serving-reviews-arent-allowed-for-localbusiness-and-organization
const telePhoneLinks = Array.from(document.getElementsByTagName('a'))
.filter(element => element.hasAttribute('href') && element.getAttribute('href')?.startsWith('tel:'))
.map(element => element.getAttribute('href')!);
const telePhoneNumbers = telePhoneLinks.map(numberHREF => numberHREF.replace('tel:', '').trim())
const telePhoneSet = new Set(telePhoneNumbers);
if (telePhoneSet.size == 1) {
localSchema.telephone = [...telePhoneSet.values()][0]
}
const emailLinks = Array.from(document.getElementsByTagName('a'))
.filter(element => element.hasAttribute('href') && element.getAttribute('href')?.startsWith('mailto:'))
.map(element => element.getAttribute('href')!);
const emails = emailLinks.map(numberHREF => numberHREF.replace('mailto:', '').trim())
const emailSet = new Set(emails);
if (emailSet.size == 1) {
localSchema.email = [...emailSet.values()][0]
}
localSchema = markupAddress(document, localSchema, localBusinessTypeFind)
this.schema = localSchema;
} else if (itemType?.includes('FAQPage')) {
this.schema = Object.assign(this.schema, {"additionalType": "FAQPage", mainEntity: Array.from(document.querySelectorAll('[itemprop="mainEntity"][itemtype^="Question"]')!)
.map(questionElementContainer => ({
"@type": "Question",
"name": questionElementContainer.querySelector('[itemprop^="name"]')?.innerHTML!,
"acceptedAnswer": {
"@type": "Answer",
"text": questionElementContainer.querySelector('[itemprop^="text"]')?.innerHTML!
}
}))}) as WithContext<FAQPage>;
}
this.schema.sameAs = Array.from(this.document.getElementsByTagName('a'))
.filter(aElement => { try { new URL(aElement.href ); return true;} catch (_e) { return false; } })
.map(anchorElement => new URL(anchorElement.href))
.filter(url =>
url.hostname.includes('facebook.com')
|| url.hostname.includes("youtube.com")
|| url.hostname.includes("twitter.com")
|| url.hostname.includes('tiktok.com')
|| url.hostname.includes('discord.com')
|| url.hostname.includes('yelp.com'))
.map(urlObj => urlObj.href)
}
generateHTML() {
const scriptTag = this.document.createElement('script');
scriptTag.type = "application/ld+json";
scriptTag.innerHTML = JSON.stringify(this.schema)
this.document.body.appendChild(scriptTag);
return '<!DOCTYPE HTML>' + this.document.documentElement.outerHTML;
}
}
function markupAddress<T extends WithContext<Exclude<LocalBusiness, string>>>(document:Document, schema: T, businessType: string): T {
let addressPotentials = Array.from(document.querySelectorAll('[itemscope][itemprop="address"][itemtype^="PostalAddress"]'))
if (addressPotentials.length > 1) {
addressPotentials = addressPotentials
.filter(addressElement => {
let node = addressElement.parentElement;
let directSubset = false;
while (node !== null) {
if (node.hasAttribute('itemscope')) {
if (node.getAttribute('itemtype')!.includes(businessType))
directSubset = true;
break;
}
node = node.parentElement;
}
return directSubset;
})
}
if (addressPotentials.length !== 1)
return schema;
schema.address = {
"@type": "PostalAddress",
"addressLocality": addressPotentials[0].querySelector<HTMLElement>('[itemprop="addressLocality"]')?.innerText?.trim() || "",
"addressRegion": addressPotentials[0].querySelector<HTMLElement>('[itemprop="addressRegion"]')?.innerText?.trim() || "",
"streetAddress": addressPotentials[0].querySelector<HTMLElement>('[itemprop="streetAddress"]')?.innerText?.trim() || "",
"postalCode": addressPotentials[0].querySelector<HTMLElement>('[itemprop="postalCode"]')?.innerText?.trim() || ""
}
return schema;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment