Merge remote-tracking branch 'origin/main'

# Conflicts:
#	package-lock.json
This commit is contained in:
2025-08-19 09:10:28 -05:00
2 changed files with 190 additions and 160 deletions
+53 -26
View File
@@ -1,19 +1,21 @@
import ShareButtons from '@/components/ShareButtons'; import ShareButtons from "@/components/ShareButtons";
import { getPostData, getSortedPostsData } from '@/lib/markdown'; import { getPostData, getSortedPostsData } from "@/lib/markdown";
import { PostData, PostMetadata } from '@/types'; import { PostData, PostMetadata } from "@/types";
import { format } from 'date-fns'; import { format } from "date-fns";
import Image from 'next/image'; import Image from "next/image";
import Link from 'next/link'; import Link from "next/link";
import React from 'react'; import React from "react";
import CommentSection from "@/components/CommentSection"; import CommentSection from "@/components/CommentSection";
import { generateMetadata as createMetadata } from '@/components/Metadata'; import { generateMetadata as createMetadata } from "@/components/Metadata";
import type { Metadata } from 'next'; import type { Metadata } from "next";
interface PageProps { interface PageProps {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
} }
export async function generateMetadata({ params }: PageProps): Promise<Metadata> { export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const { id } = await params; const { id } = await params;
const post = await getPostData(id); const post = await getPostData(id);
const postUrl = `https://confessionsofgrace.com/posts/${post.id}`; const postUrl = `https://confessionsofgrace.com/posts/${post.id}`;
@@ -21,28 +23,34 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
return createMetadata({ return createMetadata({
title: post.title, title: post.title,
description: post.excerpt, description: post.excerpt,
keywords: post.tags.join(', '), keywords: post.tags.join(", "),
image: post.coverImage, image: post.coverImage,
url: postUrl, url: postUrl,
type: 'article' type: "article",
}); });
} }
async function getPostAndMorePosts(id: string): Promise<{ post: PostData; morePosts: PostMetadata[] }> { async function getPostAndMorePosts(
id: string,
): Promise<{ post: PostData; morePosts: PostMetadata[] }> {
const post = await getPostData(id); const post = await getPostData(id);
const allPosts = getSortedPostsData(); const allPosts = getSortedPostsData();
// Filter out the current post and get a few related posts (by tags) // Filter out the current post and get a few related posts (by tags)
const otherPosts = allPosts.filter(p => p.id !== post.id); const otherPosts = allPosts.filter((p) => p.id !== post.id);
// Find posts with matching tags // Find posts with matching tags
const relatedPosts = otherPosts const relatedPosts = otherPosts
.filter(p => p.tags.some(tag => post.tags.includes(tag))) .filter((p) => p.tags.some((tag) => post.tags.includes(tag)))
.slice(0, 2); .slice(0, 2);
// If we don't have enough related posts, add recent posts // If we don't have enough related posts, add recent posts
const morePosts = relatedPosts.length < 2 const morePosts =
? [...relatedPosts, ...otherPosts.filter(p => !relatedPosts.includes(p))].slice(0, 2) relatedPosts.length < 2
? [
...relatedPosts,
...otherPosts.filter((p) => !relatedPosts.includes(p)),
].slice(0, 2)
: relatedPosts; : relatedPosts;
return { post, morePosts }; return { post, morePosts };
@@ -61,7 +69,7 @@ export default async function PostPage({ params }: PageProps) {
<span>{post.author}</span> <span>{post.author}</span>
<span className="mx-2"></span> <span className="mx-2"></span>
<time dateTime={post.date}> <time dateTime={post.date}>
{format(new Date(post.date), 'MMMM d, yyyy')} {format(new Date(post.date), "MMMM d, yyyy")}
</time> </time>
</div> </div>
{post.coverImage && ( {post.coverImage && (
@@ -76,7 +84,7 @@ export default async function PostPage({ params }: PageProps) {
</div> </div>
)} )}
<div className="flex flex-wrap gap-2 mb-6"> <div className="flex flex-wrap gap-2 mb-6">
{post.tags.map(tag => ( {post.tags.map((tag) => (
<Link <Link
key={tag} key={tag}
href={`/tags/${tag}`} href={`/tags/${tag}`}
@@ -104,15 +112,21 @@ export default async function PostPage({ params }: PageProps) {
<div className="mt-12 pt-6 border-t border-primary-200"> <div className="mt-12 pt-6 border-t border-primary-200">
<h2 className="text-2xl font-bold mb-6">More Posts</h2> <h2 className="text-2xl font-bold mb-6">More Posts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{morePosts.slice(0, 2).map(post => ( {morePosts.slice(0, 2).map((post) => (
<div key={post.id} className="bg-white p-6 rounded-md border border-primary-200 shadow-sm"> <div
key={post.id}
className="bg-white p-6 rounded-md border border-primary-200 shadow-sm"
>
<h3 className="font-bold mb-2"> <h3 className="font-bold mb-2">
<Link href={`/posts/${post.id}`} className="hover:text-accent-dark"> <Link
href={`/posts/${post.id}`}
className="hover:text-accent-dark"
>
{post.title} {post.title}
</Link> </Link>
</h3> </h3>
<p className="text-primary-600 text-sm mb-2"> <p className="text-primary-600 text-sm mb-2">
{format(new Date(post.date), 'MMMM d, yyyy')} {format(new Date(post.date), "MMMM d, yyyy")}
</p> </p>
<p className="text-primary-700">{post.excerpt}</p> <p className="text-primary-700">{post.excerpt}</p>
</div> </div>
@@ -123,9 +137,22 @@ export default async function PostPage({ params }: PageProps) {
<CommentSection postId={post.id} /> <CommentSection postId={post.id} />
<div className="mt-12 pt-6 border-t border-primary-200"> <div className="mt-12 pt-6 border-t border-primary-200">
<Link href="/public" className="text-accent-dark hover:text-accent inline-flex items-center"> <Link
<svg className="mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> href="/public"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /> className="text-accent-dark hover:text-accent inline-flex items-center"
>
<svg
className="mr-2 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg> </svg>
Back to all posts Back to all posts
</Link> </Link>
+22 -19
View File
@@ -1,4 +1,4 @@
import type {Metadata} from "next"; import type { Metadata } from "next";
interface MetaProps { interface MetaProps {
title?: string; title?: string;
@@ -10,18 +10,21 @@ interface MetaProps {
} }
export function generateMetadata({ export function generateMetadata({
title = 'Confessions of Grace', title = "Confessions of Grace",
description = 'A blog dedicated to exploring the doctrines of grace and Reformed theology.', description = "A blog dedicated to exploring the doctrines of grace and Reformed theology.",
keywords = 'reformed theology, doctrines of grace, christianity, calvinism, theology', keywords = "reformed theology, doctrines of grace, christianity, calvinism, theology",
image = '/images/og-default.jpg', image = "/images/og-default.jpg",
url = 'https://confessionsofgrace.com', url = "https://confessionsofgrace.com",
type = 'website' type = "website",
}: MetaProps = {}): Metadata { }: MetaProps = {}): Metadata {
const siteTitle = title === 'Confessions of Grace' const siteTitle =
? 'Confessions of Grace | Confessing Christ. Rejoicing in Grace.' title === "Confessions of Grace"
? "Confessions of Grace | Confessing Christ. Rejoicing in Grace."
: `${title} | Confessions of Grace`; : `${title} | Confessions of Grace`;
const fullImageUrl = image.startsWith('http') ? image : `https://www.confessionsofgrace.com${image}`; const fullImageUrl = image.startsWith("http")
? image
: `https://confessionsofgrace.com${image}`;
return { return {
title: siteTitle, title: siteTitle,
@@ -35,21 +38,21 @@ export function generateMetadata({
images: [ images: [
{ {
url: fullImageUrl, url: fullImageUrl,
alt: title || 'Confessions of Grace' alt: title || "Confessions of Grace",
} },
], ],
locale: 'en_US', locale: "en_US",
siteName: 'Confessions of Grace' siteName: "Confessions of Grace",
}, },
twitter: { twitter: {
card: 'summary_large_image', card: "summary_large_image",
title: siteTitle, title: siteTitle,
description, description,
images: [fullImageUrl] images: [fullImageUrl],
}, },
alternates: { alternates: {
canonical: url canonical: url,
} },
}; };
} }