Skip to content

Commit 0dafe43

Browse files
authored
Add site metadata, sitemap, and robots routes (#6)
## Summary - add shared site URL helper for consistent absolute metadata URLs - add site-level metadata defaults (Open Graph + Twitter) in `layout` - add blog index metadata (canonical, Open Graph, Twitter) - add per-post metadata (canonical, article Open Graph fields, Twitter) - add static metadata routes for `sitemap.xml` and `robots.txt` ## Validation - `npx -y pnpm build` - verifies static export succeeds - confirms `/robots.txt` and `/sitemap.xml` are generated
2 parents a4aa82a + a584ac5 commit 0dafe43

6 files changed

Lines changed: 115 additions & 0 deletions

File tree

src/app/blog/[slug]/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {
99
getPostBySlug,
1010
markdownToHtml,
1111
} from "@/lib/blog";
12+
import { getSiteUrl } from "@/lib/site";
13+
14+
const siteUrl = getSiteUrl();
1215

1316
interface BlogPostPageProps {
1417
params: Promise<{ slug: string }>;
@@ -32,6 +35,23 @@ export async function generateMetadata({ params }: BlogPostPageProps): Promise<M
3235
return {
3336
title: `${post.title} | Liminal HQ`,
3437
description: post.excerpt,
38+
alternates: {
39+
canonical: `/blog/${post.slug}`,
40+
},
41+
openGraph: {
42+
title: post.title,
43+
description: post.excerpt,
44+
url: `${siteUrl}/blog/${post.slug}`,
45+
siteName: "Liminal HQ",
46+
type: "article",
47+
publishedTime: `${post.date}T00:00:00.000Z`,
48+
tags: post.tags,
49+
},
50+
twitter: {
51+
card: "summary_large_image",
52+
title: post.title,
53+
description: post.excerpt,
54+
},
3555
};
3656
}
3757

src/app/blog/page.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,29 @@ import Link from "next/link";
33
import Header from "@/components/Header";
44
import Footer from "@/components/Footer";
55
import { formatPostDate, getAllPostsMeta } from "@/lib/blog";
6+
import { getSiteUrl } from "@/lib/site";
7+
8+
const siteUrl = getSiteUrl();
9+
const blogUrl = `${siteUrl}/blog`;
610

711
export const metadata: Metadata = {
812
title: "Blog | Liminal HQ",
913
description: "Technical writing and studio notes from Liminal HQ.",
14+
alternates: {
15+
canonical: "/blog",
16+
},
17+
openGraph: {
18+
title: "Blog | Liminal HQ",
19+
description: "Technical writing and studio notes from Liminal HQ.",
20+
url: blogUrl,
21+
siteName: "Liminal HQ",
22+
type: "website",
23+
},
24+
twitter: {
25+
card: "summary_large_image",
26+
title: "Blog | Liminal HQ",
27+
description: "Technical writing and studio notes from Liminal HQ.",
28+
},
1029
};
1130

1231
export default async function BlogIndexPage() {

src/app/layout.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from "next";
22
import { Inter, Space_Grotesk } from "next/font/google";
33
import BackToTopButton from "@/components/BackToTopButton";
4+
import { getSiteUrl } from "@/lib/site";
45
import "./globals.css";
56

67
const inter = Inter({
@@ -15,9 +16,24 @@ const spaceGrotesk = Space_Grotesk({
1516
display: "swap",
1617
});
1718

19+
const siteUrl = getSiteUrl();
20+
1821
export const metadata: Metadata = {
22+
metadataBase: new URL(siteUrl),
1923
title: "Liminal HQ | Independent Digital Studio",
2024
description: "Digital tools for the spaces in between. An independent studio building local-first applications.",
25+
openGraph: {
26+
title: "Liminal HQ | Independent Digital Studio",
27+
description: "Digital tools for the spaces in between. An independent studio building local-first applications.",
28+
url: siteUrl,
29+
siteName: "Liminal HQ",
30+
type: "website",
31+
},
32+
twitter: {
33+
card: "summary_large_image",
34+
title: "Liminal HQ | Independent Digital Studio",
35+
description: "Digital tools for the spaces in between. An independent studio building local-first applications.",
36+
},
2137
};
2238

2339
export default function RootLayout({

src/app/robots.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { MetadataRoute } from "next";
2+
import { getSiteUrl } from "@/lib/site";
3+
4+
export const dynamic = "force-static";
5+
6+
export default function robots(): MetadataRoute.Robots {
7+
const siteUrl = getSiteUrl();
8+
9+
return {
10+
rules: {
11+
userAgent: "*",
12+
allow: "/",
13+
},
14+
sitemap: `${siteUrl}/sitemap.xml`,
15+
host: siteUrl,
16+
};
17+
}

src/app/sitemap.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { MetadataRoute } from "next";
2+
import { getAllPostsMeta } from "@/lib/blog";
3+
import { getSiteUrl } from "@/lib/site";
4+
5+
export const dynamic = "force-static";
6+
7+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
8+
const siteUrl = getSiteUrl();
9+
const posts = await getAllPostsMeta();
10+
11+
const staticRoutes: MetadataRoute.Sitemap = [
12+
{
13+
url: `${siteUrl}/`,
14+
changeFrequency: "weekly",
15+
priority: 1,
16+
},
17+
{
18+
url: `${siteUrl}/blog`,
19+
changeFrequency: "weekly",
20+
priority: 0.8,
21+
},
22+
];
23+
24+
const postRoutes: MetadataRoute.Sitemap = posts.map((post) => ({
25+
url: `${siteUrl}/blog/${post.slug}`,
26+
lastModified: new Date(`${post.date}T00:00:00.000Z`),
27+
changeFrequency: "monthly",
28+
priority: 0.7,
29+
}));
30+
31+
return [...staticRoutes, ...postRoutes];
32+
}

src/lib/site.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const FALLBACK_SITE_URL = "https://liminalhq.ca";
2+
3+
export function getSiteUrl(): string {
4+
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? process.env.SITE_URL;
5+
6+
if (!siteUrl || siteUrl.trim().length === 0) {
7+
return FALLBACK_SITE_URL;
8+
}
9+
10+
return siteUrl.replace(/\/$/, "");
11+
}

0 commit comments

Comments
 (0)