diff --git a/.eslintrc.json b/.eslintrc.json
index 9818bfa..ba6bd61 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,6 +1,10 @@
{
- "extends": ["next/core-web-vitals", "next/typescript", "prettier"],
- "plugins": ["import"],
+ "extends": [
+ "next/core-web-vitals",
+ "next/typescript",
+ "prettier",
+ "plugin:jsx-a11y/recommended"
+ ],
"rules": {
// Possible Errors
"no-console": "warn", // Warn on console usage (can change to 'error' to disallow)
@@ -33,22 +37,6 @@
"arrow-spacing": ["error", { "before": true, "after": true }], // Enforce spacing around arrows
"prefer-const": "error", // Prefer const for variables that are never reassigned
"no-var": "error", // Disallow var (use let or const instead)
- "template-curly-spacing": ["error", "never"], // Disallow spaces inside template literals
-
- // Import
- "import/order": [
- "error",
- {
- "groups": [
- "builtin",
- "external",
- "internal",
- "parent",
- "sibling",
- "index"
- ],
- "newlines-between": "always"
- }
- ]
+ "template-curly-spacing": ["error", "never"] // Disallow spaces inside template literals
}
}
diff --git a/.gitignore b/.gitignore
index fd3dbb5..abee180 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+# User generated files
+public/postsData.json
diff --git a/config.yml b/config.yml
index f9ad224..4d043aa 100644
--- a/config.yml
+++ b/config.yml
@@ -10,6 +10,8 @@ author:
link: 'https://www.zla.app' # Link to your personal website or blog.
# Language setting: Use ISO 639-1 code (e.g., 'en' for English, 'zh' for Chinese)
lang: 'zh'
+# Your public root url, used for SEO and meta tags. (Do not include a ending slash)
+siteUrl: 'https://suzu.zla.app'
# Path to your avatar image. Can be a relative path from /public or a full URL (e.g., https://).
avatar: '/images/avatar.jpg'
@@ -71,14 +73,6 @@ disqusShortname: 'zla-pub'
# CUSTOM CODE BLOCKS
# ! WARNING: Only modify these if you understand the purpose of custom scripts.
#######################
-# Add JavaScript URLs or code snippets to be included inside
on your site.
-scriptSlotHeader:
- -
-
-# Add JavaScript URLs or code snippets to be included before the closing tag.
-scriptSlotFooter:
- - 'https://cdn.jsdelivr.net/gh/zl-asica/web-cdn/js/zlasica.js'
-
# Add custom HTML code to be included inside the section of your site.
slotFooter: |
diff --git a/eslint.config.mjs b/eslint.config.mjs
deleted file mode 100644
index f3761e9..0000000
--- a/eslint.config.mjs
+++ /dev/null
@@ -1,73 +0,0 @@
-import { FlatCompat } from '@eslint/eslintrc';
-import js from '@eslint/js';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-const compat = new FlatCompat({
- baseDirectory: __dirname,
- recommendedConfig: js.configs.recommended,
- allConfig: js.configs.all,
-});
-
-export default [
- ...compat.extends('next/core-web-vitals', 'next/typescript', 'prettier'),
- {
- rules: {
- 'no-console': 'warn',
- 'no-debugger': 'error',
- 'no-extra-semi': 'error',
- 'no-irregular-whitespace': 'error',
- curly: 'error',
- eqeqeq: ['error', 'always'],
- 'no-eval': 'error',
- 'no-implied-eval': 'error',
- 'no-throw-literal': 'error',
- 'no-unused-expressions': 'error',
- 'no-undef': 'error',
-
- 'no-unused-vars': [
- 'warn',
- {
- vars: 'all',
- args: 'none',
- },
- ],
-
- camelcase: [
- 'error',
- {
- properties: 'never',
- },
- ],
-
- 'comma-dangle': ['error', 'always-multiline'],
-
- quotes: [
- 'error',
- 'single',
- {
- avoidEscape: true,
- },
- ],
-
- semi: ['error', 'always'],
- indent: ['error', 2],
- 'eol-last': ['error', 'always'],
- 'no-trailing-spaces': 'error',
-
- 'arrow-spacing': [
- 'error',
- {
- before: true,
- after: true,
- },
- ],
-
- 'prefer-const': 'error',
- 'no-var': 'error',
- 'template-curly-spacing': ['error', 'never'],
- },
- },
-];
diff --git a/public/custom.js b/public/custom.js
new file mode 100644
index 0000000..52b9764
--- /dev/null
+++ b/public/custom.js
@@ -0,0 +1,48 @@
+'use client';
+
+// Get the current date
+function getCurrentDate() {
+ const date = new Date();
+ const formatNumber = (num) => String(num).padStart(2, '0');
+
+ const year = date.getFullYear();
+ const month = formatNumber(date.getMonth() + 1);
+ const day = formatNumber(date.getDate());
+ const hours = formatNumber(date.getHours());
+ const minutes = formatNumber(date.getMinutes());
+ const seconds = formatNumber(date.getSeconds());
+
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+}
+
+// Gray scale filter (only for /) on specific dates
+function grayScale() {
+ // Set the date you want to gray scale
+ const grayScaleDates = ['04-04', '05-12', '09-18', '11-20', '12-13'];
+
+ // Get the current date
+ const currentDate = getCurrentDate();
+ const currentMonthDay = currentDate
+ .split(' ')[0]
+ .split('-')
+ .slice(1)
+ .join('-');
+
+ // Check if the current date is in the gray scale date list
+ if (grayScaleDates.includes(currentMonthDay)) {
+ // If is, set the gray scale filter
+ document.body.style.filter = 'grayscale(100%)';
+ }
+}
+
+// Custom console log
+// eslint-disable-next-line no-console
+console.info(
+ '%c由ZL Asica制作搭建与运行\nBuilt and Operated by ZL Asica\nhttps://www.zla.app',
+ 'background:#fff;color:#000',
+);
+
+// Check if the current URL path is "/"
+if (window.location.pathname === '/') {
+ grayScale();
+}
diff --git a/src/app/about/layout.tsx b/src/app/about/layout.tsx
deleted file mode 100644
index a46dae0..0000000
--- a/src/app/about/layout.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getConfig } from '@/services/config/getConfig';
-import type { Metadata } from 'next';
-import React from 'react';
-
-const config = getConfig();
-
-export const metadata: Metadata = {
- title: `About - ${config.title}`,
- description: `About page of ${config.title} - ${config.description}`,
- openGraph: {
- title: `About - ${config.title}`,
- description: `About page of ${config.title} - ${config.description}`,
- type: 'website',
- locale: config.lang,
- },
-};
-
-export default function AboutLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return <>{children}>;
-}
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index 18623d7..08c27fb 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -1,11 +1,36 @@
+import Loading from '@/app/loading';
import PostLayout from '@/components/layout/PostLayout';
import { getConfig } from '@/services/config/getConfig';
import { getPostData } from '@/services/content/posts';
import { PostData } from '@/types';
+import type { Metadata } from 'next';
+import { Suspense } from 'react';
+
+export async function generateMetadata(): Promise {
+ const config = getConfig();
+ return {
+ title: `About - ${config.title}`,
+ description: `About page of ${config.title} - ${config.description}`,
+ openGraph: {
+ siteName: config.title,
+ type: 'profile',
+ username: config.author.name,
+ title: `About - ${config.title}`,
+ description: `About page of ${config.title} - ${config.description}`,
+ images: config.avatar,
+ url: '/about',
+ locale: config.lang,
+ },
+ };
+}
export default async function AboutPage() {
const post: PostData = await getPostData('About', 'About');
const config = getConfig();
- return ;
+ return (
+ }>
+
+
+ );
}
diff --git a/src/app/categories/[categorySlug]/page.tsx b/src/app/categories/[categorySlug]/page.tsx
index 75fce8f..d3f098a 100644
--- a/src/app/categories/[categorySlug]/page.tsx
+++ b/src/app/categories/[categorySlug]/page.tsx
@@ -1,7 +1,10 @@
+import Loading from '@/app/loading';
import PostListLayout from '@/components/layout/PostListLayout';
import { getConfig } from '@/services/config/getConfig';
import { getAllPosts } from '@/services/content/posts';
+import { Metadata } from 'next';
import { notFound } from 'next/navigation';
+import { Suspense } from 'react';
export async function generateStaticParams() {
const config = getConfig();
@@ -10,6 +13,34 @@ export async function generateStaticParams() {
}));
}
+type Props = {
+ params: Promise<{ categorySlug: string }>;
+};
+
+export async function generateMetadata({ params }: Props): Promise {
+ // read post slug
+ const category = (await params).categorySlug;
+
+ const config = getConfig();
+
+ // Find the category based on the slug from params
+ const categoryData = config.postCategories.find(
+ (cat) => cat.slug === category,
+ ) || { name: 'Not Found' };
+ return {
+ title: `分类:${categoryData.name} - ${config.title}`,
+ openGraph: {
+ siteName: config.title,
+ title: `分类:${categoryData.name} - ${config.title}`,
+ description: `分类:${categoryData.name} - ${config.description}`,
+ url: `/categories/${category}`,
+ images: config.avatar,
+ type: 'website',
+ locale: config.lang,
+ },
+ };
+}
+
export default async function CategoryPage(props: {
params: Promise<{ categorySlug: string }>;
}) {
@@ -35,8 +66,9 @@ export default async function CategoryPage(props: {
return (
{category.name}
-
-
+
}>
+
+
);
}
diff --git a/src/app/friends/layout.tsx b/src/app/friends/layout.tsx
deleted file mode 100644
index 67161ac..0000000
--- a/src/app/friends/layout.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getConfig } from '@/services/config/getConfig';
-import type { Metadata } from 'next';
-import React from 'react';
-
-const config = getConfig();
-
-export const metadata: Metadata = {
- title: `Friends - ${config.title}`,
- description: `Friends page of ${config.title} - ${config.description}`,
- openGraph: {
- title: `Friends - ${config.title}`,
- description: `Friends page of ${config.title} - ${config.description}`,
- type: 'website',
- locale: config.lang,
- },
-};
-
-export default function FriendsLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return <>{children}>;
-}
diff --git a/src/app/friends/page.tsx b/src/app/friends/page.tsx
index 740c6a6..cbaa84d 100644
--- a/src/app/friends/page.tsx
+++ b/src/app/friends/page.tsx
@@ -1,12 +1,36 @@
+import Loading from '@/app/loading';
import PostLayout from '@/components/layout/PostLayout';
import { getConfig } from '@/services/config/getConfig';
import { getPostData } from '@/services/content/posts';
import '@/styles/friendsLinks.css';
import { PostData } from '@/types';
+import type { Metadata } from 'next';
+import { Suspense } from 'react';
+
+export async function generateMetadata(): Promise {
+ const config = getConfig();
+ return {
+ title: `Friends - ${config.title}`,
+ description: `Friends page of ${config.title} - ${config.description}`,
+ openGraph: {
+ siteName: config.title,
+ title: `Friends - ${config.title}`,
+ description: `Friends page of ${config.title} - ${config.description}`,
+ url: '/friends',
+ images: config.avatar,
+ type: 'website',
+ locale: config.lang,
+ },
+ };
+}
export default async function FriendsPage() {
const post: PostData = await getPostData('Friends', 'Friends');
const config = getConfig();
- return ;
+ return (
+ }>
+
+
+ );
}
diff --git a/src/app/globals.css b/src/app/globals.css
index d596303..ff02b62 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -82,17 +82,6 @@ h4 {
margin-block-end: 1.33em;
}
-/* code {
- background: #1d1f21;
- color: #fff;
- word-break: break-word;
- font-family: 'Source Code Pro', monospace, Helvetica, Tahoma, Arial, STXihei,
- 'STHeiti Light', 'Microsoft YaHei', sans-serif;
- padding: 2px;
- text-shadow: none;
- border-radius: 0 0 5px 5px;
-} */
-
/****************************************************
* IMAGES & MEDIA
****************************************************/
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index bc38b44..79000b3 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -26,15 +26,19 @@ const notoSansSC = Noto_Sans_SC({
});
export const metadata: Metadata = {
+ metadataBase: new URL(config.siteUrl),
title: `${config.title} - ${config.subTitle}`,
description: config.description,
keywords: config.keywords,
authors: [{ url: config.author.link, name: config.author.name }],
openGraph: {
+ siteName: `${config.title} - ${config.subTitle}`,
title: `${config.title} - ${config.subTitle}`,
+ images: config.avatar,
description: config.description,
type: 'website',
locale: config.lang,
+ url: config.siteUrl,
},
};
@@ -47,9 +51,7 @@ export default function RootLayout({
return (
- {config.scriptSlotHeader?.map((scriptUrl, index) => (
-
- ))}
+
{children}
-
- {config.scriptSlotFooter?.map((scriptUrl, index) => (
-
- ))}
);
diff --git a/src/app/loading.tsx b/src/app/loading.tsx
new file mode 100644
index 0000000..dfee82b
--- /dev/null
+++ b/src/app/loading.tsx
@@ -0,0 +1,7 @@
+export default function Loading() {
+ return (
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 2df9540..905c023 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,9 +1,14 @@
+import Loading from '@/app/loading';
+import PostListLayout from '@/components/layout/PostListLayout';
import { getConfig } from '@/services/config/getConfig';
+import { getAllPosts } from '@/services/content/posts';
+import { PostData } from '@/types';
import Image from 'next/image';
-import PostsPage from './posts/page';
+import { Suspense } from 'react';
-export default function Home() {
+export default async function Home() {
const config = getConfig();
+ const posts: PostData[] = await getAllPosts();
return (
@@ -20,18 +25,21 @@ export default function Home() {
width={150}
height={150}
className='aspect-square h-[60vw] max-h-[150px] w-[60vw] max-w-[150px] rounded-full border-4 border-gray-300 object-cover md:h-[150px] md:w-[150px]'
+ priority={true}
/>
{/* Slogan */}
-
{config.slogan}
+
{config.slogan}
{/* Posts List - centered */}
);
diff --git a/src/app/posts/[slug]/page.tsx b/src/app/posts/[slug]/page.tsx
index 5d97fbc..76da41d 100644
--- a/src/app/posts/[slug]/page.tsx
+++ b/src/app/posts/[slug]/page.tsx
@@ -1,6 +1,10 @@
+import Loading from '@/app/loading';
import PostLayout from '@/components/layout/PostLayout';
+import { getConfig } from '@/services/config/getConfig';
import { getAllPosts, getPostData } from '@/services/content/posts';
import { PostData } from '@/types';
+import type { Metadata } from 'next';
+import { Suspense } from 'react';
// build static params for all posts
export async function generateStaticParams() {
@@ -10,6 +14,49 @@ export async function generateStaticParams() {
}));
}
+type Props = {
+ params: Promise<{ slug: string }>;
+};
+
+export async function generateMetadata({ params }: Props): Promise {
+ // read post slug
+ const post = (await params).slug;
+
+ // get post data
+ const postData = await getPostData(post);
+
+ // thumbnail image
+ const thumbnail = postData.frontmatter.thumbnail;
+
+ const config = getConfig();
+
+ const metaKeywords =
+ postData.frontmatter.tags?.join(', ') +
+ ', ' +
+ postData.frontmatter.categories?.join(', ') +
+ ', ' +
+ postData.frontmatter.author +
+ ', ' +
+ 'blog';
+ return {
+ title: `${postData.frontmatter.title} - ${config.title}`,
+ description: postData.postAbstract,
+ keywords: metaKeywords,
+ openGraph: {
+ siteName: config.title,
+ type: 'article',
+ authors: postData.frontmatter.author,
+ tags: metaKeywords,
+ modifiedTime: postData.frontmatter.date,
+ title: postData.frontmatter.title,
+ description: postData.postAbstract,
+ images: thumbnail,
+ url: `/posts/${post}`,
+ locale: config.lang,
+ },
+ };
+}
+
// PostPage component that receives the params directly
export default async function PostPage(props: {
params: Promise<{ slug: string }>;
@@ -17,5 +64,9 @@ export default async function PostPage(props: {
const params = await props.params;
const post: PostData = await getPostData(params.slug);
- return ;
+ return (
+ }>
+
+
+ );
}
diff --git a/src/app/posts/layout.tsx b/src/app/posts/layout.tsx
deleted file mode 100644
index 8d8543f..0000000
--- a/src/app/posts/layout.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getConfig } from '@/services/config/getConfig';
-import type { Metadata } from 'next';
-import React from 'react';
-
-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/app/posts/page.tsx b/src/app/posts/page.tsx
index d085b1d..ca3113a 100644
--- a/src/app/posts/page.tsx
+++ b/src/app/posts/page.tsx
@@ -1,15 +1,35 @@
+import Loading from '@/app/loading';
import PostListLayout from '@/components/layout/PostListLayout';
+import { getConfig } from '@/services/config/getConfig';
import { getAllPosts } from '@/services/content/posts';
import { PostData } from '@/types';
+import { Metadata } from 'next/types';
import { Suspense } from 'react';
+export async function generateMetadata(): Promise {
+ const config = getConfig();
+ return {
+ title: `Posts - ${config.title}`,
+ description: `Posts page of ${config.title} - ${config.description}`,
+ openGraph: {
+ siteName: config.title,
+ title: `Posts - ${config.title}`,
+ description: `Posts page of ${config.title} - ${config.description}`,
+ url: `${config.siteUrl}/posts`,
+ images: config.avatar,
+ type: 'website',
+ locale: config.lang,
+ },
+ };
+}
+
export default async function PostsPage() {
const posts: PostData[] = await getAllPosts();
return (
All Posts
-
Loading...}>
+ }>
diff --git a/src/app/robots.ts b/src/app/robots.ts
index f406345..7e42ce4 100644
--- a/src/app/robots.ts
+++ b/src/app/robots.ts
@@ -1,12 +1,33 @@
+import { getConfig } from '@/services/config/getConfig';
+import fs from 'fs';
import { MetadataRoute } from 'next';
+import path from 'path';
export default function robots(): MetadataRoute.Robots {
+ const config = getConfig();
+ const siteUrl = config.siteUrl;
+
+ // Load posts data from JSON file
+ const filePath = path.join(process.cwd(), 'public', 'postsData.json');
+ const posts = JSON.parse(fs.readFileSync(filePath, 'utf8'));
+
+ // Generate robots.txt entries for each post
+ const postUrls = posts.map((post) => `/posts/${post.slug}`);
+
return {
rules: {
userAgent: '*',
// allow: '/',
- disallow: '/',
+ disallow: [
+ '/',
+ '/about',
+ '/friends',
+ '/posts',
+ '/categories/*',
+ '/tags/*',
+ ...postUrls, // Dynamic post URLs
+ ],
},
- // sitemap: '',
+ sitemap: `${siteUrl}/sitemap.xml`,
};
}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..38cb549
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,49 @@
+import { getConfig } from '@/services/config/getConfig';
+import fs from 'fs';
+import type { MetadataRoute } from 'next';
+import path from 'path';
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ const config = getConfig();
+ const siteUrl = config.siteUrl;
+
+ // Load posts data from JSON file
+ const filePath = path.join(process.cwd(), 'public', 'postsData.json');
+ const posts = JSON.parse(fs.readFileSync(filePath, 'utf8'));
+
+ // Generate sitemap entries for each post
+ const postUrls = posts.map((post) => ({
+ url: `${siteUrl}/posts/${post.slug}`,
+ lastModified: post.lastModified || new Date(),
+ changeFrequency: 'weekly',
+ priority: 0.6,
+ }));
+
+ return [
+ {
+ url: siteUrl,
+ lastModified: new Date(),
+ changeFrequency: 'yearly',
+ priority: 1,
+ },
+ {
+ url: `${siteUrl}/posts`,
+ lastModified: new Date(),
+ changeFrequency: 'weekly',
+ priority: 0.8,
+ },
+ {
+ url: `${siteUrl}/about`,
+ lastModified: new Date(),
+ changeFrequency: 'monthly',
+ priority: 0.8,
+ },
+ {
+ url: `${siteUrl}/friends`,
+ lastModified: new Date(),
+ changeFrequency: 'monthly',
+ priority: 0.8,
+ },
+ ...postUrls, // Dynamic post URLs
+ ];
+}
diff --git a/src/app/tags/[tagSlug]/page.tsx b/src/app/tags/[tagSlug]/page.tsx
index f8239db..ebeee57 100644
--- a/src/app/tags/[tagSlug]/page.tsx
+++ b/src/app/tags/[tagSlug]/page.tsx
@@ -1,7 +1,11 @@
+import Loading from '@/app/loading';
import PostListLayout from '@/components/layout/PostListLayout';
+import { getConfig } from '@/services/config/getConfig';
import { getAllPosts } from '@/services/content/posts';
import { convertToPinyin, getUniqueTags } from '@/services/parsing/tagLinks';
import { notFound } from 'next/navigation';
+import { Metadata } from 'next/types';
+import { Suspense } from 'react';
// Generate static paths for all unique tags
export async function generateStaticParams() {
@@ -12,6 +16,33 @@ export async function generateStaticParams() {
}));
}
+type Props = {
+ params: Promise<{ tagSlug: string }>;
+};
+
+export async function generateMetadata({ params }: Props): Promise {
+ // read post slug
+ const tag = (await params).tagSlug;
+
+ const config = getConfig();
+
+ // Find the tag based on the slug from params
+ const uniqueTags = await getUniqueTags();
+ const tagData =
+ uniqueTags.find((t) => convertToPinyin(t) === tag) || 'Not Found';
+ return {
+ title: `标签:${tagData} - ${config.title}`,
+ openGraph: {
+ siteName: config.title,
+ title: `标签:${tagData} - ${config.title}`,
+ url: `${config.siteUrl}/tags/${tag}`,
+ images: config.avatar,
+ type: 'website',
+ locale: config.lang,
+ },
+ };
+}
+
export default async function TagPage(props: {
params: Promise<{ tagSlug: string }>;
}) {
@@ -37,8 +68,9 @@ export default async function TagPage(props: {
return (
);
}
diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx
index 2eb2846..4bef960 100644
--- a/src/components/common/Header.tsx
+++ b/src/components/common/Header.tsx
@@ -4,6 +4,7 @@ import renderMenuItems from '@/components/common/MenuItems';
import '@/styles/header.css';
import { Category } from '@/types';
import Link from 'next/link';
+import { usePathname } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { FaAngleUp, FaBars } from 'react-icons/fa6';
@@ -17,6 +18,8 @@ export default function Header({
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false); // Detect if in mobile view
const menuRef = useRef(null);
+ const pathname = usePathname();
+ const isHomePage = pathname === '/'; // Check if current page is home page
const toggleMenu = () => {
setIsOpen(!isOpen);
@@ -52,8 +55,12 @@ export default function Header({
}}
>
-
- {siteTitle}
+
+ {isHomePage ? (
+ {siteTitle}
+ ) : (
+ {siteTitle}
+ )}
{/* Display only in mobile */}
diff --git a/src/components/layout/PostLayout.tsx b/src/components/layout/PostLayout.tsx
index 6698f59..e5744aa 100644
--- a/src/components/layout/PostLayout.tsx
+++ b/src/components/layout/PostLayout.tsx
@@ -55,11 +55,15 @@ export default function PostLayout({
/* If no thumbnail, display the title at the top */
{post.frontmatter.title}
-
- {post.frontmatter.author}
- •
- {post.frontmatter.date.split(' ')[0]}
-
+
+ {(post.frontmatter.title === 'About' ||
+ post.frontmatter.title === 'Friends') ?? (
+
+ {post.frontmatter.author}
+ •
+ {post.frontmatter.date.split(' ')[0]}
+
+ )}
)}
diff --git a/src/components/layout/PostListLayout.tsx b/src/components/layout/PostListLayout.tsx
index 7b7cd0a..485705a 100644
--- a/src/components/layout/PostListLayout.tsx
+++ b/src/components/layout/PostListLayout.tsx
@@ -11,19 +11,8 @@ export default function PostListLayout({ posts }: { posts: PostData[] }) {
return (
{posts.map((post, index) => {
- const plainText = post.contentHtml
- .replace(//g, '[[MORE_PLACEHOLDER]]')
- .replace(/<[^>]*>/g, '');
-
- const moreIndex = plainText.indexOf('[[MORE_PLACEHOLDER]]');
-
- const postAbstract =
- moreIndex > 0
- ? plainText.slice(0, moreIndex).replace('[[MORE_PLACEHOLDER]]', '')
- : plainText.slice(0, 150);
-
return (
-
{/* Thumbnail */}
{post.frontmatter.thumbnail && (
)}
@@ -59,24 +50,22 @@ export default function PostListLayout({ posts }: { posts: PostData[] }) {
{/* Abstract */}
-
{postAbstract}
+
{post.postAbstract}
-
-
- {/* Read Count */}
-
-
- {readCount} 热度
-
- {/* Category */}
-
-
-
-
-
+
+ {/* Read Count */}
+
+
+ {readCount} 热度
+
+ {/* Category */}
+
+
+
+
-
+
);
})}
diff --git a/src/services/content/posts.ts b/src/services/content/posts.ts
index b2b3924..7e6d27c 100644
--- a/src/services/content/posts.ts
+++ b/src/services/content/posts.ts
@@ -1,6 +1,6 @@
import { getConfig } from '@/services/config/getConfig';
import { Frontmatter, PostData } from '@/types';
-import { promises as fsPromises } from 'fs';
+import fs, { promises as fsPromises } from 'fs';
import path from 'path';
import { parseMarkdown } from '../parsing/markdown';
@@ -95,7 +95,8 @@ export async function getAllPosts(): Promise {
const fullPath = path.join(postsDirectory, fileName);
const fileContents = await fsPromises.readFile(fullPath, 'utf8');
- const { frontmatter, contentHtml } = await parseMarkdown(fileContents);
+ const { frontmatter, postAbstract, contentHtml } =
+ await parseMarkdown(fileContents);
const processedFrontmatter = await processFrontmatter(
frontmatter as Frontmatter,
@@ -107,6 +108,7 @@ export async function getAllPosts(): Promise {
return {
slug: fileName.replace(/\.md$/, ''),
frontmatter: processedFrontmatter,
+ postAbstract,
contentHtml,
};
}),
@@ -119,6 +121,10 @@ export async function getAllPosts(): Promise {
return dateB.getTime() - dateA.getTime();
});
+ // Save posts to a JSON file for sitemap generation
+ const filePath = path.join(process.cwd(), 'public', 'postsData.json');
+ fs.writeFileSync(filePath, JSON.stringify(allPosts, null, 2), 'utf8');
+
return allPosts;
}
@@ -136,7 +142,8 @@ export async function getPostData(
const fileContents = await fsPromises.readFile(fullPath, 'utf8');
- const { frontmatter, contentHtml } = await parseMarkdown(fileContents);
+ const { frontmatter, postAbstract, contentHtml } =
+ await parseMarkdown(fileContents);
const processedFrontmatter = await processFrontmatter(
frontmatter as Frontmatter,
@@ -147,6 +154,7 @@ export async function getPostData(
return {
slug,
+ postAbstract,
frontmatter: processedFrontmatter,
contentHtml,
};
diff --git a/src/services/parsing/friendsLinks.ts b/src/services/parsing/friendsLinks.ts
index f2e8d5a..4a86aec 100644
--- a/src/services/parsing/friendsLinks.ts
+++ b/src/services/parsing/friendsLinks.ts
@@ -12,11 +12,11 @@ export const renderLinks = (jsonString: string) => {
img?: string;
des?: string;
}) => `
-
+
-
+
-
${link.title || ''}
+
${link.title || ''}
@@ -27,7 +27,7 @@ export const renderLinks = (jsonString: string) => {
// Wrap the list of links in a with a class and an
return `
diff --git a/src/services/parsing/markdown.ts b/src/services/parsing/markdown.ts
index 1859eee..4f7e99a 100644
--- a/src/services/parsing/markdown.ts
+++ b/src/services/parsing/markdown.ts
@@ -4,9 +4,28 @@ import matter from 'gray-matter';
import hljs from 'highlight.js';
import { marked, Tokens } from 'marked';
-export async function parseMarkdown(
- content: string,
-): Promise<{ frontmatter: Frontmatter; contentHtml: string }> {
+function processPostAbstract(contentHtml: string): string {
+ // Remove HTML tags and comments (before more if exist)
+ const plainText = contentHtml
+ .replace(//g, '[[MORE_PLACEHOLDER]]')
+ .replace(/<[^>]*>/g, '');
+
+ const moreIndex = plainText.indexOf('[[MORE_PLACEHOLDER]]');
+
+ const contentSliced =
+ moreIndex > 0
+ ? plainText.slice(0, moreIndex).replace('[[MORE_PLACEHOLDER]]', '')
+ : plainText.slice(0, 150);
+
+ // remove newlines and extra spaces
+ return contentSliced.replace(/\s+/g, ' ').trim();
+}
+
+export async function parseMarkdown(content: string): Promise<{
+ frontmatter: Frontmatter;
+ postAbstract: string;
+ contentHtml: string;
+}> {
const { data: frontmatterData, content: markdownContent } = matter(content);
// Replace all comments but keep
@@ -71,8 +90,11 @@ export async function parseMarkdown(
// Process the sanitized content through marked
const processedContent = await marked(contentSanitized);
+ const postAbstract = processPostAbstract(processedContent);
+
return {
frontmatter: frontmatterData as Frontmatter,
+ postAbstract: postAbstract,
contentHtml: processedContent,
};
}
diff --git a/src/styles/friendsLinks.css b/src/styles/friendsLinks.css
index 9c6fbee..834627e 100644
--- a/src/styles/friendsLinks.css
+++ b/src/styles/friendsLinks.css
@@ -58,6 +58,18 @@
); /* Add a background color in case the image doesn't load or is small */
}
+/* Style for each card name (title) */
+.friends-links
+ > .friends-links-list
+ > .friend-link-item
+ > .friend-link-content
+ > .friend-link-title {
+ font-size: 1.2em;
+ margin: 1.2em 0;
+ line-height: 1.4;
+ position: relative;
+}
+
/* Insert description dynamically when hovering (using data-description attribute) */
.friends-links > .friends-links-list > .friend-link-item:hover::after {
content: attr(data-description);
diff --git a/src/styles/postContent.css b/src/styles/postContent.css
index 13f2aff..28203f9 100644
--- a/src/styles/postContent.css
+++ b/src/styles/postContent.css
@@ -54,6 +54,12 @@
color: var(--darkForeground);
}
+ p {
+ margin: 1.2em 0;
+ line-height: 1.8;
+ font-size: 1.1em;
+ }
+
/* Blockquote */
blockquote {
padding: 0.5em 1em;
@@ -115,6 +121,7 @@
color: var(--skyBlue);
text-decoration: underline;
transition: color 0.2s ease;
+ margin: 0 0.2em;
}
a:link {
diff --git a/src/types.d.ts b/src/types.d.ts
index aef55e9..062008c 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -13,6 +13,7 @@ export interface Frontmatter {
// Post data
export interface PostData {
slug: string;
+ postAbstract: string;
frontmatter: Frontmatter;
contentHtml: string;
}
@@ -35,6 +36,7 @@ export type Config = {
link: string;
};
lang: string;
+ siteUrl: string;
avatar: string;
background: string;
slogan: string;
@@ -54,8 +56,6 @@ export type Config = {
thumbnailAbout: boolean;
thumbnailFriends: boolean;
disqusShortname: string;
- scriptSlotHeader: string[];
- scriptSlotFooter: string[];
slotFooter: string;
slotComment: string;
};