Archived
:)
This commit is contained in:
+3
-1
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {Suspense} from 'react';
|
||||
import { getSortedPostsData } from '@/lib/markdown';
|
||||
import { PostMetadata } from '@/types';
|
||||
import { generateMetadata as createMetadata } from '@/components/Metadata';
|
||||
@@ -24,7 +24,9 @@ export default async function SearchPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-6">Search Results</h1>
|
||||
<Suspense fallback={<div>Loading search...</div>}>
|
||||
<SearchClient allPosts={allPosts} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+17
-7
@@ -10,38 +10,48 @@ interface PageProps {
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const posts = getSortedPostsData();
|
||||
const tags = Array.from(new Set(posts.flatMap(post => post.tags)));
|
||||
|
||||
return tags.map((tag) => ({
|
||||
tag: tag,
|
||||
return tags
|
||||
.filter(tag => tag && tag.trim() !== '') // Filter out empty or undefined tags
|
||||
.map((tag) => ({
|
||||
tag: encodeURIComponent(tag),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error generating static params for tags:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { tag } = await params;
|
||||
const decodedTag = decodeURIComponent(tag);
|
||||
return createMetadata({
|
||||
title: `Posts tagged "${tag}"`,
|
||||
description: `Browse all posts tagged with "${tag}" on Confessions of Grace.`,
|
||||
title: `Posts tagged "${decodedTag}"`,
|
||||
description: `Browse all posts tagged with "${decodedTag}" on Confessions of Grace.`,
|
||||
url: `https://confessionsofgrace.com/tags/${tag}`,
|
||||
type: 'website'
|
||||
});
|
||||
}
|
||||
|
||||
async function getPostsByTagData(tag: string): Promise<PostMetadata[]> {
|
||||
return getPostsByTag(tag);
|
||||
const decodedTag = decodeURIComponent(tag);
|
||||
return getPostsByTag(decodedTag);
|
||||
}
|
||||
|
||||
export default async function TagPage({ params }: PageProps) {
|
||||
const { tag } = await params;
|
||||
const decodedTag = decodeURIComponent(tag);
|
||||
const posts = await getPostsByTagData(tag);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-4">Tag: {tag}</h1>
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-4">Tag: {decodedTag}</h1>
|
||||
<p className="text-lg text-primary-600">
|
||||
{posts.length} {posts.length === 1 ? 'post' : 'posts'} tagged with "{tag}"
|
||||
{posts.length} {posts.length === 1 ? 'post' : 'posts'} tagged with "{decodedTag}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
+53
-122
@@ -1,148 +1,79 @@
|
||||
|
||||
import ShareButtons from '@/components/ShareButtons';
|
||||
import { getPostData, getSortedPostsData, getAllPostIds } from '@/lib/markdown';
|
||||
import { PostData, PostMetadata } from '@/types';
|
||||
import { format } from 'date-fns';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import CommentSection from "@/components/CommentSection";
|
||||
import Link from 'next/link';
|
||||
import { getSortedPostsData } from '@/lib/markdown';
|
||||
import { generateMetadata as createMetadata } from '@/components/Metadata';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return createMetadata({
|
||||
title: 'Tags',
|
||||
description: 'Browse all tags used on Confessions of Grace to find posts by topic.',
|
||||
url: 'https://confessionsofgrace.com/tags',
|
||||
type: 'website'
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
async function getAllTags(): Promise<{ tag: string; count: number }[]> {
|
||||
try {
|
||||
const posts = getSortedPostsData();
|
||||
return posts.map((post) => ({
|
||||
id: post.id,
|
||||
}));
|
||||
const tagCount: { [key: string]: number } = {};
|
||||
|
||||
// Count occurrences of each tag
|
||||
posts.forEach(post => {
|
||||
post.tags.forEach(tag => {
|
||||
if (tag && tag.trim() !== '') {
|
||||
tagCount[tag] = (tagCount[tag] || 0) + 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Convert to array and sort by count (descending) then by name
|
||||
return Object.entries(tagCount)
|
||||
.map(([tag, count]) => ({ tag, count }))
|
||||
.sort((a, b) => {
|
||||
if (b.count !== a.count) return b.count - a.count;
|
||||
return a.tag.localeCompare(b.tag);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to generate post paths during build:', error);
|
||||
console.error('Error getting tags:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
const post = await getPostData(id);
|
||||
const postUrl = `https://confessionsofgrace.com/posts/${post.id}`;
|
||||
|
||||
return createMetadata({
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
keywords: post.tags.join(', '),
|
||||
image: post.coverImage,
|
||||
url: postUrl,
|
||||
type: 'article'
|
||||
});
|
||||
}
|
||||
|
||||
async function getPostAndMorePosts(id: string): Promise<{ post: PostData; morePosts: PostMetadata[] }> {
|
||||
const post = await getPostData(id);
|
||||
const allPosts = getSortedPostsData();
|
||||
|
||||
// Filter out the current post and get a few related posts (by tags)
|
||||
const otherPosts = allPosts.filter(p => p.id !== post.id);
|
||||
|
||||
// Find posts with matching tags
|
||||
const relatedPosts = otherPosts
|
||||
.filter(p => p.tags.some(tag => post.tags.includes(tag)))
|
||||
.slice(0, 2);
|
||||
|
||||
// If we don't have enough related posts, add recent posts
|
||||
const morePosts = relatedPosts.length < 2
|
||||
? [...relatedPosts, ...otherPosts.filter(p => !relatedPosts.includes(p))].slice(0, 2)
|
||||
: relatedPosts;
|
||||
|
||||
return { post, morePosts };
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
const { post, morePosts } = await getPostAndMorePosts(id);
|
||||
const postUrl = `https://confessionsofgrace.com/posts/${post.id}`;
|
||||
export default async function TagsPage() {
|
||||
const tags = await getAllTags();
|
||||
|
||||
return (
|
||||
<article className="max-w-3xl mx-auto">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-2">{post.title}</h1>
|
||||
<div className="flex items-center text-primary-500 mb-6">
|
||||
<span>{post.author}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<time dateTime={post.date}>
|
||||
{format(new Date(post.date), 'MMMM d, yyyy')}
|
||||
</time>
|
||||
</div>
|
||||
{post.coverImage && (
|
||||
<div className="relative h-64 md:h-96 w-full mb-8 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={post.coverImage}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{post.tags.map(tag => (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-6">Browse by Tag</h1>
|
||||
|
||||
<p className="text-lg text-primary-700 mb-8">
|
||||
Explore posts organized by topic. Click on any tag to see all posts with that tag.
|
||||
</p>
|
||||
|
||||
{tags.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{tags.map(({ tag, count }) => (
|
||||
<Link
|
||||
key={tag}
|
||||
href={`/tags/${tag}`}
|
||||
className="text-sm bg-primary-100 text-primary-600 px-3 py-1 rounded-md hover:bg-primary-200"
|
||||
href={`/tags/${encodeURIComponent(tag)}`}
|
||||
className="bg-white border border-primary-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
{tag}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-primary-700">{tag}</span>
|
||||
<span className="bg-primary-100 text-primary-600 px-2 py-1 rounded-full text-sm">
|
||||
{count} {count === 1 ? 'post' : 'posts'}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className="blog-post prose prose-lg max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-primary-200">
|
||||
<ShareButtons
|
||||
url={postUrl}
|
||||
title={post.title}
|
||||
description={post.excerpt}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-xl text-primary-500">No tags found.</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-6 border-t border-primary-200">
|
||||
<h2 className="text-2xl font-bold mb-6">More Posts</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{morePosts.slice(0, 2).map(post => (
|
||||
<div key={post.id} className="bg-white p-6 rounded-md border border-primary-200 shadow-sm">
|
||||
<h3 className="font-bold mb-2">
|
||||
<Link href={`/posts/${post.id}`} className="hover:text-accent-dark">
|
||||
{post.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-primary-600 text-sm mb-2">
|
||||
{format(new Date(post.date), 'MMMM d, yyyy')}
|
||||
</p>
|
||||
<p className="text-primary-700">{post.excerpt}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommentSection postId={post.id} />
|
||||
|
||||
<div className="mt-12 pt-6 border-t border-primary-200">
|
||||
<Link href="/posts" 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>
|
||||
Back to all posts
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
+48
-5
@@ -1,3 +1,4 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
@@ -11,12 +12,21 @@ const postsDirectory = path.join(process.cwd(), 'data/posts');
|
||||
export function getSortedPostsData(): PostMetadata[] {
|
||||
// Ensure we only run this on the server
|
||||
if (typeof window === 'undefined') {
|
||||
try {
|
||||
// Get file names under /posts
|
||||
const fileNames = fs.readdirSync(postsDirectory);
|
||||
const allPostsData = fileNames.map((fileName) => {
|
||||
const allPostsData = fileNames
|
||||
.filter(fileName => fileName.endsWith('.md')) // Only process .md files
|
||||
.map((fileName) => {
|
||||
// Remove ".md" from file name to get id
|
||||
const id = fileName.replace(/\.md$/, '');
|
||||
|
||||
// Skip if id is empty or undefined
|
||||
if (!id || id === 'undefined') {
|
||||
console.warn(`Skipping invalid filename: ${fileName}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read markdown file as string
|
||||
const fullPath = path.join(postsDirectory, fileName);
|
||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||
@@ -34,7 +44,8 @@ export function getSortedPostsData(): PostMetadata[] {
|
||||
tags: matterResult.data.tags || [],
|
||||
coverImage: matterResult.data.coverImage || undefined,
|
||||
} as PostMetadata;
|
||||
});
|
||||
})
|
||||
.filter((post): post is PostMetadata => post !== null); // Filter out null values
|
||||
|
||||
// Sort posts by date
|
||||
return allPostsData.sort((a, b) => {
|
||||
@@ -44,6 +55,10 @@ export function getSortedPostsData(): PostMetadata[] {
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error reading posts directory:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty array if running on client side
|
||||
@@ -53,15 +68,27 @@ export function getSortedPostsData(): PostMetadata[] {
|
||||
export function getAllPostIds() {
|
||||
// Ensure we only run this on the server
|
||||
if (typeof window === 'undefined') {
|
||||
try {
|
||||
const fileNames = fs.readdirSync(postsDirectory);
|
||||
|
||||
return fileNames.map((fileName) => {
|
||||
return fileNames
|
||||
.filter(fileName => fileName.endsWith('.md'))
|
||||
.map((fileName) => {
|
||||
const id = fileName.replace(/\.md$/, '');
|
||||
if (!id || id === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
params: {
|
||||
id: fileName.replace(/\.md$/, ''),
|
||||
id,
|
||||
},
|
||||
};
|
||||
});
|
||||
})
|
||||
.filter((item): item is { params: { id: string } } => item !== null);
|
||||
} catch (error) {
|
||||
console.error('Error reading posts directory:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty array if running on client side
|
||||
@@ -71,7 +98,19 @@ export function getAllPostIds() {
|
||||
export async function getPostData(id: string): Promise<PostData> {
|
||||
// Ensure we only run this on the server
|
||||
if (typeof window === 'undefined') {
|
||||
try {
|
||||
// Validate id
|
||||
if (!id || id === 'undefined') {
|
||||
throw new Error(`Invalid post id: ${id}`);
|
||||
}
|
||||
|
||||
const fullPath = path.join(postsDirectory, `${id}.md`);
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
throw new Error(`Post file not found: ${fullPath}`);
|
||||
}
|
||||
|
||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
// Use gray-matter to parse the post metadata section
|
||||
@@ -94,6 +133,10 @@ export async function getPostData(id: string): Promise<PostData> {
|
||||
tags: matterResult.data.tags || [],
|
||||
coverImage: matterResult.data.coverImage || undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error loading post ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty object if running on client side (should never happen in practice)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
||||
|
||||
export default defineCloudflareConfig();
|
||||
Generated
+12595
-15
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -6,9 +6,13 @@
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opennextjs/cloudflare": "^1.6.5",
|
||||
"@supabase/supabase-js": "^2.55.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -28,6 +32,7 @@
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"wrangler": "^4.31.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "confessions-of-grace"
|
||||
main = ".open-next/worker.js"
|
||||
compatibility_date = "2025-03-25"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
[assets]
|
||||
directory = ".open-next/assets"
|
||||
binding = "ASSETS"
|
||||
Reference in New Issue
Block a user