Last active
August 15, 2022 19:26
-
-
Save NightScript370/177098b4ba5f2dab2909dc8dbee7f623 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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