H1Todo-list
- 
性能优化
 - 
作者卡片新增 Hover 描述后显示我的标签,用弹幕形式
 - 
给 Author 页面加入动画(视差动画,依靠滑动触发)
 - 
Moment、love、bucket list 超过一定数量就套一个渐变透明玻璃外壳,或者一个按钮,点击查看更多(看能否找到 React 版瀑布流 JS,可控制很重要,而不是每次刷新才能加载)
 - 
图片灯箱
 - 
侧栏组件
- 网站指标(运行时间,文章字数,微博粉丝,B 站粉丝数)
 
 - 
评论区(用 section tag)
 - 
Stats
 - 
友链
 - 
TOC
 - 
自动给文章加一个 abbrlink
 - 
文章主题色优化,比如基于主题色生成深色、浅色
 - 
Navbar 上加一个页面跳转加载条。
 
H1搭建点滴
H2渲染 Markdown
我使用了 MDX,理由是对 JSX、TSX 友好,且内置很多丰富功能,the
remarkandrehypeecosystem 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.mjsjs
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.tsxtsx
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 的配置,这么配置后,语法高亮能正常使用,其余关于语法高亮的配置在
optionsobject 里设置。
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
dateortextstuff 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





