From fd5137528dfc0c9d7777923ffb9e67adfedef36c Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Fri, 19 Aug 2022 15:09:44 -0400 Subject: [PATCH] WIP: copying over the v0 components --- packages/astro/components/SEO.astro | 91 ++++++++++++++++++++++++++ packages/astro/components/Schema.astro | 16 +++++ packages/astro/components/index.ts | 2 + packages/astro/components/schema.ts | 80 ++++++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 packages/astro/components/SEO.astro create mode 100644 packages/astro/components/Schema.astro create mode 100644 packages/astro/components/schema.ts diff --git a/packages/astro/components/SEO.astro b/packages/astro/components/SEO.astro new file mode 100644 index 000000000..f38b2e001 --- /dev/null +++ b/packages/astro/components/SEO.astro @@ -0,0 +1,91 @@ +--- +export interface Image { + src: string; + alt: string; +} + +export interface SEOMetadata { + name: string; + title: string; + description: string; + image: Image; + canonicalURL?: URL | string; + locale?: string; +} + +export interface OpenGraph extends Partial { + type?: 'website' | 'article'; +} + +export interface Twitter extends Partial { + handle?: string; + card?: 'summary_large_image'; +} + +export interface Props extends SEOMetadata { + og?: OpenGraph; + twitter?: Twitter; +} + +const { + name, + title, + description, + image, + locale = 'en', + canonicalURL = new URL(Astro.url.pathname, Astro.site), + og: _og = { }, + twitter: _twitter = { } +} = Astro.props as Props; + +const og: OpenGraph = { + name, + title, + description, + canonicalURL, + image, + locale, + type: 'website', + ..._og, +} + +const twitter: Twitter = { + name, + title, + description, + canonicalURL, + image, + locale, + card: 'summary_large_image', + ..._twitter, +}; +--- + + + + +{title} + + + + + + + + +{og.image && ( + + +)} + + + +{twitter.handle && ( + +)} + + +{twitter.image && ( + + +)} diff --git a/packages/astro/components/Schema.astro b/packages/astro/components/Schema.astro new file mode 100644 index 000000000..d7aa773fa --- /dev/null +++ b/packages/astro/components/Schema.astro @@ -0,0 +1,16 @@ +--- +import type { Thing } from 'schema-dts'; +import { ldToString } from './schema.js'; + +export interface Props { + /** Adds indentation, white space, and line break characters to JSON-LD output. {@link JSON.stringify} */ + space?: string | number; + json: Thing | Thing[]; +} + +const { space, json } = Astro.props as Props; + +const children = ldToString(json, space); +--- + + diff --git a/packages/astro/components/index.ts b/packages/astro/components/index.ts index 864c7cc3b..2771318f5 100644 --- a/packages/astro/components/index.ts +++ b/packages/astro/components/index.ts @@ -1,2 +1,4 @@ export { default as Code } from './Code.astro'; export { default as Debug } from './Debug.astro'; +export { default as Schema } from './Schema.astro'; +export { default as SEO } from './SEO.astro'; diff --git a/packages/astro/components/schema.ts b/packages/astro/components/schema.ts new file mode 100644 index 000000000..6fe5054ab --- /dev/null +++ b/packages/astro/components/schema.ts @@ -0,0 +1,80 @@ +import type { Graph, Thing, WithContext } from 'schema-dts'; + +type JsonValueScalar = string | boolean | number; +type JsonValue = + | JsonValueScalar + | Array + | { [key: string]: JsonValue }; +type JsonReplacer = (_: string, value: JsonValue) => JsonValue | undefined; + +const ESCAPE_ENTITIES = Object.freeze({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", +}); +const ESCAPE_REGEX = new RegExp( + `[${Object.keys(ESCAPE_ENTITIES).join("")}]`, + "g" +); +const ESCAPE_REPLACER = (t: string): string => + ESCAPE_ENTITIES[t as keyof typeof ESCAPE_ENTITIES]; + +/** +* A replacer for JSON.stringify to strip JSON-LD of illegal HTML entities +* per https://www.w3.org/TR/json-ld11/#restrictions-for-contents-of-json-ld-script-elements +*/ +const safeJsonLdReplacer: JsonReplacer = (() => { + // Replace per https://www.w3.org/TR/json-ld11/#restrictions-for-contents-of-json-ld-script-elements + // Solution from https://stackoverflow.com/a/5499821/864313 + return (_: string, value: JsonValue): JsonValue | undefined => { + switch (typeof value) { + case "object": + // Omit null values. + if (value === null) { + return undefined; + } + + return value; // JSON.stringify will recursively call replacer. + case "number": + case "boolean": + case "bigint": + return value; // These values are not risky. + case "string": + return value.replace(ESCAPE_REGEX, ESCAPE_REPLACER); + default: { + // We shouldn't expect other types. + isNever(value); + + // JSON.stringify will remove this element. + return undefined; + } + } + }; +})(); + +// Utility: Assert never +function isNever(_: never): void {} + +function withContext(thing: T): WithContext { + return { + '@context': 'https://schema.org', + ...(thing as Object) + } as WithContext; +} + +function asGraph(things: Thing[]): Graph { + return { + '@context': 'https://schema.org', + '@graph': things + } +} + +export function ldToString(json: Thing | Thing[], space?: number | string) { + const ld = Array.isArray(json) + ? asGraph(json) + : withContext(json); + + return JSON.stringify(ld, safeJsonLdReplacer, space); +}