Compare commits
3 commits
main
...
feat/seo-c
Author | SHA1 | Date | |
---|---|---|---|
|
aed9d05ac9 | ||
|
4eb49196cf | ||
|
fd5137528d |
6 changed files with 202 additions and 0 deletions
95
packages/astro/components/SEO.astro
Normal file
95
packages/astro/components/SEO.astro
Normal 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} />
|
||||||
|
)}
|
16
packages/astro/components/Schema.astro
Normal file
16
packages/astro/components/Schema.astro
Normal 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>
|
|
@ -1,2 +1,4 @@
|
||||||
export { default as Code } from './Code.astro';
|
export { default as Code } from './Code.astro';
|
||||||
export { default as Debug } from './Debug.astro';
|
export { default as Debug } from './Debug.astro';
|
||||||
|
export { default as Schema } from './Schema.astro';
|
||||||
|
export { default as SEO } from './SEO.astro';
|
||||||
|
|
80
packages/astro/components/schema.ts
Normal file
80
packages/astro/components/schema.ts
Normal 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({
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': """,
|
||||||
|
"'": "'",
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
}
|
|
@ -128,6 +128,7 @@
|
||||||
"rehype": "^12.0.1",
|
"rehype": "^12.0.1",
|
||||||
"resolve": "^1.22.0",
|
"resolve": "^1.22.0",
|
||||||
"rollup": "^2.75.6",
|
"rollup": "^2.75.6",
|
||||||
|
"schema-dts": "^1.1.0",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"shiki": "^0.10.1",
|
"shiki": "^0.10.1",
|
||||||
"sirv": "^2.0.2",
|
"sirv": "^2.0.2",
|
||||||
|
|
|
@ -450,6 +450,7 @@ importers:
|
||||||
resolve: ^1.22.0
|
resolve: ^1.22.0
|
||||||
rollup: ^2.75.6
|
rollup: ^2.75.6
|
||||||
sass: ^1.52.2
|
sass: ^1.52.2
|
||||||
|
schema-dts: ^1.1.0
|
||||||
semver: ^7.3.7
|
semver: ^7.3.7
|
||||||
shiki: ^0.10.1
|
shiki: ^0.10.1
|
||||||
sirv: ^2.0.2
|
sirv: ^2.0.2
|
||||||
|
@ -507,6 +508,7 @@ importers:
|
||||||
rehype: 12.0.1
|
rehype: 12.0.1
|
||||||
resolve: 1.22.1
|
resolve: 1.22.1
|
||||||
rollup: 2.77.3
|
rollup: 2.77.3
|
||||||
|
schema-dts: 1.1.0
|
||||||
semver: 7.3.7
|
semver: 7.3.7
|
||||||
shiki: 0.10.1
|
shiki: 0.10.1
|
||||||
sirv: 2.0.2
|
sirv: 2.0.2
|
||||||
|
@ -15256,6 +15258,12 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
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:
|
/section-matter/1.0.0:
|
||||||
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
|
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
Loading…
Reference in a new issue