slug 兼容性优化,支持用任意前缀访问文章不限于article

This commit is contained in:
tangly1024.com
2024-05-08 15:13:12 +08:00
parent f803b7ad06
commit de0908ea94
7 changed files with 209 additions and 151 deletions

View File

@@ -26,9 +26,12 @@ const Equation = dynamic(
{ ssr: false } { ssr: false }
) )
const Pdf = dynamic(() => import('react-notion-x/build/third-party/pdf').then(m => m.Pdf), { const Pdf = dynamic(
ssr: false () => import('react-notion-x/build/third-party/pdf').then(m => m.Pdf),
}) {
ssr: false
}
)
// https://github.com/txs // https://github.com/txs
// import PrismMac from '@/components/PrismMac' // import PrismMac from '@/components/PrismMac'
@@ -46,13 +49,25 @@ const TweetEmbed = dynamic(() => import('react-tweet-embed'), {
/** /**
* 文内google广告 * 文内google广告
*/ */
const AdEmbed = dynamic(() => import('@/components/GoogleAdsense').then(m => m.AdEmbed), { ssr: true }) const AdEmbed = dynamic(
() => import('@/components/GoogleAdsense').then(m => m.AdEmbed),
{ ssr: true }
)
const Collection = dynamic(() => import('react-notion-x/build/third-party/collection').then(m => m.Collection), { const Collection = dynamic(
ssr: true () =>
}) import('react-notion-x/build/third-party/collection').then(
m => m.Collection
),
{
ssr: true
}
)
const Modal = dynamic(() => import('react-notion-x/build/third-party/modal').then(m => m.Modal), { ssr: false }) const Modal = dynamic(
() => import('react-notion-x/build/third-party/modal').then(m => m.Modal),
{ ssr: false }
)
const Tweet = ({ id }) => { const Tweet = ({ id }) => {
return <TweetEmbed tweetId={id} /> return <TweetEmbed tweetId={id} />
@@ -83,19 +98,22 @@ const NotionPage = ({ post, className }) => {
// 将相册gallery下的图片加入放大功能 // 将相册gallery下的图片加入放大功能
if (siteConfig('POST_DISABLE_GALLERY_CLICK')) { if (siteConfig('POST_DISABLE_GALLERY_CLICK')) {
setTimeout(() => { setTimeout(() => {
if (isBrowser) { const imgList = document?.querySelectorAll(
const imgList = document?.querySelectorAll('.notion-collection-card-cover img') '.notion-asset-wrapper-image img'
if (imgList && zoomRef.current) { )
for (let i = 0; i < imgList.length; i++) {
zoomRef.current.attach(imgList[i])
}
}
const cards = document.getElementsByClassName('notion-collection-card') console.log('放大', imgList)
for (const e of cards) {
e.removeAttribute('href') if (imgList && zoomRef.current) {
for (let i = 0; i < imgList.length; i++) {
zoomRef.current.attach(imgList[i])
} }
} }
const cards = document.getElementsByClassName('notion-collection-card')
for (const e of cards) {
e.removeAttribute('href')
}
}, 800) }, 800)
} }
@@ -108,10 +126,16 @@ const NotionPage = ({ post, className }) => {
const allAnchorTags = document.getElementsByTagName('a') // 或者使用 document.querySelectorAll('a') 获取 NodeList const allAnchorTags = document.getElementsByTagName('a') // 或者使用 document.querySelectorAll('a') 获取 NodeList
for (const anchorTag of allAnchorTags) { for (const anchorTag of allAnchorTags) {
if (anchorTag?.target === '_blank') { if (anchorTag?.target === '_blank') {
const hrefWithoutQueryHash = anchorTag.href.split('?')[0].split('#')[0] const hrefWithoutQueryHash = anchorTag.href
const hrefWithRelativeHash = currentURL.split('#')[0] + anchorTag.href.split('#')[1] .split('?')[0]
.split('#')[0]
const hrefWithRelativeHash =
currentURL.split('#')[0] + anchorTag.href.split('#')[1]
if (currentURL === hrefWithoutQueryHash || currentURL === hrefWithRelativeHash) { if (
currentURL === hrefWithoutQueryHash ||
currentURL === hrefWithRelativeHash
) {
anchorTag.target = '_self' anchorTag.target = '_self'
} }
} }
@@ -121,14 +145,20 @@ const NotionPage = ({ post, className }) => {
// 放大图片:调整图片质量 // 放大图片:调整图片质量
const observer = new MutationObserver((mutationsList, observer) => { const observer = new MutationObserver((mutationsList, observer) => {
mutationsList.forEach(mutation => { mutationsList.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') { if (
mutation.type === 'attributes' &&
mutation.attributeName === 'class'
) {
if (mutation.target.classList.contains('medium-zoom-image--opened')) { if (mutation.target.classList.contains('medium-zoom-image--opened')) {
// 等待动画完成后替换为更高清的图像 // 等待动画完成后替换为更高清的图像
setTimeout(() => { setTimeout(() => {
// 获取该元素的 src 属性 // 获取该元素的 src 属性
const src = mutation?.target?.getAttribute('src') const src = mutation?.target?.getAttribute('src')
// 替换为更高清的图像 // 替换为更高清的图像
mutation?.target?.setAttribute('src', compressImage(src, siteConfig('IMAGE_ZOOM_IN_WIDTH', 1200))) mutation?.target?.setAttribute(
'src',
compressImage(src, siteConfig('IMAGE_ZOOM_IN_WIDTH', 1200))
)
}, 800) }, 800)
} }
} }
@@ -136,21 +166,23 @@ const NotionPage = ({ post, className }) => {
}) })
// 监视整个文档中的元素和属性的变化 // 监视整个文档中的元素和属性的变化
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['class'] }) observer.observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ['class']
})
return () => { return () => {
observer.disconnect() observer.disconnect()
} }
}, []) }, [post])
if (!post || !post.blockMap) {
return <>{post?.summary || ''}</>
}
return ( return (
<div id='notion-article' className={`mx-auto overflow-hidden ${className || ''}`}> <div
id='notion-article'
className={`mx-auto overflow-hidden ${className || ''}`}>
<NotionRenderer <NotionRenderer
recordMap={post.blockMap} recordMap={post?.blockMap}
mapPageUrl={mapPageUrl} mapPageUrl={mapPageUrl}
mapImageUrl={mapImgUrl} mapImageUrl={mapImgUrl}
components={{ components={{

View File

@@ -156,19 +156,11 @@ function getCustomNav({ allPages }) {
if (allPages && allPages.length > 0) { if (allPages && allPages.length > 0) {
allPages.forEach(p => { allPages.forEach(p => {
p.to = p.slug p.to = p.slug
if (p?.slug?.indexOf('http') === 0) {
p.target = '_blank'
} else {
p.target = '_self'
if (p?.slug?.indexOf('/') !== 0) {
p.to = '/' + p.slug
}
}
customNav.push({ customNav.push({
icon: p.icon || null, icon: p.icon || null,
name: p.title, name: p.title,
to: p.slug, href: p.href,
target: '_blank', target: p.target,
show: true show: true
}) })
}) })
@@ -192,15 +184,6 @@ function getCustomMenu({ collectionData, NOTION_CONFIG }) {
if (menuPages && menuPages.length > 0) { if (menuPages && menuPages.length > 0) {
menuPages.forEach(e => { menuPages.forEach(e => {
e.show = true e.show = true
if (e?.slug?.indexOf('http') === 0) {
e.target = '_blank'
e.to = e.slug
} else {
e.target = '_self'
if (e?.slug?.indexOf('http') !== 0 && e?.slug?.indexOf('/') !== 0) {
e.to = '/' + e.slug
}
}
if (e.type === BLOG.NOTION_PROPERTY_NAME.type_menu) { if (e.type === BLOG.NOTION_PROPERTY_NAME.type_menu) {
menus.push(e) menus.push(e)
} else if (e.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) { } else if (e.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) {

View File

@@ -4,6 +4,7 @@ import { getDateValue, getTextContent } from 'notion-utils'
import formatDate from '../utils/formatDate' import formatDate from '../utils/formatDate'
// import { createHash } from 'crypto' // import { createHash } from 'crypto'
import md5 from 'js-md5' import md5 from 'js-md5'
import { checkContainHttp, sliceUrlFromHttp } from '../utils'
import { mapImgUrl } from './mapImage' import { mapImgUrl } from './mapImage'
/** /**
@@ -106,7 +107,7 @@ export default async function getPageProperties(
properties.pageCover = mapImgUrl(value?.format?.page_cover, value) ?? '' properties.pageCover = mapImgUrl(value?.format?.page_cover, value) ?? ''
properties.pageCoverThumbnail = properties.pageCoverThumbnail =
mapImgUrl(value?.format?.page_cover, value, 'block') ?? '' mapImgUrl(value?.format?.page_cover, value, 'block') ?? ''
properties.ext = converToJSON(properties?.ext) properties.ext = convertToJSON(properties?.ext)
properties.content = value.content ?? [] properties.content = value.content ?? []
properties.tagItems = properties.tagItems =
properties?.tags?.map(tag => { properties?.tags?.map(tag => {
@@ -117,28 +118,41 @@ export default async function getPageProperties(
}) || [] }) || []
delete properties.content delete properties.content
// 处理URL // 处理URL 为文章添加一个href字段专门用来跳转访问
if (properties.type === 'Post') { if (properties.type === 'Post') {
properties.slug = BLOG.POST_URL_PREFIX properties.href = BLOG.POST_URL_PREFIX
? generateCustomizeUrl(properties) ? generateCustomizeUrl(properties)
: properties.slug ?? properties.id : properties.slug ?? properties.id
} else if (properties.type === 'Page') { } else if (properties.type === 'Page') {
properties.slug = properties.slug ?? properties.id properties.href = properties.slug ?? properties.id
} else if (properties.type === 'Menu' || properties.type === 'SubMenu') { } else if (properties.type === 'Menu' || properties.type === 'SubMenu') {
// 菜单路径为空、作为可展开菜单使用 // 菜单路径为空、作为可展开菜单使用
properties.to = properties.slug ?? '#' properties.href = properties.slug ?? '#'
properties.name = properties.title ?? '' properties.name = properties.title ?? ''
} }
// 开启伪静态路径 // 开启伪静态路径
if (JSON.parse(BLOG.PSEUDO_STATIC)) { if (JSON.parse(BLOG.PSEUDO_STATIC)) {
if ( if (
!properties?.slug?.endsWith('.html') && !properties?.href?.endsWith('.html') &&
!properties?.slug?.startsWith('http') !properties?.href?.startsWith('http')
) { ) {
properties.slug += '.html' properties.href += '.html'
} }
} }
// 最终检查超链接
properties.href = checkContainHttp(properties?.href)
? sliceUrlFromHttp(properties?.href)
: `/${properties.href}`
// 设置链接在页内或新页面打开
if (properties.href?.indexOf('http') === 0) {
properties.target = '_blank'
} else {
properties.target = '_self'
}
// 密码字段md5 // 密码字段md5
properties.password = properties.password properties.password = properties.password
? md5(properties.slug + properties.password) ? md5(properties.slug + properties.password)
@@ -151,7 +165,7 @@ export default async function getPageProperties(
* @param {*} str * @param {*} str
* @returns * @returns
*/ */
function converToJSON(str) { function convertToJSON(str) {
if (!str) { if (!str) {
return {} return {}
} }

90
lib/utils/post.js Normal file
View File

@@ -0,0 +1,90 @@
/**
* 文章相关工具
*/
import { checkContainHttp } from '.'
/**
* 获取文章的关联推荐文章列表,目前根据标签关联性筛选
* @param post
* @param {*} allPosts
* @param {*} count
* @returns
*/
export function getRecommendPost(post, allPosts, count = 6) {
let recommendPosts = []
const postIds = []
const currentTags = post?.tags || []
for (let i = 0; i < allPosts.length; i++) {
const p = allPosts[i]
if (p.id === post.id || p.type.indexOf('Post') < 0) {
continue
}
for (let j = 0; j < currentTags.length; j++) {
const t = currentTags[j]
if (postIds.indexOf(p.id) > -1) {
continue
}
if (p.tags && p.tags.indexOf(t) > -1) {
recommendPosts.push(p)
postIds.push(p.id)
}
}
}
if (recommendPosts.length > count) {
recommendPosts = recommendPosts.slice(0, count)
}
return recommendPosts
}
/**
* 确认slug中不包含 / 符号
* @param {*} row
* @returns
*/
export function checkSlugHasNoSlash(row) {
let slug = row.slug
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return (
(slug.match(/\//g) || []).length === 0 &&
!checkContainHttp(slug) &&
row.type.indexOf('Menu') < 0
)
}
/**
* 检查url中包含一个 /
* @param {*} row
* @returns
*/
export function checkSlugHasOneSlash(row) {
let slug = row.slug
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return (
(slug.match(/\//g) || []).length === 1 &&
!checkContainHttp(slug) &&
row.type.indexOf('Menu') < 0
)
}
/**
* 检查url中包含两个及以上的 /
* @param {*} row
* @returns
*/
export function checkSlugHasMorThanTwoSlash(row) {
let slug = row.slug
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return (
(slug.match(/\//g) || []).length >= 2 &&
row.type.indexOf('Menu') < 0 &&
!checkContainHttp(slug)
)
}

View File

@@ -2,9 +2,9 @@ import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData' import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia' import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils' import { checkSlugHasMorThanTwoSlash, getRecommendPost } from '@/lib/utils/post'
import { idToUuid } from 'notion-utils' import { idToUuid } from 'notion-utils'
import Slug, { getRecommendPost } from '..' import Slug from '..'
/** /**
* 根据notion的slug访问页面 * 根据notion的slug访问页面
@@ -33,7 +33,7 @@ export async function getStaticPaths() {
return { return {
paths: allPages paths: allPages
?.filter(row => checkSlug(row)) ?.filter(row => checkSlugHasMorThanTwoSlash(row))
.map(row => ({ .map(row => ({
params: { params: {
prefix: row.slug.split('/')[0], prefix: row.slug.split('/')[0],
@@ -54,20 +54,15 @@ export async function getStaticProps({
params: { prefix, slug, suffix }, params: { prefix, slug, suffix },
locale locale
}) { }) {
let fullSlug = prefix + '/' + slug + '/' + suffix.join('/') const fullSlug = prefix + '/' + slug + '/' + suffix.join('/')
const from = `slug-props-${fullSlug}` const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from, locale }) const props = await getGlobalData({ from, locale })
if (siteConfig('PSEUDO_STATIC', BLOG.PSEUDO_STATIC, props.NOTION_CONFIG)) {
if (!fullSlug.endsWith('.html')) {
fullSlug += '.html'
}
}
// 在列表内查找文章 // 在列表内查找文章
props.post = props?.allPages?.find(p => { props.post = props?.allPages?.find(p => {
return ( return (
p.type.indexOf('Menu') < 0 && p.type.indexOf('Menu') < 0 &&
(p.slug === fullSlug || p.id === idToUuid(fullSlug)) (p.slug === slug || p.slug === fullSlug || p.id === idToUuid(fullSlug))
) )
}) })
@@ -132,16 +127,4 @@ export async function getStaticProps({
} }
} }
function checkSlug(row) {
let slug = row.slug
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return (
(slug.match(/\//g) || []).length >= 2 &&
row.type.indexOf('Menu') < 0 &&
!checkContainHttp(slug)
)
}
export default PrefixSlug export default PrefixSlug

View File

@@ -2,9 +2,13 @@ import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData' import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia' import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils' import {
checkSlugHasNoSlash,
checkSlugHasOneSlash,
getRecommendPost
} from '@/lib/utils/post'
import { idToUuid } from 'notion-utils' import { idToUuid } from 'notion-utils'
import Slug, { getRecommendPost } from '..' import Slug from '..'
/** /**
* 根据notion的slug访问页面 * 根据notion的slug访问页面
@@ -26,12 +30,26 @@ export async function getStaticPaths() {
const from = 'slug-paths' const from = 'slug-paths'
const { allPages } = await getGlobalData({ from }) const { allPages } = await getGlobalData({ from })
// 根据slug中的 / 分割成prefix和slug两个字段 ; 例如 article/test // 根据slug中的 / 分割成prefix和slug两个字段 ; 例如 article/test
// 最终用户可以通过 [domain]/[prefix]/[slug] 路径访问,即这里的 [domain]/article/test
const paths = allPages const paths = allPages
?.filter(row => checkSlug(row)) ?.filter(row => checkSlugHasOneSlash(row))
.map(row => ({ .map(row => ({
params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] } params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] }
})) }))
// 增加一种访问路径 允许通过 [category]/[slug] 访问文章
// 例如文章slug 是 test 然后文章的分类category是 production
// 则除了 [domain]/[slug] 以外,还支持分类名访问: [domain]/[category]/[slug]
console.log(
allPages
?.filter(row => checkSlugHasNoSlash(row) && row.category)
.map(row => ({
params: { prefix: row.category, slug: row.slug }
}))
)
return { return {
paths: paths, paths: paths,
fallback: true fallback: true
@@ -39,20 +57,15 @@ export async function getStaticPaths() {
} }
export async function getStaticProps({ params: { prefix, slug }, locale }) { export async function getStaticProps({ params: { prefix, slug }, locale }) {
let fullSlug = prefix + '/' + slug const fullSlug = prefix + '/' + slug
const from = `slug-props-${fullSlug}` const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from, locale }) const props = await getGlobalData({ from, locale })
if (siteConfig('PSEUDO_STATIC', BLOG.PSEUDO_STATIC, props.NOTION_CONFIG)) {
if (!fullSlug.endsWith('.html')) {
fullSlug += '.html'
}
}
// 在列表内查找文章 // 在列表内查找文章
props.post = props?.allPages?.find(p => { props.post = props?.allPages?.find(p => {
return ( return (
p.type.indexOf('Menu') < 0 && p.type.indexOf('Menu') < 0 &&
(p.slug === fullSlug || p.id === idToUuid(fullSlug)) (p.slug === slug || p.slug === fullSlug || p.id === idToUuid(fullSlug))
) )
}) })
@@ -116,15 +129,5 @@ export async function getStaticProps({ params: { prefix, slug }, locale }) {
) )
} }
} }
function checkSlug(row) {
let slug = row.slug
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return (
(slug.match(/\//g) || []).length === 1 &&
!checkContainHttp(slug) &&
row.type.indexOf('Menu') < 0
)
}
export default PrefixSlug export default PrefixSlug

View File

@@ -3,7 +3,7 @@ import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData' import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents' import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia' import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils' import { checkSlugHasNoSlash, getRecommendPost } from '@/lib/utils/post'
import { getLayoutByTheme } from '@/themes/theme' import { getLayoutByTheme } from '@/themes/theme'
import md5 from 'js-md5' import md5 from 'js-md5'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@@ -71,7 +71,7 @@ export async function getStaticPaths() {
const from = 'slug-paths' const from = 'slug-paths'
const { allPages } = await getGlobalData({ from }) const { allPages } = await getGlobalData({ from })
const paths = allPages const paths = allPages
?.filter(row => checkSlug(row)) ?.filter(row => checkSlugHasNoSlash(row))
.map(row => ({ params: { prefix: row.slug } })) .map(row => ({ params: { prefix: row.slug } }))
return { return {
paths: paths, paths: paths,
@@ -158,51 +158,4 @@ export async function getStaticProps({ params: { prefix }, locale }) {
} }
} }
/**
* 获取文章的关联推荐文章列表,目前根据标签关联性筛选
* @param post
* @param {*} allPosts
* @param {*} count
* @returns
*/
export function getRecommendPost(post, allPosts, count = 6) {
let recommendPosts = []
const postIds = []
const currentTags = post?.tags || []
for (let i = 0; i < allPosts.length; i++) {
const p = allPosts[i]
if (p.id === post.id || p.type.indexOf('Post') < 0) {
continue
}
for (let j = 0; j < currentTags.length; j++) {
const t = currentTags[j]
if (postIds.indexOf(p.id) > -1) {
continue
}
if (p.tags && p.tags.indexOf(t) > -1) {
recommendPosts.push(p)
postIds.push(p.id)
}
}
}
if (recommendPosts.length > count) {
recommendPosts = recommendPosts.slice(0, count)
}
return recommendPosts
}
function checkSlug(row) {
let slug = row.slug
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return (
(slug.match(/\//g) || []).length === 0 &&
!checkContainHttp(slug) &&
row.type.indexOf('Menu') < 0
)
}
export default Slug export default Slug