H1Todo-list
-
性能优化
-
作者卡片新增 Hover 描述后显示我的标签,用弹幕形式
-
给 Author 页面加入动画(视差动画,依靠滑动触发)
-
Moment、love、bucket list 超过一定数量就套一个渐变透明玻璃外壳,或者一个按钮,点击查看更多(看能否找到 React 版瀑布流 JS,可控制很重要,而不是每次刷新才能加载)
-
图片灯箱
-
侧栏组件
- 网站指标(运行时间,文章字数,微博粉丝,B 站粉丝数)
-
评论区(用 section tag)
-
Stats
-
友链
-
TOC
-
自动给文章加一个 abbrlink
-
文章主题色优化,比如基于主题色生成深色、浅色
-
Navbar 上加一个页面跳转加载条。
H1搭建点滴
H2渲染 Markdown
我使用了 MDX,理由是对 JSX、TSX 友好,且内置很多丰富功能,the
remark
andrehype
ecosystem contains plugins for syntax highlighting, linking headings, generating a table of contents, and more.Video: NextJS 13 教程:从 Markdown 文件创建静态博客 (看完对如何获取本地 MD 数据且进行一系列处理有帮助)
Doc: Copy to Clipboard Button In MDX with Next.js and Rehype Pretty Code (显示代码语言)
Note:
- MDX 文件里可以写 HTML 元素,其中元素的 style 属性是按照 JSX 里的格式。For example,
style={{marginRight: spacing + 'em'}}
. - remark plugins work with markdown and rehype plugins work with HTML.
H3Configure
tsx
<MDXRemote
source={postContent?.content}
components={{
h1: ({ children }) => (
<h1 id={children}>
<Link href={`#${children}`} className="header">
H1
</Link>
{children}
</h1>
),
img: ({ src, alt }) => {
return (
<>
<img src={src} alt={alt} />
{alt && <figcaption>{alt}</figcaption>}
</>
);
},
p: ({ children, ...props }) => {
if (children) {
const ParaComponent =
children?.type?.name === "img" ? "figure" : "p";
return (
<ParaComponent {...props}>{children}</ParaComponent>
);
}
return null;
},
pre: ({ children, ...props }: any) => {
const lang = props["data-language"];
return (
<details open {...props}>
<summary>{lang}</summary>
{children}
</details>
);
},
}}
options={{
...
}}
/>
H3Syntax highlighting
吐槽:花了我好多时间,因为目前是 Next.js 推崇使用
./app
的时期,很多技术文档没来得及更新,在网上搜解决方案的帖子也寥寥无几,我真就是不放弃一直钻研写得没那么详细的文档。场景:我使用了 next-mdx-remote 作为 Markdown file 解析工具,语法高亮使用的是 rehype-pretty-code, 主要它俩如何正确配置成了问题,自己摸索出来了。
-
先将
next.config.js
替换成next.config.mjs
js
import fs from "node:fs"; import nextMDX from "@next/mdx"; import rehypePrettyCode from "rehype-pretty-code"; const options = { // See Options section below. }; const withMDX = nextMDX({ extension: /\.mdx?$/, options: { rehypePlugins: [[rehypePrettyCode, options]], }, }); /** @type {import('next').NextConfig} */ const nextConfig = { // Configure `pageExtensions` to include MDX files pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], }; export default withMDX(nextConfig);
-
来到使用
<MDXRemote/>
的文件下进行一些配置,比如我这是/app/posts/[abbrlink]/page.tsx
tsx
import rehypePrettyCode from "rehype-pretty-code"; const options = { // See Options section below. }; <MDXRemote source={postContent?.content} components={{ ... }} options={{ mdxOptions: { rehypePlugins: [ [ rehypePrettyCode, { keepBackground: false, defaultLang: "plaintext", theme: { dark: "github-dark", light: "github-light", }, }, ], ], format: "mdx", }, }} />
重点看 options 的配置,这么配置后,语法高亮能正常使用,其余关于语法高亮的配置在
options
object 里设置。
next.config.mjs
里的配置是给xxx.mdx
文件使用的。
H3Autolink literals, Footnotes, Strikethrough, Tables, Tasklists
有一些 Markdown 功能 MDXRemote 没有,remark-gfm 就弥补了。
H2音乐播放器——Aplayer
Office Website: Aplayer
还有网易云自带外链播放器
H2Extract dominant color from images
H3method 1
Drawback: I find there is no version for TSX.
Plugin: rgbaster.js.
js
function getThemeColor(img) {
if (!img) {
document.styleSheets[0].deleteRule(0);
const batteryImg = document.querySelector(".battery");
if (batteryImg) {
batteryImg.src =
"https://cdn.jsdelivr.net/npm/hassan-assets/img/status-bar-dark.svg";
}
} else {
RGBaster.colors(img, {
paletteSize: 30,
exclude: ["rgb(255,255,255)", "rgb(0,0,0)", "rgb(254,254,254)"],
success: function (payload) {
const c = payload.dominant.match(/\d+/g);
const lightColor = `rgba(${c[0]},${c[1]},${c[2]},0.4)`;
const grayLevel = c[0] * 0.299 + c[1] * 0.587 + c[2] * 0.114;
if (grayLevel >= 180) {
// 若为浅色
document.styleSheets[0].insertRule(
`
:root {
--color-theme: ${payload.dominant} !important;
--color-theme-font: var(--color-font-black) !important;
--color-theme-light: ${lightColor} !important;
--color-theme-bright: ${payload.dominant} !important;
--color-theme-grey: rgba(0,0,0,.6) !important;
--color-author-box: rgba(0,0,0,.1) !important;
}`
);
document.querySelector(".battery").src =
"https://cdn.jsdelivr.net/npm/hassan-assets/img/status-bar-dark.svg";
} else {
// 若为深色
document.styleSheets[0].insertRule(
`:root {
--color-theme: ${payload.dominant} !important;
--color-theme-font: var(--color-font-white) !important;
--color-theme-light: ${lightColor} !important;
--color-theme-bright: ${payload.dominant} !important;
--color-theme-grey: rgba(255,255,255,.7) !important;
--color-author-box: rgba(255,255,255,.2) !important;
}`
);
document.querySelector(".battery").src =
"https://cdn.jsdelivr.net/npm/hassan-assets/img/status-bar-light.svg";
}
},
error: function () {
document.styleSheets[0].insertRule(
`:root {
--color-theme: var(--color-theme);
}`
);
},
});
}
}
H3method 2
Plugin: colorthief - neutrixs
tsx
"use client";
import { useEffect, useState } from "react";
import ColorThief from "@neutrixs/colorthief";
import { usePathname } from "next/navigation";
const getBrightColor = (r: number, g: number, b: number) => {
const luminance =
0.2126 * (r / 255) + 0.7152 * (g / 255) + 0.0722 * (b / 255);
const factor = luminance <= 0.5 ? Math.round(-200 * luminance + 100) : 0;
r = Math.min(255, r + factor);
g = Math.min(255, g + factor);
b = Math.min(255, b + factor);
return `rgb(${r},${g},${b})`;
};
// Prevents colors from being too light or dark
const handleColor = (r: number, g: number, b: number) => {
if (r >= 250 && g >= 250 && b >= 250) {
return `rgb(${r - 15},${g - 15},${b - 15})`;
} else if (r <= 11 && g <= 11 && b <= 11) {
return `rgb(${r + 30},${g + 30},${b + 30})`;
} else {
return `rgb(${r},${g},${b})`;
}
};
const resetTheme = () => {
document.styleSheets[0].insertRule(
`
:root {
--color-theme: #81d8d0;
--color-theme1: #aad7d2;
--color-theme2: #6bb0a9;
--color-theme3: #b266bc;
--color-theme4: #b989bf;
--color-theme5: #9957a1;
--color-theme-light: #aeeee7;
--color-theme-bright: #71dfd4;
--color-theme-font: var(--color-font-black);
--color-theme-header: #81d8d0;
}`
);
};
const setTheme = (cover: HTMLImageElement) => {
document.styleSheets[0].deleteRule(0);
const colorThief = new ColorThief();
const palette = colorThief.getPalette(cover, 6);
const [r, g, b] = palette[0];
const headerColor = `rgb(${r},${g},${b})`;
const lightColor = `rgba(${r},${g},${b},0.4)`;
const brightColor = getBrightColor(r, g, b);
const greyLevel = r * 0.299 + g * 0.587 + b * 0.114;
const colors = palette.map((color) =>
handleColor(color[0], color[1], color[2])
);
document.styleSheets[0].insertRule(
`
:root {
--color-theme: ${colors[0]};
--color-theme1: ${colors[1]};
--color-theme2: ${colors[2]};
--color-theme3: ${colors[3]};
--color-theme4: ${colors[4]};
--color-theme5: ${colors[5]};
--color-theme-font: ${
greyLevel >= 170 ? "var(--color-font-black)" : "var(--color-font-white)"
};
--color-theme-light: ${lightColor};
--color-theme-bright: ${brightColor};
--color-theme-header: ${headerColor};
}`
);
};
const ThemeColor = () => {
const pathname = usePathname();
const crtPage = pathname.startsWith("/posts/")
? "post"
: pathname.startsWith("/love")
? "love"
: "page";
const [prevPage, setPrevPage] = useState(crtPage);
const [isResetTheme, setIsResetTheme] = useState(false);
if (prevPage !== crtPage) {
if (prevPage !== "page" && crtPage === "page") {
// 从其它到page页面
setIsResetTheme(true);
} else if (prevPage === "page" && crtPage !== "page") {
// 从page到其它页面
setIsResetTheme(false);
}
setPrevPage(crtPage);
}
useEffect(() => {
resetTheme();
}, []);
useEffect(() => {
if (crtPage === "post") {
const cover = document.querySelector(".cover") as HTMLImageElement;
if (cover.complete) {
setTheme(cover);
} else {
cover.addEventListener("load", function () {
setTheme(cover);
});
}
} else if (crtPage === "love") {
document.styleSheets[0].deleteRule(0);
document.styleSheets[0].insertRule(
`
:root {
--color-theme: #e9dbf0;
--color-theme-light: #e9dbf0;
--color-theme-bright: #d8aeed;
--color-theme-font: var(--color-font-black);
}`
);
} else if (isResetTheme) {
document.styleSheets[0].deleteRule(0);
resetTheme();
}
}, [pathname]);
return null;
};
export default ThemeColor;
H2Smooth scroll
Plugin: React Lenis
global.scss
scss
html.lenis {
height: auto;
}
.lenis.lenis-smooth {
scroll-behavior: auto;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: hidden;
}
.lenis.lenis-scrolling iframe {
pointer-events: none;
}
H3Background scrolls at different paces (deprecated)
tsx
const bg = document.querySelector(".background") as any;
const lenis = useLenis(({ scroll }) => {
bg["style"].backgroundPositionY = `-${scroll / 2}px`;
});
H2Pickup Lines Card Widget
API: https://rizzapi-doc.vercel.app/
H1疑难杂症
H2作者卡片-时间显示延迟
- 像这种需要在客户端加载的组件要在代码顶部加上
'use client'
。 getCrtTime()
记得最开始要执行一次,否则会时钟会在你指定间隔时间后显示。
tsx
'use client'
{...}
useEffect(() => {
getCrtTime();
const interval = setInterval(() => {
getCrtTime();
}, 1000); // 每秒更新
return () => {
clearInterval(interval); // 清除定时器以防内存泄漏
};
}, [])
H2Object 储存 Icon 值并应用到页面
第一种情况:
Object.ts
tsx
interface Button {
icon: IconType;
...
}
...
social: [
{
icon: RiGithubLine,
link: "https://github.com/harrisblog/",
},
]
Custom.tsx
tsx
{
config.aside.author.social.map((item) => (
<Link href={item.link} key={item.link}>
{<item.icon />}
</Link>
));
}
记住啦,{<item.icon />}
长这样。
第二种情况:
tsx
const Button = ({ icon: Icon }: Button) => <Icon />;
H2瀑布流布局
场景:我一直不知如何在 Next.js 使用 JS 外链。
tsx
<>
<div className="grid">
<div className="item">Solid Snake</div>
<div className="item">Riou</div>
<div className="item">Jack Russel</div>
</div>
<Script src="https://cdnjs.cloudflare.com/ajax/libs/waterfall.js/1.0.2/waterfall.min.js" />
<Script strategy="lazyOnload">waterfall('.grid');</Script>
</>
H3CSS 设置宽度
scss
@mixin setListWidth($col, $mediaQueries) {
width: calc((100% - (#{$col} - 1) * var(--gap)) / #{$col});
@each $query in $mediaQueries {
$col: $col - 1;
@media (max-width: $query) {
width: calc((100% - (#{$col - 1}) * var(--gap)) / #{$col});
}
}
}
H3若应用在 React 里
jsx
useEffect(() => {
// Ensure Waterfall.js is loaded before using it
const script = document.createElement("script");
script.src =
"https://cdnjs.cloudflare.com/ajax/libs/waterfall.js/1.0.2/waterfall.min.js";
script.onload = () => {
// Ensure the function is available before calling it
if (window.waterfall) {
window.waterfall(".container");
}
};
document.body.appendChild(script);
// Clean up script after component unmounts
return () => {
document.body.removeChild(script);
};
}, []);
H2Swiper 轮播效果
场景:使用官网说的 Swiper React Components 时会报错,只需申明是 CSR 即可,我理解的是因为这个组件需要 user action,比如用户可以手动点击、滑动等操作。
在 tsx file 顶部加一个 "use client"
即可。
H2Switch Display Mode
状态管理使用的是 React 自带的 Context。Redux 在
Next.js
下会报错、加载速度极慢,且我这是小型项目,无需 fancy features ,故不推荐。
Partial code snippet:
layout.tsx
tsx
import { ThemeProvider } from "@/context/ThemeProvider";
return (
<html lang="en">
<ThemeProvider>
<Navbar />
{children}
<Footer />
</ThemeProvider>
</html>
);
ThemeContext.tsx
(重点)
tsx
"use client";
import { createContext, useState } from "react";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
const ThemeContext = createContext({
mode: "light",
switchMode: () => {},
});
const ThemeProvider = ({ children }: any) => {
const theme = !window.matchMedia
? "light"
: window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
const [mode, setMode] = useState(theme);
const switchMode = () => {
setMode((prev) => (prev == "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ mode, switchMode }}>
<body className={inter.className} data-theme={mode}>
{children}
</body>
</ThemeContext.Provider>
);
};
export { ThemeProvider, ThemeContext };
可以看到
body
被我从layout.tsx
移过来了,其实原计划直接在layout.tsx
里用useContext
获取 mode 值并运用到body
上,但奈何layout.tsx
是 server side,标注"use client"
的话会出现额外问题,所以我灵机一动直接将body
写到prodiver
里。
xxx.module.scss
scss
[data-theme="dark"] {...}
H2Pagination for Home page
Video: My New Favorite Pagination Method - Josh tried coding
Unlike this video, the way I implement pagination by using params
not searchParams
. That means posts on different pages can be accessed by visiting https://example.com/[page]
.
Code snippet:
/app/[page]/page.tsx
tsx
const page = ({ params }: Params) => {
const page_index = params["page"] ?? "1";
const per_page = config.page.per_page;
const start = (Number(page_index) - 1) * per_page;
const end = start + per_page;
return {
<PostCardsList start={start} end={end} />
}
}
PostCardsList.tsx
tsx
const PostCardsList = ({
start = 0,
end = config.page.per_page,
}: PostCardsList) => {
const sortedPostsData = getPostsSortedByCreateDate().slice(start, end);
if (!sortedPostsData.length) {
notFound();
}
return {...}
}
H2Loop over a number in React inside JSX
Scenario: Sometimes we need to render multiple similar components and don't need to use an existed array. So we need to loop over a loop count value.
tsx
<ul>
{[...Array(10)].map((_, i) => {
return <li key={i}>{i}</li>;
})}
</ul>
H2Exclusive type
Scenario: When I wrote Chip component, I expected it either accept
date
ortext
stuff like that, otherwise code editor would throw an exception.
Here's what I did.
tsx
interface Common {
size?: ChipSizes;
icon?: IconType | string;
style?: ChipStyles;
}
interface Time extends Common {
date: string | Date;
text?: never;
link?: never;
onClick?: never;
}
interface Link extends Common {
text?: string;
link: string;
color?: ChipColors;
title?: string;
className?: string;
onClick?: never;
}
interface Button extends Common {
...
}
interface Text extends Common {
...
}
type Chip = Time | Link | Button | Text;
const Chip = (props: Chip) => {
if ("date" in props) {
{props.date}
...
} else if ("link" in props) {...}
...
}
H2Related Posts
The algorithms here are super brain-burning.
ts
export const getPostData = (abbrlink: string) => {
//...
const postsTags: PostMetadata[] = [];
postData.tags.forEach((tag) =>
postsTags.push(
...postsMetadata.filter(
(post) => post.tags.includes(tag) && post.abbrlink !== abbrlink
)
)
);
const postsCategories: PostMetadata[] = [];
postData.categories.forEach((category) =>
postsCategories.push(
...postsMetadata.filter(
(post) =>
post.categories.includes(category) && post.abbrlink !== abbrlink
)
)
);
const postsWeighted: any[] = [];
postsTags.forEach((post) => {
const index = postsWeighted.findIndex(
(origin) => origin.abbrlink === post.abbrlink
);
if (index !== -1) {
postsWeighted[index].weight += 2;
} else {
postsWeighted.push({ ...post, weight: 2 });
}
});
postsCategories.forEach((post) => {
const index = postsWeighted.findIndex(
(origin) => origin.abbrlink === post.abbrlink
);
if (index !== -1) {
postsWeighted[index].weight++;
} else {
postsWeighted.push({ ...post, weight: 1 });
}
});
postsWeighted.sort((a, b) => b.weight - a.weight);
const relatedPosts = postsWeighted.slice(0, 4);
return {
postData: postData,
prev: prev,
next: next,
relatedPosts: relatedPosts,
};
};
H1优化网站
TODO