From f0e73cf90005709e20b2f16f63d67d6802af3332 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 22 Jan 2024 04:27:34 +0100 Subject: feat(layout): add seo metadata to all pages --- apps/website/src/components/shared/BaseHead.astro | 51 +++++++++++- apps/website/src/components/shared/SEO.astro | 91 +++++++++++----------- apps/website/src/content/blog/first.md | 2 +- apps/website/src/content/config.ts | 11 ++- apps/website/src/data/site-info.ts | 22 ++++++ apps/website/src/layouts/BlogPost.astro | 6 +- apps/website/src/layouts/Layout.astro | 27 +------ apps/website/src/pages/about.astro | 2 +- apps/website/src/pages/blog/index.astro | 8 +- apps/website/src/pages/branding.astro | 2 +- apps/website/src/pages/contact.astro | 2 +- apps/website/src/pages/index.astro | 2 +- apps/website/src/pages/legal/ip.astro | 2 +- apps/website/src/pages/legal/privacy.astro | 2 +- apps/website/src/pages/legal/security.astro | 2 +- apps/website/src/pages/legal/terms.astro | 2 +- apps/website/src/pages/mods.astro | 2 +- apps/website/src/pages/oss.astro | 2 +- .../src/pages/projects/oneconfig/download.astro | 2 +- .../src/pages/projects/oneconfig/index.astro | 2 +- apps/website/src/pages/rss.xml.js | 16 ---- apps/website/src/pages/rss.xml.ts | 30 +++++++ apps/website/src/process-env.ts | 14 ++++ apps/website/tsconfig.json | 4 +- 24 files changed, 193 insertions(+), 113 deletions(-) create mode 100644 apps/website/src/data/site-info.ts delete mode 100644 apps/website/src/pages/rss.xml.js create mode 100644 apps/website/src/pages/rss.xml.ts create mode 100644 apps/website/src/process-env.ts (limited to 'apps/website') diff --git a/apps/website/src/components/shared/BaseHead.astro b/apps/website/src/components/shared/BaseHead.astro index bfe8f78..39e98a7 100644 --- a/apps/website/src/components/shared/BaseHead.astro +++ b/apps/website/src/components/shared/BaseHead.astro @@ -1,12 +1,57 @@ --- +import smartypants from 'smartypants'; +import siteInfo from '../../data/site-info'; +import SEO from './SEO.astro'; +import Favicon from '/media/polyfrost/minimal_bg.svg?url'; + export type Props = { - siteName: string; title?: string; - description: string; - image: { src: string; alt: string }; + description?: string; + image?: { src: string; alt: string }; canonicalURL?: URL | null; pageType?: 'website' | 'article'; }; const twitterHandle = 'polyfrost'; + +const { + description = siteInfo.description, + image = siteInfo.image, + canonicalURL = new URL(Astro.request.url, Astro.site), + pageType = 'website', +} = Astro.props; + +const title = [Astro.props.title, siteInfo.name].filter(Boolean).join(' | '); +const resolvedImage = { + src: new URL(image.src, Astro.site).toString(), + alt: image.alt, +}; --- + + + + + +<meta name="generator" content={Astro.generator} /> + +<!-- Font Preloads --> +<link rel="preconnect" href="https://fonts.googleapis.com"/> +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/> +<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"/> +<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&display=swap" rel="stylesheet"/> + +<!-- Low Priority Global Metadata --> +<link rel="icon" type="image/svg+xml" href={Favicon}/> +<link rel="sitemap" href="/sitemap-index.xml" /> +<link rel="mask-icon" href="/media/polyfrost/minimal_bg.svg" /> +<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="RSS" /> + +<SEO + name={siteInfo.name} + title={title} + description={description} + image={resolvedImage} + twitter={{ handle: twitterHandle }} + og={{ type: pageType }} + canonicalURL={canonicalURL} +/> diff --git a/apps/website/src/components/shared/SEO.astro b/apps/website/src/components/shared/SEO.astro index c3b8c52..cd44da4 100644 --- a/apps/website/src/components/shared/SEO.astro +++ b/apps/website/src/components/shared/SEO.astro @@ -1,20 +1,23 @@ --- -import smartypants from 'smartypants'; +export type Image = { + src: string; + alt: string; +}; -type SEOMetadata = { - name?: string; +export type SEOMetadata = { + name: string; title: string; description: string; - image?: { src: string; alt: string }; - canonicalURL?: URL | null; + image: Image; + canonicalURL?: URL | string | null; locale?: string; }; -type OpenGraph = Partial<SEOMetadata> & { +export type OpenGraph = Partial<SEOMetadata> & { type?: string; }; -type Twitter = Partial<SEOMetadata> & { +export type Twitter = Partial<SEOMetadata> & { handle?: string; card?: 'summary' | 'summary_large_image'; }; @@ -26,64 +29,62 @@ export type Props = SEOMetadata & { const { name, + title, description, image, locale = 'en', canonicalURL = new URL(Astro.url.pathname, Astro.site), - og: _og = {}, - twitter: _twitter = {}, } = Astro.props; -const title = [Astro.props.title, name].filter(Boolean).join(' | '); -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 }; -const ensureSlash = (url: string | URL) => `${url.toString().replace(/\/$/, '')}/`; ---- +const og = { + name, + title, + description, + canonicalURL, + image, + locale, + type: 'website', + ...Astro.props.og, +} satisfies OpenGraph; -<!-- Global Metadata --> -<meta charset="utf-8" /> -<meta name="generator" content={Astro.generator} /> -<meta name="viewport" content="width=device-width" /> -<meta name="theme-color" content="#d2e1f9" /> -<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> -<link rel="mask-icon" href="/favicon.svg" color="#d2e1f9" /> -<link rel="sitemap" href="/sitemap-index.xml" /> -<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="RSS" /> +const twitter = { + name, + title, + description, + canonicalURL, + image, + locale, + card: 'summary_large_image', + ...Astro.props.twitter, +} satisfies Twitter; -<title set:html={smartypants(title, 1)} /> +function formatCanonicalURL(url: string | URL) { + const path = url.toString(); + const hasQueryParams = path.includes('?'); + if (hasQueryParams) + path.replace(/\/?$/, ''); + return path.replace(/\/?$/, hasQueryParams ? '' : ''); +} +--- <!-- Page Metadata --> -<meta name="generator" content={Astro.generator} /> -{canonicalURL && <link rel="canonical" href={ensureSlash(canonicalURL)} />} -<title>{title} +{canonicalURL && } -{og.canonicalURL && } +{og.canonicalURL && } -{ - og.image && ( - <> - - - - ) -} +{og.image && } +{og.image && } {twitter.card && } -{twitter.handle && } +{twitter.handle && } -{ - twitter.image && ( - <> - - - - ) -} +{twitter.image && } +{twitter.image && } diff --git a/apps/website/src/content/blog/first.md b/apps/website/src/content/blog/first.md index 3066715..2b5f64a 100644 --- a/apps/website/src/content/blog/first.md +++ b/apps/website/src/content/blog/first.md @@ -1,7 +1,7 @@ --- title: 'First post' description: 'Lorem ipsum dolor sit amet' -pubDate: 'Jul 08 2022' +publishDate: 'Jul 08 2022' heroImage: '/blog-placeholder-3.jpg' --- diff --git a/apps/website/src/content/config.ts b/apps/website/src/content/config.ts index 8d68c0e..6f6cde9 100644 --- a/apps/website/src/content/config.ts +++ b/apps/website/src/content/config.ts @@ -1,12 +1,15 @@ -import { defineCollection, z } from 'astro:content'; +import { defineCollection } from 'astro:content'; +import { z } from 'zod'; const blog = defineCollection({ schema: z.object({ title: z.string(), description: z.string(), - pubDate: z.coerce.date(), - updatedDate: z.coerce.date().optional(), - heroImage: z.string().optional(), + publishDate: z.string().or(z.date()).transform(val => new Date(val)), + updatedDate: z.string().or(z.date()).transform(val => new Date(val)).optional(), + socialImage: z.string().optional(), + coverImage: z.string().optional(), + lang: z.enum(['en']).default('en'), }), }); diff --git a/apps/website/src/data/site-info.ts b/apps/website/src/data/site-info.ts new file mode 100644 index 0000000..cb7784f --- /dev/null +++ b/apps/website/src/data/site-info.ts @@ -0,0 +1,22 @@ +export interface SiteInfo { + name: string; + title: string; + description: string; + image: { + src: string; + alt: string; + }; +}; + +// TODO: move config.ts to here so we can use astro components inside config +const siteInfo: SiteInfo = { + name: 'Polyfrost', + title: 'Polyfrost', + description: 'Building beautiful mods and tools, byte by byte', + image: { + src: '/media/polyfrost/minimal.svg', + alt: 'Polyfrost Logo', + }, +}; + +export default siteInfo; diff --git a/apps/website/src/layouts/BlogPost.astro b/apps/website/src/layouts/BlogPost.astro index 188aae1..17ff74d 100644 --- a/apps/website/src/layouts/BlogPost.astro +++ b/apps/website/src/layouts/BlogPost.astro @@ -6,18 +6,18 @@ import Layout from './Layout.astro'; type Props = CollectionEntry<'blog'>['data']; -const { title, description, pubDate, updatedDate, heroImage } = Astro.props; +const { title, description, publishDate, updatedDate, coverImage } = Astro.props; ---
- {heroImage && Hero Image} + {coverImage && Hero Image}
- + { updatedDate && (
diff --git a/apps/website/src/layouts/Layout.astro b/apps/website/src/layouts/Layout.astro index f916ee1..b1cde5c 100644 --- a/apps/website/src/layouts/Layout.astro +++ b/apps/website/src/layouts/Layout.astro @@ -1,37 +1,18 @@ --- import Footer from '@components/base/Footer.astro'; -import Favicon from '/media/polyfrost/minimal_bg.svg?url'; import Navbar from '../components/base/navbar/Navbar.astro'; +import BaseHead, { type Props as HeadProps } from '../components/shared/BaseHead.astro'; import '../styles/global.css'; -interface Props { - title?: string; - description?: string; - favicon?: string; -} +export type Props = HeadProps; -const { - title = 'Polyfrost', - description = 'Official website for Polyfrost.', - favicon = Favicon, -} = Astro.props; +const { ...head } = Astro.props; --- - - - - - - - - - - - - {title} + diff --git a/apps/website/src/pages/about.astro b/apps/website/src/pages/about.astro index ac51204..5f85a7d 100644 --- a/apps/website/src/pages/about.astro +++ b/apps/website/src/pages/about.astro @@ -7,7 +7,7 @@ import Layout from '@layouts/Layout.astro'; --- - +
Our journey
diff --git a/apps/website/src/pages/blog/index.astro b/apps/website/src/pages/blog/index.astro index 4bccad4..8603764 100644 --- a/apps/website/src/pages/blog/index.astro +++ b/apps/website/src/pages/blog/index.astro @@ -4,9 +4,7 @@ import { getCollection } from 'astro:content'; import FormattedDate from '../../components/base/FormattedDate.astro'; import Layout from '../../layouts/Layout.astro'; -const posts = (await getCollection('blog')).sort( - (a, b) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf(), -); +const posts = await getCollection('blog'); --- @@ -16,10 +14,10 @@ const posts = (await getCollection('blog')).sort( posts.map(post => (
  • - +

    {post.data.title}

    - +

  • diff --git a/apps/website/src/pages/branding.astro b/apps/website/src/pages/branding.astro index e367f3b..4b41edd 100644 --- a/apps/website/src/pages/branding.astro +++ b/apps/website/src/pages/branding.astro @@ -12,7 +12,7 @@ function formatModName(name: string): string { } --- - +
    diff --git a/apps/website/src/pages/contact.astro b/apps/website/src/pages/contact.astro index aac214d..53a6a81 100644 --- a/apps/website/src/pages/contact.astro +++ b/apps/website/src/pages/contact.astro @@ -8,7 +8,7 @@ import Layout from '@layouts/Layout.astro'; --- - +
    Feeling social? Come chat with us
    diff --git a/apps/website/src/pages/index.astro b/apps/website/src/pages/index.astro index 14e59de..c2835ac 100644 --- a/apps/website/src/pages/index.astro +++ b/apps/website/src/pages/index.astro @@ -8,7 +8,7 @@ import Icon from '@components/icons/Icon.astro'; import configConst from '@config'; import Layout from '@layouts/Layout.astro'; --- - +
    +
    diff --git a/apps/website/src/pages/legal/privacy.astro b/apps/website/src/pages/legal/privacy.astro index d3940f2..61fbdff 100644 --- a/apps/website/src/pages/legal/privacy.astro +++ b/apps/website/src/pages/legal/privacy.astro @@ -10,7 +10,7 @@ This will be updated in the future, if necessary. `.trim(); --- - +
    diff --git a/apps/website/src/pages/legal/security.astro b/apps/website/src/pages/legal/security.astro index a80d3f9..0bbf50e 100644 --- a/apps/website/src/pages/legal/security.astro +++ b/apps/website/src/pages/legal/security.astro @@ -10,7 +10,7 @@ This will be updated in the future, if necessary. `.trim(); --- - +
    diff --git a/apps/website/src/pages/legal/terms.astro b/apps/website/src/pages/legal/terms.astro index 6ed2009..e894d76 100644 --- a/apps/website/src/pages/legal/terms.astro +++ b/apps/website/src/pages/legal/terms.astro @@ -10,7 +10,7 @@ This will be updated in the future, if necessary. `.trim(); --- - +
    diff --git a/apps/website/src/pages/mods.astro b/apps/website/src/pages/mods.astro index ae1002c..adde3d7 100644 --- a/apps/website/src/pages/mods.astro +++ b/apps/website/src/pages/mods.astro @@ -14,7 +14,7 @@ const modrinthType = configConst.socials.modrinth.type; const modrinthUrl = `https://modrinth.com/${modrinthType}/${modrinthId}`; --- - +
    diff --git a/apps/website/src/pages/oss.astro b/apps/website/src/pages/oss.astro index a54d929..b7b2715 100644 --- a/apps/website/src/pages/oss.astro +++ b/apps/website/src/pages/oss.astro @@ -39,7 +39,7 @@ const rightCodeBlock = leftCodeBlock; --- - +
    diff --git a/apps/website/src/pages/projects/oneconfig/download.astro b/apps/website/src/pages/projects/oneconfig/download.astro index b8aee51..07279ba 100644 --- a/apps/website/src/pages/projects/oneconfig/download.astro +++ b/apps/website/src/pages/projects/oneconfig/download.astro @@ -7,7 +7,7 @@ import configConst from '@config'; import Layout from '@layouts/Layout.astro'; --- - +
    diff --git a/apps/website/src/pages/projects/oneconfig/index.astro b/apps/website/src/pages/projects/oneconfig/index.astro index e8283f3..ed018cd 100644 --- a/apps/website/src/pages/projects/oneconfig/index.astro +++ b/apps/website/src/pages/projects/oneconfig/index.astro @@ -13,7 +13,7 @@ import { Code } from 'astro:components'; --- - +
    diff --git a/apps/website/src/pages/rss.xml.js b/apps/website/src/pages/rss.xml.js deleted file mode 100644 index fe05755..0000000 --- a/apps/website/src/pages/rss.xml.js +++ /dev/null @@ -1,16 +0,0 @@ -import rss from '@astrojs/rss'; -import { getCollection } from 'astro:content'; - -export async function GET(context) { - const posts = await getCollection('blog'); - - return rss({ - title: 'Polyfrost Blog', - description: 'Recieve Polyfrost updates here', - site: context.site, - items: posts.map(post => ({ - ...post.data, - link: `/blog/${post.slug}/`, - })), - }); -} diff --git a/apps/website/src/pages/rss.xml.ts b/apps/website/src/pages/rss.xml.ts new file mode 100644 index 0000000..db8cfff --- /dev/null +++ b/apps/website/src/pages/rss.xml.ts @@ -0,0 +1,30 @@ +import rss from '@astrojs/rss'; +import { getCollection } from 'astro:content'; +import type { APIRoute } from 'astro'; + +function sortPosts(a: { data: { publishDate: Date } }, b: { data: { publishDate: Date } }) { + return Number(b.data.publishDate) - Number(a.data.publishDate); +} + +function formatDate(date: Date) { + date.setUTCHours(0); + return date; +} + +export const GET: APIRoute = async (context) => { + const unsortedPosts = [...(await getCollection('blog'))]; + const posts = unsortedPosts.sort((a, b) => sortPosts(a, b)); + + return rss({ + title: 'Polyfrost Blog', + description: 'Recieve Polyfrost updates here', + site: context.site!.href, + items: posts.map(post => ({ + ...post.data, + title: post.data.title, + description: post.data.description, + pubDate: formatDate(post.data.publishDate), + link: `/blog/${post.slug}/`, + })), + }); +}; diff --git a/apps/website/src/process-env.ts b/apps/website/src/process-env.ts new file mode 100644 index 0000000..5106c6c --- /dev/null +++ b/apps/website/src/process-env.ts @@ -0,0 +1,14 @@ +import process from 'node:process'; +import { z } from 'zod'; + +const schema = z.object({}); + +export function env() { + const result = schema.safeParse(process.env); + if (!result.success) { + throw new Error(`Missing environment variables: ${result.error.issues + .map(issue => issue.path.join('.')).join(', ')}`); + } + + return result.data; +} diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index 139b7e0..1f976ee 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -2,6 +2,7 @@ "extends": "astro/tsconfigs/strict", "compilerOptions": { "composite": false, + "jsx": "preserve", "baseUrl": ".", "paths": { "@components/*": ["src/components/*"], @@ -12,5 +13,6 @@ "@config": ["config.ts"], "@styles/*": ["src/styles/*"] } - } + }, + "exclude": ["node_modules", "dist", ".vercel", "public"] } -- cgit