Todo-list
搭建点滴
渲染 Markdown
我使用了 MDX ,理由是对 JSX、TSX 友好,且内置很多丰富功能,the remark
and rehype
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 .
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 = {{
...
}}
/>
Syntax 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
文件使用的。
有一些 Markdown 功能 MDXRemote 没有,remark-gfm 就弥补了。
音乐播放器——Aplayer
Office Website: Aplayer
还有网易云自带外链播放器
method 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);
}`
);
},
});
}
}
method 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;
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 ;
}
tsx const bg = document. querySelector ( ".background" ) as any ;
const lenis = useLenis (({ scroll }) => {
bg[ "style" ].backgroundPositionY = `-${ scroll / 2 }px` ;
});
API: https://rizzapi-doc.vercel.app/
疑难杂症
作者卡片-时间显示延迟
像这种需要在客户端加载的组件要在代码顶部加上'use client'
。
getCrtTime()
记得最开始要执行一次,否则会时钟会在你指定间隔时间后显示。
tsx 'use client'
{ ... }
useEffect (() => {
getCrtTime ();
const interval = setInterval (() => {
getCrtTime ();
}, 1000 ); // 每秒更新
return () => {
clearInterval (interval); // 清除定时器以防内存泄漏
};
}, [])
Object 储存 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 />;
场景:我一直不知如何在 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 >
</>
CSS 设置宽度
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} );
}
}
}
若应用在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);
};
}, []);
场景:使用官网说的 Swiper React Components 时会报错,只需申明是 CSR 即可,我理解的是因为这个组件需要 user action,比如用户可以手动点击、滑动等操作。
在 tsx file 顶部加一个 "use client"
即可。
Switch 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 " ] {.. . }
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 { ... }
}
Loop 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 >
Exclusive type
Scenario: When I wrote Chip component, I expected it either accept date
or text
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) { ... }
...
}
Related 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,
};
};
优化网站
TODO