-
Your Slogan Goes Here
+
+
{config.slogan}
{/* Posts List - centered */}
diff --git a/src/app/posts/[slug]/page.tsx b/src/app/posts/[slug]/page.tsx
index 9bfb145..ce0040b 100644
--- a/src/app/posts/[slug]/page.tsx
+++ b/src/app/posts/[slug]/page.tsx
@@ -4,6 +4,7 @@ import { PostData } from '@/types';
import '@/styles/codeblock.css';
import '@/styles/postContent.css';
import 'highlight.js/styles/an-old-hope.css';
+import { getConfig } from '@/lib/getConfig';
interface PostPageProps {
params: {
@@ -12,6 +13,7 @@ interface PostPageProps {
}
export default async function PostPage({ params }: PostPageProps) {
+ const config = getConfig();
const post: PostData = await getPostData(params.slug);
return (
@@ -47,6 +49,12 @@ export default async function PostPage({ params }: PostPageProps) {
className='post-content mx-auto mt-10 w-full max-w-3xl'
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
/>
+
+ {/* Comment */}
+
);
}
diff --git a/src/app/posts/layout.tsx b/src/app/posts/layout.tsx
new file mode 100644
index 0000000..ec99378
--- /dev/null
+++ b/src/app/posts/layout.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import type { Metadata } from 'next';
+import { getConfig } from '@/lib/getConfig';
+
+const config = getConfig();
+
+export const metadata: Metadata = {
+ title: `Posts - ${config.title}`,
+ description: `Posts page of ${config.title} - ${config.description}`,
+ openGraph: {
+ title: `Posts - ${config.title}`,
+ description: `Posts page of ${config.title} - ${config.description}`,
+ type: 'website',
+ locale: config.lang,
+ },
+};
+
+export default function PostsLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return <>{children}>;
+}
diff --git a/src/components/common/Footer.tsx b/src/components/common/Footer.tsx
index d8ba2f2..f1c6040 100644
--- a/src/components/common/Footer.tsx
+++ b/src/components/common/Footer.tsx
@@ -1,25 +1,53 @@
-import { FaGithub, FaLinkedinIn, FaInstagram } from 'react-icons/fa6';
+import {
+ FaGithub,
+ FaLinkedinIn,
+ FaInstagram,
+ FaXTwitter,
+ FaBilibili,
+ FaYoutube,
+ FaRss,
+ FaTelegram,
+} from 'react-icons/fa6';
+import { SiZhihu } from 'react-icons/si';
+import { MdEmail } from 'react-icons/md';
+import { getConfig } from '@/lib/getConfig';
+import Link from 'next/link';
export default function Footer() {
+ const config = getConfig();
const currentYear = new Date().getFullYear();
- const contactList = [
- {
- href: 'https://www.instagram.com/zl_asica/',
- title: 'Instagram',
- icon:
,
- },
- {
- href: 'https://github.com/ZL-Asica',
- title: 'GitHub',
- icon:
,
- },
- {
- href: 'https://linkedin.com/in/elara-liu',
- title: 'LinkedIn',
- icon:
,
- },
- ];
+ const socialIcons = {
+ instagram: FaInstagram,
+ github: FaGithub,
+ x: FaXTwitter,
+ linkedin: FaLinkedinIn,
+ youtube: FaYoutube,
+ telegram: FaTelegram,
+ bilibili: FaBilibili,
+ zhihu: SiZhihu,
+ email: MdEmail,
+ rss: FaRss,
+ };
+
+ const contactList = Object.entries(config.socialMedia)
+ .map(([key, href]) => {
+ if (!href) return null;
+ const IconComponent = socialIcons[key];
+ const title = key.charAt(0).toUpperCase() + key.slice(1); // Capitalize key for title
+ const contactHref = key === 'email' ? `mailto:${href}` : href;
+ return {
+ href: contactHref,
+ title,
+ icon:
,
+ };
+ })
+ .filter(
+ (
+ contact,
+ ): contact is { href: string; title: string; icon: JSX.Element } =>
+ contact !== null,
+ ); // Asserts that contact is not null
return (
);
}
diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx
index 38b2161..a0dc0cc 100644
--- a/src/components/common/Header.tsx
+++ b/src/components/common/Header.tsx
@@ -10,7 +10,7 @@ import {
} from 'react-icons/fa6';
import { useState, useRef, useEffect } from 'react';
-export default function Header() {
+export default function Header({ siteTitle }: { siteTitle: string }) {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef
(null);
@@ -58,7 +58,9 @@ export default function Header() {
>
- Suzu Blog
+
+ {siteTitle}
+
{/* Hamburger Menu Button */}
{
+ const filePath = path.join(process.cwd(), 'config.yml');
+ const fileContents = fs.readFileSync(filePath, 'utf8');
+ const config = yaml.parse(fileContents) as Config;
+ return config;
+};
diff --git a/src/lib/posts.ts b/src/lib/posts.ts
index 6b2db37..c56f83b 100644
--- a/src/lib/posts.ts
+++ b/src/lib/posts.ts
@@ -2,6 +2,7 @@ import { promises as fsPromises } from 'fs';
import path from 'path';
import { parseMarkdown } from './markdown';
import { PostData, Frontmatter } from '@/types';
+import { getConfig } from '@/lib/getConfig';
const postsDirectory = path.join(process.cwd(), 'src/posts');
@@ -26,6 +27,8 @@ function formatDateTime(dateTime: string): string {
// Get all posts data
export async function getAllPosts(): Promise {
+ const config = getConfig();
+
const fileNames = await fsPromises.readdir(postsDirectory);
const allPosts = await Promise.all(
@@ -41,7 +44,8 @@ export async function getAllPosts(): Promise {
// Limit title and author length to prevent overflow
frontmatter.title =
frontmatter.title?.slice(0, 100) || fileName.replace(/\.md$/, '');
- frontmatter.author = frontmatter.author?.slice(0, 30) || 'ZL Asica';
+ frontmatter.author =
+ frontmatter.author?.slice(0, 30) || config.author.name;
// TODO: Add default thumbnail
From d7887ada314a48df3e70dd89f48e6d3c710cfa67 Mon Sep 17 00:00:00 2001
From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com>
Date: Sat, 19 Oct 2024 22:14:16 -0500
Subject: [PATCH 5/6] fix: eslint errors.
---
src/app/layout.tsx | 1 +
src/components/common/Footer.tsx | 8 ++++----
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 3ddfeef..a69c658 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import type { Metadata } from 'next';
+// eslint-disable-next-line camelcase
import { Roboto, Noto_Sans_SC } from 'next/font/google';
import './globals.css';
import ThemeProvider from '@/components/ThemeProvider';
diff --git a/src/components/common/Footer.tsx b/src/components/common/Footer.tsx
index f1c6040..54b0bbd 100644
--- a/src/components/common/Footer.tsx
+++ b/src/components/common/Footer.tsx
@@ -32,7 +32,9 @@ export default function Footer() {
const contactList = Object.entries(config.socialMedia)
.map(([key, href]) => {
- if (!href) return null;
+ if (!href) {
+ return null;
+ }
const IconComponent = socialIcons[key];
const title = key.charAt(0).toUpperCase() + key.slice(1); // Capitalize key for title
const contactHref = key === 'email' ? `mailto:${href}` : href;
@@ -43,9 +45,7 @@ export default function Footer() {
};
})
.filter(
- (
- contact,
- ): contact is { href: string; title: string; icon: JSX.Element } =>
+ (contact): contact is { href: string; title: string; icon } =>
contact !== null,
); // Asserts that contact is not null
From 0f84e2aef92eb2ba13ca0a69d753f3d7dc184196 Mon Sep 17 00:00:00 2001
From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com>
Date: Sat, 19 Oct 2024 22:31:14 -0500
Subject: [PATCH 6/6] feat: add logic to avoid thumbnail not exist.
- Merge frontmatter process logic into 1 single function.
- Add logic avoid parsing not md files.
---
config.yml | 2 +-
src/lib/posts.ts | 114 ++++++++++++++++++++++++++-----------
src/posts/sample-post-2.md | 1 -
src/posts/sample-post.md | 2 +-
4 files changed, 83 insertions(+), 36 deletions(-)
diff --git a/config.yml b/config.yml
index 6a48dc1..3a8d4fe 100644
--- a/config.yml
+++ b/config.yml
@@ -26,7 +26,7 @@ socialMedia:
bilibili: 'https://space.bilibili.com/29018759'
zhihu: 'https://www.zhihu.com/people/zl-asica'
email: 'zl@zla.app' # Only put your email address here, no mailto: prefix
- rss: '/feed.xml'
+ rss: ''
# Set your own js script inside
scriptSlotHeader:
diff --git a/src/lib/posts.ts b/src/lib/posts.ts
index c56f83b..fd67ff2 100644
--- a/src/lib/posts.ts
+++ b/src/lib/posts.ts
@@ -6,67 +6,107 @@ import { getConfig } from '@/lib/getConfig';
const postsDirectory = path.join(process.cwd(), 'src/posts');
+// Helper function to check if the file is a Markdown file
+function isMarkdownFile(fileName: string): boolean {
+ return fileName.endsWith('.md');
+}
+
// Helper function to validate and format date (yyyy-mm-dd hh:mm:ss)
function formatDateTime(dateTime: string): string {
const [date, time] = dateTime.includes(' ')
? dateTime.split(' ')
: [dateTime, ''];
- // Validate date, if invalid return empty string (used to trigger fallback)
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(date)) {
return '';
}
- // Validate time, default to '00:00:00' if missing or invalid
const timeRegex = /^\d{2}:\d{2}:\d{2}$/;
const formattedTime = timeRegex.test(time) ? time : '00:00:00';
return `${date} ${formattedTime}`;
}
+// Helper function to check if the file exists (only for local paths)
+async function fileExists(filePath: string): Promise {
+ try {
+ await fsPromises.access(filePath);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+// Helper function to process frontmatter
+async function processFrontmatter(
+ frontmatter: Frontmatter,
+ fileName: string,
+ fullPath: string,
+ config: ReturnType,
+): Promise {
+ // Limit title and author length
+ frontmatter.title =
+ frontmatter.title?.slice(0, 100) || fileName.replace(/\.md$/, '');
+ frontmatter.author = frontmatter.author?.slice(0, 30) || config.author.name;
+
+ // Handle thumbnail, check if it's a local file and if it exists
+ if (frontmatter.thumbnail && !frontmatter.thumbnail.includes('://')) {
+ const thumbnailPath = path.join(
+ process.cwd(),
+ 'public',
+ frontmatter.thumbnail,
+ );
+ const exists = await fileExists(thumbnailPath);
+ if (!exists) {
+ // Fallback to config background if file does not exist
+ frontmatter.thumbnail = config.background;
+ }
+ } else {
+ frontmatter.thumbnail = frontmatter.thumbnail || config.background;
+ }
+
+ // Format date, fallback to last modified date if invalid
+ const formattedDate = frontmatter.date
+ ? formatDateTime(frontmatter.date)
+ : '';
+ if (!formattedDate) {
+ const stats = await fsPromises.stat(fullPath);
+ const lastModifiedDate = stats.mtime.toISOString().split('T');
+ // yyyy-mm-dd hh:mm:ss
+ frontmatter.date = `${lastModifiedDate[0]} ${lastModifiedDate[1].split('.')[0]}`;
+ } else {
+ frontmatter.date = formattedDate;
+ }
+
+ return frontmatter;
+}
+
// Get all posts data
export async function getAllPosts(): Promise {
const config = getConfig();
-
const fileNames = await fsPromises.readdir(postsDirectory);
+ // Filter to only include .md files
+ const markdownFiles = fileNames.filter(isMarkdownFile);
+
const allPosts = await Promise.all(
- fileNames.map(async (fileName) => {
+ markdownFiles.map(async (fileName) => {
const fullPath = path.join(postsDirectory, fileName);
const fileContents = await fsPromises.readFile(fullPath, 'utf8');
- // Asynchronously parse markdown file
const { frontmatter, contentHtml } = await parseMarkdown(fileContents);
- // Ensure mandatory fields have default values
-
- // Limit title and author length to prevent overflow
- frontmatter.title =
- frontmatter.title?.slice(0, 100) || fileName.replace(/\.md$/, '');
- frontmatter.author =
- frontmatter.author?.slice(0, 30) || config.author.name;
-
- // TODO: Add default thumbnail
-
- // Format date, if date is invalid or missing, use last modified date as fallback
- const formattedDate = frontmatter.date
- ? formatDateTime(frontmatter.date)
- : '';
-
- if (!formattedDate) {
- const stats = await fsPromises.stat(fullPath);
- const lastModifiedDate = stats.mtime.toISOString().split('T');
- const fallbackDate = `${lastModifiedDate[0]} ${lastModifiedDate[1].split('.')[0]}`; // yyyy-mm-dd hh:mm:ss
- frontmatter.date = fallbackDate;
- } else {
- frontmatter.date = formattedDate;
- }
+ const processedFrontmatter = await processFrontmatter(
+ frontmatter as Frontmatter,
+ fileName,
+ fullPath,
+ config,
+ );
return {
- slug: fileName.replace(/\.md$/, ''), // Use file name as slug (without .md)
- // Use assert to tell TypeScript that frontmatter is Frontmatter type
- frontmatter: frontmatter as Frontmatter,
+ slug: fileName.replace(/\.md$/, ''),
+ frontmatter: processedFrontmatter,
contentHtml,
};
}),
@@ -76,7 +116,7 @@ export async function getAllPosts(): Promise {
allPosts.sort((a, b) => {
const dateA = new Date(a.frontmatter.date);
const dateB = new Date(b.frontmatter.date);
- return dateB.getTime() - dateA.getTime(); // Sort descending (newest first)
+ return dateB.getTime() - dateA.getTime();
});
return allPosts;
@@ -84,14 +124,22 @@ export async function getAllPosts(): Promise {
// Get single post data
export async function getPostData(slug: string): Promise {
+ const config = getConfig();
const fullPath = path.join(postsDirectory, `${slug}.md`);
const fileContents = await fsPromises.readFile(fullPath, 'utf8');
const { frontmatter, contentHtml } = await parseMarkdown(fileContents);
+ const processedFrontmatter = await processFrontmatter(
+ frontmatter as Frontmatter,
+ `${slug}.md`,
+ fullPath,
+ config,
+ );
+
return {
slug,
- frontmatter,
+ frontmatter: processedFrontmatter,
contentHtml,
};
}
diff --git a/src/posts/sample-post-2.md b/src/posts/sample-post-2.md
index 5270d38..68dd53f 100644
--- a/src/posts/sample-post-2.md
+++ b/src/posts/sample-post-2.md
@@ -1,7 +1,6 @@
---
title: '我的第二篇文章'
date: '2024-10-01 00:00:00'
-thumbnail: '/images/katomegumi.jpg'
categories:
- 全栈
- 教程
diff --git a/src/posts/sample-post.md b/src/posts/sample-post.md
index fb094fe..e64f160 100644
--- a/src/posts/sample-post.md
+++ b/src/posts/sample-post.md
@@ -1,7 +1,7 @@
---
title: '我的第一篇文章'
date: '2024-01-01 20:10:00'
-thumbnail: '/images/katomegumi.jpg'
+thumbnail: '/images/background.jpg'
categories:
- 前端开发
- 教程