Post's cover

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 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.

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, 主要它俩如何正确配置成了问题,自己摸索出来了。

  1. 先将 next.config.js 替换成 next.config.mjs

    jsimport 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);
  2. 来到使用 <MDXRemote/> 的文件下进行一些配置,比如我这是 /app/posts/[abbrlink]/page.tsx

    tsximport 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 就弥补了。

H2音乐播放器——Aplayer

Office Website: Aplayer

还有网易云自带外链播放器

H2Extract dominant color from images

H3method 1

Drawback: I find there is no version for TSX.

Plugin: rgbaster.js.

jsfunction 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

scsshtml.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)

tsxconst 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

tsxinterface 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 />} 长这样。

第二种情况

tsxconst 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里

jsxuseEffect(() => { // 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

tsximport { 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

tsxconst 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

tsxconst 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 or text stuff like that, otherwise code editor would throw an exception.

Here's what I did.

tsxinterface 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) {...} ... }

The algorithms here are super brain-burning.

tsexport 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

Prev

Next

Related Posts