diff --git a/lib/notion/mapImage.js b/lib/notion/mapImage.js index 523980ae..3d91cd4d 100644 --- a/lib/notion/mapImage.js +++ b/lib/notion/mapImage.js @@ -1,7 +1,7 @@ import BLOG from '@/blog.config' /** - * Notion图片映射处理有emjji的图标 + * Notion图片映射处理有emoji的图标 * @param {*} img * @param {*} value * @returns @@ -20,7 +20,7 @@ const mapImgUrl = (img, block, type = 'block') => { } // notion永久图床地址 - if (!ret && img.indexOf('secure.notion-static.com') > 0 && BLOG.IMG_URL_TYPE === 'Notion') { + if (!ret && img.indexOf('secure.notion-static.com') > 0 && (BLOG.IMG_URL_TYPE === 'Notion' || type !== 'block')) { ret = 'https://www.notion.so/image/' + encodeURIComponent(img) + '?table=' + type + '&id=' + block.id } diff --git a/lib/robots.txt.js b/lib/robots.txt.js index 917796d7..4405164f 100644 --- a/lib/robots.txt.js +++ b/lib/robots.txt.js @@ -3,17 +3,23 @@ import fs from 'fs' import BLOG from '@/blog.config' export async function generateRobotsTxt() { - fs.mkdirSync('./public/rss', { recursive: true }) - fs.writeFileSync('./public/robots.txt', ` - # * - User-agent: * - Allow: / - - # Host - Host: ${BLOG.LINK} - - # Sitemaps - Sitemap: ${BLOG.LINK}/sitemap.xml - - `) + const content = ` + # * + User-agent: * + Allow: / + + # Host + Host: ${BLOG.LINK} + + # Sitemaps + Sitemap: ${BLOG.LINK}/sitemap.xml + + ` + try { + fs.mkdirSync('./public', { recursive: true }) + fs.writeFileSync('./public/robots.txt', content) + } catch (error) { + // 在vercel运行环境是只读的,这里会报错; + // 但在vercel编译阶段、或VPS等其他平台这行代码会成功执行 + } } diff --git a/lib/rss.js b/lib/rss.js index 66723f93..02121d49 100644 --- a/lib/rss.js +++ b/lib/rss.js @@ -5,6 +5,26 @@ import ReactDOMServer from 'react-dom/server' import { getPostBlocks } from './notion' import NotionPage from '@/components/NotionPage' +/** + * 生成RSS内容 + * @param {*} post + * @returns + */ +const createFeedContent = async post => { + // 加密的文章内容只返回摘要 + if (post.password && post.password !== '') { + return post.summary + } + const blockMap = await getPostBlocks(post.id, 'rss-content') + if (blockMap) { + post.blockMap = blockMap + const content = ReactDOMServer.renderToString() + const regexExp = + /
.*?<\/svg>
.*?<\/div><\/div>
.*?<\/div><\/div><\/div><\/div>/g + return content.replace(regexExp, '') + } +} + export async function generateRss(posts) { const year = new Date().getFullYear() const feed = new Feed({ @@ -30,28 +50,14 @@ export async function generateRss(posts) { }) } - fs.mkdirSync('./public/rss', { recursive: true }) - fs.writeFileSync('./public/rss/feed.xml', feed.rss2()) - fs.writeFileSync('./public/rss/atom.xml', feed.atom1()) - fs.writeFileSync('./public/rss/feed.json', feed.json1()) -} - -/** - * 生成RSS内容 - * @param {*} post - * @returns - */ -const createFeedContent = async post => { - // 加密的文章内容只返回摘要 - if (post.password && post.password !== '') { - return post.summary - } - const blockMap = await getPostBlocks(post.id, 'rss-content') - if (blockMap) { - post.blockMap = blockMap - const content = ReactDOMServer.renderToString() - const regexExp = - /
.*?<\/svg>
.*?<\/div><\/div>
.*?<\/div><\/div><\/div><\/div>/g - return content.replace(regexExp, '') + try { + fs.mkdirSync('./public/rss', { recursive: true }) + fs.writeFileSync('./public/rss/feed.xml', feed.rss2()) + fs.writeFileSync('./public/rss/atom.xml', feed.atom1()) + fs.writeFileSync('./public/rss/feed.json', feed.json1()) + } catch (error) { + // 在vercel运行环境是只读的,这里会报错; + // 但在vercel编译阶段、或VPS等其他平台这行代码会成功执行 + // RSS被高频词访问将大量消耗服务端资源,故作为静态文件 } } diff --git a/lib/sitemap.xml.js b/lib/sitemap.xml.js index 0e057610..7522951d 100644 --- a/lib/sitemap.xml.js +++ b/lib/sitemap.xml.js @@ -3,8 +3,6 @@ import fs from 'fs' import BLOG from '@/blog.config' export async function generateSitemapXml({ allPages }) { - fs.mkdirSync('./public', { recursive: true }) - const urls = [{ loc: `${BLOG.LINK}`, lastmod: new Date().toISOString().split('T')[0], @@ -31,9 +29,13 @@ export async function generateSitemapXml({ allPages }) { }) }) const xml = createSitemapXml(urls) - fs.writeFileSync('./public/sitemap.xml', xml) + try { + fs.writeFileSync('sitemap.xml', xml) + fs.writeFileSync('./public/sitemap.xml', xml) + } catch (error) { + console.warn('无法写入文件', error) + } } - function createSitemapXml(urls) { let urlsXml = '' urls.forEach(u => { @@ -46,7 +48,7 @@ function createSitemapXml(urls) { }) return ` - { + let isMobile = false + if (!isBrowser()) { + return isMobile + } + + // 这个判断会引发 TypeError: navigator.userAgentData.mobile is undefined 问题,导致博客无法正常工作 + // if (!isMobile && navigator.userAgentData.mobile) { + // isMobile = true + // } + + if (!isMobile && (/Mobi|Android|iPhone/i.test(navigator.userAgent))) { + isMobile = true + } + + if (/Android|iPhone|iPad|iPod/i.test(navigator.platform)) { + isMobile = true + } + + if (typeof window.orientation !== 'undefined') { + isMobile = true + } + + return isMobile +} diff --git a/pages/_app.js b/pages/_app.js index 41feba71..e6749281 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -24,6 +24,7 @@ import smoothscroll from 'smoothscroll-polyfill' import AOS from 'aos' import 'aos/dist/aos.css' // You can also use for styles +import { isMobile } from '@/lib/utils' const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false }) const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false }) @@ -57,7 +58,9 @@ const MyApp = ({ Component, pageProps }) => { useEffect(() => { AOS.init() - smoothscroll.polyfill() + if (isMobile()) { + smoothscroll.polyfill() + } }, []) return ( diff --git a/pages/index.js b/pages/index.js index 663462fd..542500fb 100644 --- a/pages/index.js +++ b/pages/index.js @@ -5,7 +5,6 @@ import * as ThemeMap from '@/themes' import { useGlobal } from '@/lib/global' import { generateRss } from '@/lib/rss' import { generateRobotsTxt } from '@/lib/robots.txt' -import { generateSitemapXml } from '@/lib/sitemap.xml' const Index = props => { const { theme } = useGlobal() const ThemeComponents = ThemeMap[theme] @@ -44,14 +43,12 @@ export async function getStaticProps() { } } + // 生成robotTxt + generateRobotsTxt() // 生成Feed订阅 if (JSON.parse(BLOG.ENABLE_RSS)) { generateRss(props?.latestPosts || []) } - // 生成robotTxt - generateRobotsTxt() - // 生成sitemap.xml - generateSitemapXml({ allPages: props.allPages }) delete props.allPages diff --git a/pages/sitemap.xml.js b/pages/sitemap.xml.js new file mode 100644 index 00000000..b17b3e45 --- /dev/null +++ b/pages/sitemap.xml.js @@ -0,0 +1,60 @@ +// pages/sitemap.xml.js +import { getServerSideSitemap } from 'next-sitemap' +import { getGlobalNotionData } from '@/lib/notion/getNotionData' +import BLOG from '@/blog.config' + +export const getServerSideProps = async (ctx) => { + const { allPages } = await getGlobalNotionData({ from: 'rss' }) + const defaultFields = [ + { + loc: `${BLOG.LINK}`, + lastmod: new Date().toISOString().split('T')[0], + changefreq: 'daily', + priority: '0.7' + }, { + loc: `${BLOG.LINK}/archive`, + lastmod: new Date().toISOString().split('T')[0], + changefreq: 'daily', + priority: '0.7' + }, { + loc: `${BLOG.LINK}/category`, + lastmod: new Date().toISOString().split('T')[0], + changefreq: 'daily', + priority: '0.7' + }, { + loc: `${BLOG.LINK}/feed`, + lastmod: new Date().toISOString().split('T')[0], + changefreq: 'daily', + priority: '0.7' + }, { + loc: `${BLOG.LINK}/search`, + lastmod: new Date().toISOString().split('T')[0], + changefreq: 'daily', + priority: '0.7' + }, { + loc: `${BLOG.LINK}/tag`, + lastmod: new Date().toISOString().split('T')[0], + changefreq: 'daily', + priority: '0.7' + } + ] + const postFields = allPages?.map(post => { + return { + loc: `${BLOG.LINK}/${post.slug}`, + lastmod: new Date(post?.date?.start_date || post?.createdTime).toISOString().split('T')[0], + changefreq: 'daily', + priority: '0.7' + } + }) + const fields = defaultFields.concat(postFields) + + // 缓存 + ctx.res.setHeader( + 'Cache-Control', + 'public, max-age=3600, stale-while-revalidate=59' + ) + + return getServerSideSitemap(ctx, fields) +} + +export default () => { } diff --git a/themes/example/components/BlogListScroll.js b/themes/example/components/BlogListScroll.js index d70e4bc5..b3d7f3f0 100644 --- a/themes/example/components/BlogListScroll.js +++ b/themes/example/components/BlogListScroll.js @@ -78,5 +78,5 @@ export const BlogListScroll = props => {
- ); + ) } diff --git a/themes/hexo/LayoutBase.js b/themes/hexo/LayoutBase.js index 701daa71..066c4404 100644 --- a/themes/hexo/LayoutBase.js +++ b/themes/hexo/LayoutBase.js @@ -1,6 +1,6 @@ import CommonHead from '@/components/CommonHead' -import { useEffect, useState } from 'react' - +import { useCallback, useEffect, useState } from 'react' +import throttle from 'lodash.throttle' import Footer from './components/Footer' import JumpToTopButton from './components/JumpToTopButton' import SideRight from './components/SideRight' @@ -42,23 +42,20 @@ const LayoutBase = props => { ) const { onLoading } = useGlobal() + const throttleMs = 200 + const scrollListener = useCallback(throttle(() => { + const targetRef = document.getElementById('wrapper') + const clientHeight = targetRef?.clientHeight + const scrollY = window.pageYOffset + const fullHeight = clientHeight - window.outerHeight + let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0)) + if (per > 100) per = 100 + const shouldShow = scrollY > 100 && per > 0 - const scrollListener = () => { - requestAnimationFrame(() => { - const targetRef = document.getElementById('wrapper') - const clientHeight = targetRef?.clientHeight - const scrollY = window.pageYOffset - const fullHeight = clientHeight - window.outerHeight - let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0)) - if (per > 100) per = 100 - const shouldShow = scrollY > 100 && per > 0 - - if (shouldShow !== showFloatButton) { - switchShow(shouldShow) - } - // changePercent(per) - }) - } + if (shouldShow !== showFloatButton) { + switchShow(shouldShow) + } + }, throttleMs)) useEffect(() => { document.addEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener) diff --git a/themes/hexo/components/Announcement.js b/themes/hexo/components/Announcement.js index 391fd3a8..677cb467 100644 --- a/themes/hexo/components/Announcement.js +++ b/themes/hexo/components/Announcement.js @@ -7,7 +7,7 @@ const Announcement = ({ post, className }) => { const { locale } = useGlobal() if (post?.blockMap) { return
-
+
{locale.COMMON.ANNOUNCEMENT}
{post && (
diff --git a/themes/hexo/components/BlogPostCard.js b/themes/hexo/components/BlogPostCard.js index 02940295..eeb6e0b2 100644 --- a/themes/hexo/components/BlogPostCard.js +++ b/themes/hexo/components/BlogPostCard.js @@ -1,108 +1,50 @@ import BLOG from '@/blog.config' import Link from 'next/link' import React from 'react' -import TagItemMini from './TagItemMini' import CONFIG_HEXO from '../config_hexo' -import NotionPage from '@/components/NotionPage' -import Image from 'next/image' +import { BlogPostCardInfo } from './BlogPostCardInfo' +// import Image from 'next/image' -const BlogPostCard = ({ post, showSummary, siteInfo }) => { +const BlogPostCard = ({ index, post, showSummary, siteInfo }) => { const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap if (post && !post.page_cover && CONFIG_HEXO.POST_LIST_COVER_DEFAULT) { post.page_cover = siteInfo?.pageCover } const showPageCover = CONFIG_HEXO.POST_LIST_COVER && post?.page_cover + const delay = (index % 2) * 200 + return (
-
- + {/* 文字内容 */} +
- {post.title} + - - -
- - - - {post.date?.start_date || post.lastEditedTime} - - -
- - {(!showPreview || showSummary) && !post.results && ( -

- {post.summary} -

- )} - - {/* 搜索结果 */} - {post.results && ( -

- {post.results.map(r => ( - {r} - ))} -

- )} - - {showPreview && ( -
- -
- )} - -
- - - - {post.category} - - -
-
- {' '} - {post.tagItems.map(tag => ( - - ))} -
-
-
+ {/* 图片封面 */} {showPageCover && !showPreview && post?.page_cover && (
{/* eslint-disable-next-line @next/next/no-img-element */} - {/* {post.title} */} -
+ /> + {/*
{ blurDataURL='/bg_image.jpg' style={{ objectFit: 'cover' }} fill/> -
+
*/}
)} diff --git a/themes/hexo/components/BlogPostCardInfo.js b/themes/hexo/components/BlogPostCardInfo.js new file mode 100644 index 00000000..8c6c6be1 --- /dev/null +++ b/themes/hexo/components/BlogPostCardInfo.js @@ -0,0 +1,79 @@ +import BLOG from '@/blog.config' +import NotionPage from '@/components/NotionPage' +import Link from 'next/link' +import TagItemMini from './TagItemMini' + +export const BlogPostCardInfo = ({ post, showPreview, showSummary }) => { + return <> + {/* 标题 */} + + + {post.title} + + + + {/* 日期 */} +
+ + + + {post.date?.start_date || post.lastEditedTime} + + +
+ + {/* 摘要 */} + {(!showPreview || showSummary) && !post.results && ( +

+ {post.summary} +

+ )} + + {/* 搜索结果 */} + {post.results && ( +

+ {post.results.map(r => ( + {r} + ))} +

+ )} + {/* 预览 */} + {showPreview && ( +
+ +
+ )} + + {/* 分类标签 */} +
+ + + + {post.category} + + +
+
+ {' '} + {post.tagItems.map(tag => ( + + ))} +
+
+
+ +} diff --git a/themes/hexo/components/BlogPostListPage.js b/themes/hexo/components/BlogPostListPage.js index ff4249d7..9a2c68f7 100644 --- a/themes/hexo/components/BlogPostListPage.js +++ b/themes/hexo/components/BlogPostListPage.js @@ -22,7 +22,7 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => { {/* 文章列表 */}
{posts.map(post => ( - + ))}
{showPagination && } diff --git a/themes/hexo/components/Header.js b/themes/hexo/components/Header.js index d6fff722..c15153f3 100644 --- a/themes/hexo/components/Header.js +++ b/themes/hexo/components/Header.js @@ -1,8 +1,9 @@ -import Image from 'next/image' -import { useEffect, useState } from 'react' +// import Image from 'next/image' +import { useCallback, useEffect, useState } from 'react' import Typed from 'typed.js' import CONFIG_HEXO from '../config_hexo' import NavButtonGroup from './NavButtonGroup' +import throttle from 'lodash.throttle' let wrapperTop = 0 let windowTop = 0 @@ -53,17 +54,47 @@ const Header = props => { }) } + const autoScrollEnd = () => { + if (autoScroll) { + windowTop = window.scrollY + autoScroll = false + } + } + const throttleMs = 200 + const scrollTrigger = useCallback(throttle(() => { + if (screen.width <= 768) { + return + } + + const scrollS = window.scrollY + // 自动滚动 + if ((scrollS > windowTop) & (scrollS < window.innerHeight) && !autoScroll + ) { + autoScroll = true + window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) + autoScrollEnd() + } + if ((scrollS < windowTop) && (scrollS < window.innerHeight) && !autoScroll) { + autoScroll = true + window.scrollTo({ top: 0, behavior: 'smooth' }) + autoScrollEnd() + } + windowTop = scrollS + }, throttleMs)) + return (