Compare commits

...

3 commits

Author SHA1 Message Date
Tony Sullivan
aed9d05ac9 adding the schema-dts dependency 2022-08-19 19:37:27 -04:00
Tony Sullivan
4eb49196cf tweaking og and twitter card types 2022-08-19 19:34:08 -04:00
Tony Sullivan
fd5137528d WIP: copying over the v0 components 2022-08-19 15:09:44 -04:00
6 changed files with 202 additions and 0 deletions

View file

@ -0,0 +1,95 @@
---
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<SEOMetadata> {
type?: string;
}
export interface Twitter extends Partial<SEOMetadata> {
handle?: string;
card?:
| 'summary'
| '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,
};
---
<!-- Page Metadata -->
<meta name="generator" content={Astro.generator} />
<link rel="canonical" href={canonicalURL} />
<title>{title}</title>
<!-- OpenGraph Tags -->
<meta property="og:title" content={og.title} />
<meta property="og:type" content={og.type} />
<meta property="og:url" content={og.canonicalURL} />
<meta property="og:locale" content={og.locale} />
<meta property="og:description" content={og.description} />
<meta property="og:site_name" content={og.name} />
{og.image && (
<meta property="og:image" content={og.image.src} />
<meta property="og:image:alt" content={og.image.alt} />
)}
<!-- Twitter Tags -->
{twitter.card && (
<meta name="twitter:card" content={twitter.card} />
)}
{twitter.handle && (
<meta name="twitter:site" content={twitter.handle} />
)}
<meta name="twitter:title" content={twitter.title} />
<meta name="twitter:description" content={twitter.description} />
{twitter.image && (
<meta name="twitter:image" content={twitter.image.src} />
<meta name="twitter:image:alt" content={twitter.image.alt} />
)}

View file

@ -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);
---
<script type="application/ld+json" set:html={children}></script>

View file

@ -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';

View file

@ -0,0 +1,80 @@
import type { Graph, Thing, WithContext } from 'schema-dts';
type JsonValueScalar = string | boolean | number;
type JsonValue =
| JsonValueScalar
| Array<JsonValue>
| { [key: string]: JsonValue };
type JsonReplacer = (_: string, value: JsonValue) => JsonValue | undefined;
const ESCAPE_ENTITIES = Object.freeze({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&apos;",
});
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<T extends Thing>(thing: T): WithContext<T> {
return {
'@context': 'https://schema.org',
...(thing as Object)
} as WithContext<T>;
}
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);
}

View file

@ -128,6 +128,7 @@
"rehype": "^12.0.1",
"resolve": "^1.22.0",
"rollup": "^2.75.6",
"schema-dts": "^1.1.0",
"semver": "^7.3.7",
"shiki": "^0.10.1",
"sirv": "^2.0.2",

View file

@ -450,6 +450,7 @@ importers:
resolve: ^1.22.0
rollup: ^2.75.6
sass: ^1.52.2
schema-dts: ^1.1.0
semver: ^7.3.7
shiki: ^0.10.1
sirv: ^2.0.2
@ -507,6 +508,7 @@ importers:
rehype: 12.0.1
resolve: 1.22.1
rollup: 2.77.3
schema-dts: 1.1.0
semver: 7.3.7
shiki: 0.10.1
sirv: 2.0.2
@ -15256,6 +15258,12 @@ packages:
dependencies:
loose-envify: 1.4.0
/schema-dts/1.1.0:
resolution: {integrity: sha512-vdmbs/5ycj4zyKpZIDqTcy+IZi4s7c38RVAYuDmRi7zgxUT8wRWPMLzg0jr7FjdVunYu9yZ00F3+XcZTTFcTOQ==}
peerDependencies:
typescript: '>=4.1.0'
dev: false
/section-matter/1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}