diff options
| -rw-r--r-- | src/components/Head.astro | 9 | ||||
| -rw-r--r-- | src/components/JsonLd.astro | 12 | ||||
| -rw-r--r-- | src/layouts/BaseLayout.astro | 9 | ||||
| -rw-r--r-- | src/pages/404.astro | 20 | ||||
| -rw-r--r-- | src/pages/blog/[...slug].astro | 44 | ||||
| -rw-r--r-- | src/pages/blog/index.astro | 20 | ||||
| -rw-r--r-- | src/pages/index.astro | 12 | ||||
| -rw-r--r-- | src/utils/schemas/blogPostSchema.ts | 49 | ||||
| -rw-r--r-- | src/utils/schemas/blogSchema.ts | 46 | ||||
| -rw-r--r-- | src/utils/schemas/breadcrumbSchema.ts | 21 | ||||
| -rw-r--r-- | src/utils/schemas/ids.ts | 3 | ||||
| -rw-r--r-- | src/utils/schemas/pageSchema.ts | 43 | ||||
| -rw-r--r-- | src/utils/schemas/personSchema.ts | 17 | ||||
| -rw-r--r-- | src/utils/schemas/websiteSchema.ts | 23 |
14 files changed, 227 insertions, 101 deletions
diff --git a/src/components/Head.astro b/src/components/Head.astro index 3fded95..258162d 100644 --- a/src/components/Head.astro +++ b/src/components/Head.astro @@ -1,15 +1,16 @@ --- -import type { WithContext, Thing } from "schema-dts"; +import type { Thing } from "schema-dts"; import JsonLd from "./JsonLd.astro"; type Props = { readonly description: string; readonly preview: string; - readonly schema: WithContext<Thing>; + readonly robots?: string; + readonly schema: Thing[]; readonly title: string; }; -const { description, preview, schema, title } = Astro.props; +const { description, preview, robots = "index, follow", schema, title } = Astro.props; const canonicalUrl = new URL(Astro.url.pathname, Astro.site); const previewUrl = new URL(preview, Astro.site); @@ -21,7 +22,7 @@ const previewUrl = new URL(preview, Astro.site); <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta name="description" content={description} /> - <meta name="robots" content="index, follow" /> + <meta name="robots" content={robots} /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="/feed.xml" rel="alternate" title="RSS" type="application/atom+xml" /> diff --git a/src/components/JsonLd.astro b/src/components/JsonLd.astro index b58efd7..d12cb78 100644 --- a/src/components/JsonLd.astro +++ b/src/components/JsonLd.astro @@ -1,12 +1,18 @@ --- -import type { WithContext, Thing } from "schema-dts"; +import type { Thing } from "schema-dts"; type Props = { - readonly schema: WithContext<Thing>; + readonly schema: Thing[]; }; const { schema } = Astro.props; -const json = JSON.stringify(schema); + +const payload = { + "@context": "https://schema.org", + "@graph": schema, +}; + +const json = JSON.stringify(payload); --- <!-- JSON-LD --> diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index ed3baeb..a2f0327 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,5 +1,5 @@ --- -import type { WithContext, Thing } from "schema-dts"; +import type { Thing } from "schema-dts"; import Analytics from "../components/Analytics.astro"; import Head from "../components/Head.astro"; import Header from "../components/Header.astro"; @@ -9,15 +9,16 @@ type Props = { readonly description: string; readonly lang: string; readonly preview: string; - readonly schema: WithContext<Thing>; + readonly robots?: string; + readonly schema: Thing[]; readonly title: string; }; -const { description, lang, preview, schema, title } = Astro.props; +const { description, lang, preview, robots, schema, title } = Astro.props; --- <html lang={lang}> - <Head title={title} description={description} preview={preview} schema={schema} /> + <Head title={title} description={description} preview={preview} robots={robots} schema={schema} /> <body> <main> diff --git a/src/pages/404.astro b/src/pages/404.astro index 3ec9feb..e76215e 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -8,16 +8,20 @@ const description = "The page you're looking for doesn't exist!"; const preview = config.og.defaultPreview; const lang = "en"; -const schema = pageSchema({ - siteUrl: new URL("/", Astro.site).toString(), - page: "/404", - title, - description, - lang, -}); +const siteUrl = new URL("/", Astro.site).toString(); + +const schema = [ + pageSchema({ + siteUrl, + page: "/404", + title, + description, + lang, + }), +]; --- -<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}> +<Layout title={title} description={description} preview={preview} lang={lang} robots="noindex, follow" schema={schema}> <div style={{ "text-align": "center" }}> <h1>404</h1> <p><strong>Page not found</strong></p> diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index 3bd2c61..5ba6b3b 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -1,9 +1,13 @@ --- import { type CollectionEntry, getCollection, render } from "astro:content"; -import Comments from "../../components/Comments.astro"; -import Layout from "../../layouts/BaseLayout.astro"; import blogPostSchema from "../../utils/schemas/blogPostSchema"; +import breadcrumbSchema from "../../utils/schemas/breadcrumbSchema"; +import Comments from "../../components/Comments.astro"; import dayjs from "dayjs"; +import Layout from "../../layouts/BaseLayout.astro"; +import personSchema from "../../utils/schemas/personSchema"; +import websiteSchema from "../../utils/schemas/websiteSchema"; +import { config } from "../../config"; type Props = CollectionEntry<"blog">; @@ -33,17 +37,31 @@ const dateModified = post.data.dateModified?.toISOString(); const datePublished = post.data.datePublished.toISOString(); const formattedDate = dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY"); -const schema = blogPostSchema({ - siteUrl: new URL("/", Astro.site).toString(), - dateModified, - datePublished, - description, - isBasedOn, - lang, - preview, - slug, - title, -}); +const siteUrl = new URL("/", Astro.site).toString(); + +const schema = [ + websiteSchema({ siteUrl, name: config.og.website, description, lang }), + personSchema({ siteUrl }), + blogPostSchema({ + siteUrl, + dateModified, + datePublished, + description, + isBasedOn, + lang, + preview, + slug, + title, + }), + breadcrumbSchema({ + siteUrl, + items: [ + { name: "Home", url: "/" }, + { name: "Blog", url: "/blog/" }, + { name: title, url: `/blog/${slug}` }, + ], + }), +]; --- <style lang="scss"> diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 3a27111..aa1b46c 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -3,9 +3,11 @@ import type { CollectionEntry } from "astro:content"; import { config } from "../../config"; import { getCollection } from "astro:content"; import blogSchema from "../../utils/schemas/blogSchema"; +import breadcrumbSchema from "../../utils/schemas/breadcrumbSchema"; import Layout from "../../layouts/BaseLayout.astro"; import PostElement from "../../components/PostElement.astro"; import RSSIcon from "../../components/Icons/RSS.astro"; +import websiteSchema from "../../utils/schemas/websiteSchema"; const posts = await getCollection("blog", ({ data }) => { return data.draft !== true; @@ -29,11 +31,19 @@ const description = "Explore Valentin Popov's blog on software development, tech const preview = config.og.defaultPreview; const lang = "en"; -const schema = blogSchema({ - siteUrl: new URL("/", Astro.site).toString(), - title, - posts, -}); +const siteUrl = new URL("/", Astro.site).toString(); + +const schema = [ + websiteSchema({ siteUrl, name: config.og.website, description, lang }), + blogSchema({ siteUrl, title, description, lang, posts }), + breadcrumbSchema({ + siteUrl, + items: [ + { name: "Home", url: "/" }, + { name: "Blog", url: "/blog/" }, + ], + }), +]; --- <Layout title={title} description={description} preview={preview} lang={lang} schema={schema}> diff --git a/src/pages/index.astro b/src/pages/index.astro index b235b9b..b9c6400 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,21 +3,19 @@ import { config } from "../config"; import LatestPostsSection from "../components/Sections/LatestPosts.astro"; import Layout from "../layouts/BaseLayout.astro"; import pageSchema from "../utils/schemas/pageSchema"; +import personSchema from "../utils/schemas/personSchema"; import SocialLinksSection from "../components/Sections/SocialLinks.astro"; import WelcomeSection from "../components/Sections/Welcome.astro"; +import websiteSchema from "../utils/schemas/websiteSchema"; const title = "Valentin Popov – Software Developer & Team Lead | Tech Insights"; const description = "Blog by Valentin Popov — software developer and team lead writing about code, side projects, digital tools, and fun experiments."; const preview = config.og.defaultPreview; const lang = "en"; -const schema = pageSchema({ - siteUrl: new URL("/", Astro.site).toString(), - page: "/", - title, - description, - lang, -}); +const siteUrl = new URL("/", Astro.site).toString(); + +const schema = [websiteSchema({ siteUrl, name: config.og.website, description, lang }), personSchema({ siteUrl }), pageSchema({ siteUrl, page: "/", title, description, lang, type: "ProfilePage" })]; --- <Layout title={title} description={description} preview={preview} lang={lang} schema={schema}> diff --git a/src/utils/schemas/blogPostSchema.ts b/src/utils/schemas/blogPostSchema.ts index 87e1bf2..76c6a6f 100644 --- a/src/utils/schemas/blogPostSchema.ts +++ b/src/utils/schemas/blogPostSchema.ts @@ -1,5 +1,5 @@ -import type { WithContext, BlogPosting } from "schema-dts"; -import { config } from "../../config"; +import type { BlogPosting } from "schema-dts"; +import { personId, websiteId } from "./ids"; export type BlogPostSchemaParams = { readonly dateModified: string; @@ -13,25 +13,26 @@ export type BlogPostSchemaParams = { readonly title: string; }; -export default ({ siteUrl, slug, title, description, preview, datePublished, dateModified, lang, isBasedOn }: BlogPostSchemaParams): WithContext<BlogPosting> => ({ - "@context": "https://schema.org", - "@type": "BlogPosting", - "url": new URL(`/blog/${slug}`, siteUrl).toString(), - "headline": title, - "description": description, - "image": new URL(preview, siteUrl).toString(), - "datePublished": datePublished, - "dateModified": dateModified, - "inLanguage": lang, - "author": { - "@type": "Person", - "name": config.author.name, - "url": config.author.url, - "sameAs": config.author.sameAs, - }, - "mainEntityOfPage": { - "@type": "WebPage", - "@id": new URL(`/blog/${slug}`, siteUrl).toString(), - }, - ...(isBasedOn && { isBasedOn: isBasedOn }), -}); +export default ({ siteUrl, slug, title, description, preview, datePublished, dateModified, lang, isBasedOn }: BlogPostSchemaParams): BlogPosting => { + const url = new URL(`/blog/${slug}`, siteUrl).toString(); + + return { + "@type": "BlogPosting", + "@id": url, + "url": url, + "headline": title, + "description": description, + "image": new URL(preview, siteUrl).toString(), + "datePublished": datePublished, + "dateModified": dateModified, + "inLanguage": lang, + "author": { "@id": personId(siteUrl) }, + "publisher": { "@id": personId(siteUrl) }, + "isPartOf": { "@id": websiteId(siteUrl) }, + "mainEntityOfPage": { + "@type": "WebPage", + "@id": url, + }, + ...(isBasedOn && { isBasedOn: isBasedOn }), + }; +}; diff --git a/src/utils/schemas/blogSchema.ts b/src/utils/schemas/blogSchema.ts index 196f037..ff3ce65 100644 --- a/src/utils/schemas/blogSchema.ts +++ b/src/utils/schemas/blogSchema.ts @@ -1,26 +1,36 @@ -import type { WithContext, CollectionPage } from "schema-dts"; +import type { CollectionPage } from "schema-dts"; import type { CollectionEntry } from "astro:content"; +import { websiteId } from "./ids"; export type BlogSchemaParams = { + readonly description: string; + readonly lang: string; readonly posts: CollectionEntry<"blog">[]; readonly siteUrl: string; readonly title: string; }; -export default ({ siteUrl, title, posts }: BlogSchemaParams): WithContext<CollectionPage> => ({ - "@context": "https://schema.org", - "@type": "CollectionPage", - "url": new URL("/blog/", siteUrl).toString(), - "name": title, - "mainEntity": { - "@type": "ItemList", - "itemListOrder": "https://schema.org/ItemListOrderDescending", - "numberOfItems": posts.length, - "itemListElement": posts.map((post, index) => ({ - "@type": "ListItem", - "position": index + 1, - "url": new URL(`/blog/${post.id}`, siteUrl).toString(), - "name": post.data.title, - })), - }, -}); +export default ({ siteUrl, title, description, lang, posts }: BlogSchemaParams): CollectionPage => { + const url = new URL("/blog/", siteUrl).toString(); + + return { + "@type": "CollectionPage", + "@id": url, + "url": url, + "name": title, + "description": description, + "inLanguage": lang, + "isPartOf": { "@id": websiteId(siteUrl) }, + "mainEntity": { + "@type": "ItemList", + "itemListOrder": "https://schema.org/ItemListOrderDescending", + "numberOfItems": posts.length, + "itemListElement": posts.map((post, index) => ({ + "@type": "ListItem", + "position": index + 1, + "url": new URL(`/blog/${post.id}`, siteUrl).toString(), + "name": post.data.title, + })), + }, + }; +}; diff --git a/src/utils/schemas/breadcrumbSchema.ts b/src/utils/schemas/breadcrumbSchema.ts new file mode 100644 index 0000000..e1a1ea3 --- /dev/null +++ b/src/utils/schemas/breadcrumbSchema.ts @@ -0,0 +1,21 @@ +import type { BreadcrumbList } from "schema-dts"; + +export type BreadcrumbItem = { + readonly name: string; + readonly url: string; +}; + +export type BreadcrumbSchemaParams = { + readonly items: BreadcrumbItem[]; + readonly siteUrl: string; +}; + +export default ({ items, siteUrl }: BreadcrumbSchemaParams): BreadcrumbList => ({ + "@type": "BreadcrumbList", + "itemListElement": items.map((item, index) => ({ + "@type": "ListItem", + "position": index + 1, + "name": item.name, + "item": new URL(item.url, siteUrl).toString(), + })), +}); diff --git a/src/utils/schemas/ids.ts b/src/utils/schemas/ids.ts new file mode 100644 index 0000000..1d7e8c5 --- /dev/null +++ b/src/utils/schemas/ids.ts @@ -0,0 +1,3 @@ +export const websiteId = (siteUrl: string): string => new URL("#website", siteUrl).toString(); + +export const personId = (siteUrl: string): string => new URL("#person", siteUrl).toString(); diff --git a/src/utils/schemas/pageSchema.ts b/src/utils/schemas/pageSchema.ts index 606488b..ba8fd86 100644 --- a/src/utils/schemas/pageSchema.ts +++ b/src/utils/schemas/pageSchema.ts @@ -1,23 +1,36 @@ -import type { WithContext, WebPage } from "schema-dts"; +import type { ProfilePage, WebPage } from "schema-dts"; +import { personId, websiteId } from "./ids"; export type WebsiteSchemaParams = { readonly description: string; + readonly lang: string; + readonly mainEntityId?: string; readonly page: string; readonly siteUrl: string; readonly title: string; - readonly lang: string; + readonly type?: "WebPage" | "ProfilePage"; }; -export default ({ siteUrl, page, title, description, lang }: WebsiteSchemaParams): WithContext<WebPage> => ({ - "@context": "https://schema.org", - "@type": "WebPage", - "@id": new URL(page, siteUrl).toString(), - "url": new URL(page, siteUrl).toString(), - "name": title, - "description": description, - "inLanguage": lang, - "mainEntity": { - "@type": "WebSite", - "@id": new URL("/", siteUrl).toString(), - }, -}); +export default ({ siteUrl, page, title, description, lang, type = "WebPage", mainEntityId }: WebsiteSchemaParams): WebPage | ProfilePage => { + const url = new URL(page, siteUrl).toString(); + + const base = { + "@type": type, + "@id": url, + "url": url, + "name": title, + "description": description, + "inLanguage": lang, + "isPartOf": { "@id": websiteId(siteUrl) }, + } as const; + + if (type === "ProfilePage") { + return { + ...base, + "@type": "ProfilePage", + "mainEntity": { "@id": mainEntityId ?? personId(siteUrl) }, + }; + } + + return base; +}; diff --git a/src/utils/schemas/personSchema.ts b/src/utils/schemas/personSchema.ts new file mode 100644 index 0000000..77e443d --- /dev/null +++ b/src/utils/schemas/personSchema.ts @@ -0,0 +1,17 @@ +import type { Person } from "schema-dts"; +import { config } from "../../config"; +import { personId } from "./ids"; + +export type PersonSchemaParams = { + readonly siteUrl: string; +}; + +export default ({ siteUrl }: PersonSchemaParams): Person => ({ + "@type": "Person", + "@id": personId(siteUrl), + "name": config.author.name, + "url": config.author.url, + "email": config.author.email, + "image": new URL(config.og.defaultPreview, siteUrl).toString(), + "sameAs": config.author.sameAs, +}); diff --git a/src/utils/schemas/websiteSchema.ts b/src/utils/schemas/websiteSchema.ts new file mode 100644 index 0000000..2f69427 --- /dev/null +++ b/src/utils/schemas/websiteSchema.ts @@ -0,0 +1,23 @@ +import type { WebSite } from "schema-dts"; +import { config } from "../../config"; +import { personId, websiteId } from "./ids"; + +export type WebsiteSchemaParams = { + readonly description: string; + readonly lang: string; + readonly name: string; + readonly siteUrl: string; +}; + +export default ({ siteUrl, name, description, lang }: WebsiteSchemaParams): WebSite => ({ + "@type": "WebSite", + "@id": websiteId(siteUrl), + "url": siteUrl, + "name": name, + "description": description, + "inLanguage": lang, + "publisher": { "@id": personId(siteUrl) }, + "author": { "@id": personId(siteUrl) }, + "copyrightHolder": { "@id": personId(siteUrl) }, + "sameAs": config.author.sameAs, +}); |
