准备工作
notion-next:4.7.11
memos:v0.23.0
📝 导航
改动的文件:

增加新的页面路由
在项目根目录 /pages文件夹中新建一个名为memos的文件夹,并在文件夹中创建一个名为Index.js的文件夹
image.png在pages目录下memos/index.js添加以下代码
jsximport { DynamicLayout } from '@/themes/theme' import { siteConfig } from '@/lib/config' import { getGlobalData } from '@/lib/db/getSiteData' import React from 'react' import BLOG from '@/blog.config' const GalleryIndex = props => { const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG) return <DynamicLayout theme={theme} layoutName='LayoutMemosGallery' {...props} /> } export async function getStaticProps() { const from = 'gallery-index-props' const props = await getGlobalData({ from }) delete props.allPages return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) } } export default GalleryIndex
主题配置增加新页面的主题索引
在 blog.config.js 文件的 LAYOUT_MAPPINGS中增加路径 /memos 的组件映射
NotionNext 4.3版本才支持LAYOUT_MAPPINGS配置,如果不是这个版本,修改更复杂些,需要自己到themes/theme.js中修改对应的映射,建议升级到4.7.11版本。
image.png添加以下代码到指定位置:
jsx"/gallery" : "LayoutGallery",
然后在blog.config.js指定位置,添加以下环境变量配置:
image.png在blog.config.js文件中增加:
jsx//**************** 自定义配置官方没有提供,需要自己手动引入 start **************** MEMOS_GET_HOST_URL: process.env.NEXT_PUBLIC_MEMOS_GET_HOST_URL || 'https://memos.chenge.ink',// 随记地址 MEMOS_GET_CREATOR_ID: process.env.NEXT_PUBLIC_MEMOS_GET_CREATOR_ID || '1',// 随记创建者id MEMOS_GET_TAG: process.env.NEXT_PUBLIC_MEMOS_GET_TAG || 'images',// 随记标签 MEMOS_GET_PUBLIC: process.env.NEXT_PUBLIC_MEMOS_GET_PUBLIC || 'PUBLIC',// 随记公开状态 MEMOS_GALLERY_ENABLE: process.env.NEXT_PUBLIC_MEMOS_GALLERY_ENABLE || false,// 随记画册开关 //**************** 自定义配置官方没有提供,需要自己手动引入 start ****************
主题的代码修改,增加Gallery模块
hexo-change
这是hexo主题,我这里只是复制了一份进行修改的
建议将你选定的主题复制一份出来,重命名为自己的名字(比如我复制了hexo主题文件夹并命名为chenge),然后将主题切换为自己的主题,后续的主题修改就都在自己创建的主题中,不会和NotionNext官方的更新冲突。
image.png创建 MemosGallery.js 文件
新增一个MemosGallery组件,专门用于呈现Memos标签为images中图片url内容。我的路径是 themes/chenge/components/MemosGallery.js,你按照自己的主题找对应位置即可。
jsximport React, { useEffect, useState } from 'react'; import { loadExternalResource } from '@/lib/utils'; import { siteConfig } from '@/lib/config' import Script from 'next/script' const MemosGallery = () => { const [isResourcesLoaded, setResourcesLoaded] = useState(false); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); // 加载资源 useEffect(() => { const loadResources = async () => { try { console.log('Starting to load resources...'); // 只加载必要的 CSS 资源 await Promise.all([ loadExternalResource('/css/MemosGallery.css', 'css'), ]); console.log('Resources loaded successfully'); setResourcesLoaded(true); } catch (err) { console.error('Failed to load resources:', err); setError(err); } }; loadResources(); }, []); // 初始化 Memos 配置 useEffect(() => { if (isResourcesLoaded && typeof window !== 'undefined') { try { // 确保在 window 对象上创建 memosGalleryConfig window.memosGalleryConfig = { urlHost: siteConfig('MEMOS_GET_HOST_URL') || "https://memos.lxip.top", creatorId: siteConfig('MEMOS_GET_CREATOR_ID') || 1, tag: siteConfig("MEMOS_GET_TAG") || "images", public: siteConfig('MEMOS_GET_PUBLIC') || "PUBLIC", onLoadComplete: () => setIsLoading(false), onLoadError: (err) => { setError(err); setIsLoading(false); } }; // 初始化完配置后再加载 gallery.js const script = document.createElement('script'); script.src = '/js/gallery/gallery.js'; script.async = true; document.body.appendChild(script); } catch (err) { console.error('Failed to initialize memos gallery:', err); setError(err); setIsLoading(false); } } }, [isResourcesLoaded]); if (error) { return ( <div className="error-container"> <h3>Error loading memos gallery:</h3> <p>{error.message}</p> <pre>{error.stack}</pre> </div> ); } return ( <> {/* 使用 Next.js Script 组件加载基础依赖 */} <Script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" strategy="beforeInteractive" onError={(e) => { console.error('Failed to load jQuery:', e); setError(new Error('Failed to load jQuery')); }} /> <Script src="https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js" strategy="afterInteractive" onError={(e) => { console.error('Failed to load FancyBox:', e); setError(new Error('Failed to load FancyBox')); }} /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css" /> <section id="main" className="container"> <blockquote id="tag-filter" className="filter"> <div id="tags"></div> </blockquote> {isLoading && ( <div className="loading-container"> <div className="loading-spinner"></div> {/* <p>Loading gallery...</p> */} </div> )} <div id="memos_gallery" className="memos_gallery"> <div id='gallery-photos' className="gallery-photos"> <div className="page"></div> </div> </div> {/* <div id="back-to-top" style={{display: 'none'}}> <i className="fas fa-arrow-up"></i> </div> */} </section> </> ); } export default MemosGallery
然后在hexo-change主题目录下index文件中,引入对应的组件
image.pngjsximport MemosGallery from './components/MemosGallery'
然后在themes\hexo-change\index.js文件中的文章详情或者其他位置增加代码:
jsx/** * Memos 画册 * @param {*} props * @returns */ const LayoutMemosGallery = props => { const memoPageInfo = { id: "9e6c78642def47bcbabe35f5263076390", // 固定ID,确保唯一性 type: "MemosGallery", title: "我的画廊", }; return ( <div className="w-full lg:hover:shadow rounded-md lg:rounded-md lg:px-2 lg:py-4 article"> <div id="article-wrapper" className="overflow-x-auto flex-grow mx-auto md:w-full px-3 font-serif"> <article itemScope itemType="https://schema.org/Movie" className="subpixel-antialiased overflow-y-hidden overflow-x-hidden" > {/* Notion文章主体 */} <section className='justify-center mx-auto max-w-2xl lg:max-w-full'> <MemosGallery {...props}/> </section> </article> <div className='pt-4 border-dashed'></div> {/* 评论互动 */} <div className="duration-200 overflow-x-auto px-3"> <Comment frontMatter={memoPageInfo} /> </div> </div> </div>) }
在themes\hexo-change\index.js文件底部暴露LayoutMemosGallery
image.png引入静态资源文件
由于MemosGallery组件中引入了外部资源,如一些外部的css和js样式来实现说说页的样式和动态加载,需要将对应的这些文件分别放入public文件夹下。
下载地址:https://www.jianguoyun.com/p/Df-IzQsQg7KQDRjdiO8FIAA
public\js\gallery\data.js
public\js\gallery\imgStatus.min.js
public\js\gallery\lately.min.js
public\js\gallery\gallery.js
jsxconsole.log( "\n %c MemosGallery v1.0.2 %c https://i.yct.ee/ \n", "color: #fadfa3; background: #030307; padding:5px 0;", "background: #fadfa3; padding:5px 0;" ); // 等待 jQuery 和配置加载完成 function initGallery() { if (typeof jQuery === 'undefined') { setTimeout(initGallery, 100); return; } if (!window.memosGalleryConfig) { setTimeout(initGallery, 100); return; } $(document).ready(function() { const memosGalleryObj = window.memosGalleryConfig; photos(memosGalleryObj); //memoss获取的是一个div盒子 if (typeof (memos_gallery) !== "undefined") { //配置对象通常需要保留所有值,包括 falsy 值 //一次性合并所有属性,性能通常更好 Object.assign(memosGalleryObj, memos_gallery); } $(".arrow").click(function(){ $(".bg").remove(); $(".text").remove(); $(window).scroll(); }) $(window).scroll(function () { var scrollTop = $(window).scrollTop(); if (scrollTop > 1000) { $("#back-to-top").fadeIn(); } else { $("#back-to-top").fadeOut(); } }); $("#back-to-top").click(function () { $("html, body").animate({ scrollTop: 0 }, 800); return false; }); }); } initGallery(); function photos(memosGalleryObj) { const urlStr = `creator=='users/${memosGalleryObj.creatorId}'&&visibilities==['${memosGalleryObj.public}']&&tag_search==['${memosGalleryObj.tag}']`; const url = memosGalleryObj.urlHost+"/api/v1/memos?filter="+encodeURIComponent(urlStr); fetch(url) .then((res) => { if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); } return res.json(); }) .then((data) => { if (!data || !data.memos) { throw new Error('Invalid data format: missing memos property'); } let html = ""; const imgs = data.memos.reduce((acc, item) => { const matches = item.content.match(/\!\[.*?\]\(.*?\)/g) || []; return acc.concat(matches); }, []); html = imgs.map(item => { const img = item.replace(/!\[.*?\]\((.*?)\)/g, "$1"); const tat = item.replace(/!\[(.*?)\]\(.*?\)/g, "$1"); const [time, title] = tat.includes(" ") ? tat.split(" ") : [null, tat]; return ` <div class="gallery-photo"> <a href="${img}" data-fancybox="gallery" class="fancybox" data-thumb="${img}"> <img src="${img}" loading="lazy" decoding="async" onload="this.classList.add('loaded')" onerror="this.src='/svg/gallery.svg'" > ${title ? `<span class="photo-title">${title}</span>` : ''} ${time ? `<span class="photo-time">${time}</span>` : ''} </a> </div> `; }).join(''); const pageElement = document.querySelector(".gallery-photos .page"); if (pageElement) { pageElement.innerHTML = html; // 确保 jQuery 和 FancyBox 都已加载 if (typeof jQuery !== 'undefined' && typeof jQuery.fn.fancybox !== 'undefined') { // 初始化 FancyBox $('[data-fancybox="gallery"]').fancybox({ buttons: [ "zoom", "slideShow", "fullScreen", "download", "thumbs", "close" ], loop: true, protect: true, wheel: true, transitionEffect: "slide", toolbar: true, hash: false, beforeShow: function(instance, current) { $(document).on('wheel', function(e) { if (e.originalEvent.deltaY > 0) { instance.next(); } else { instance.previous(); } }); }, afterClose: function() { $(document).off('wheel'); } }); } else { console.error('FancyBox not loaded properly'); } window.Lately && Lately.init({ target: ".photo-time" }); memosGalleryObj.onLoadComplete && memosGalleryObj.onLoadComplete(); } }) .catch(error => { console.error('Error loading gallery:', error); const errorMessage = error.message || 'Unknown error occurred'; const pageElement = document.querySelector(".gallery-photos .page"); if (pageElement) { pageElement.innerHTML = ` <div class="error-message"> <h3>Failed to load gallery:</h3> <p>${errorMessage}</p> <p>Please check your network connection and configuration.</p> </div>`; } memosGalleryObj.onLoadError && memosGalleryObj.onLoadError(error); }); } // 添加滚动时的图片懒加载处理 $(window).on('scroll', function() { $('.photo-img').each(function() { if ($(this).offset().top < $(window).scrollTop() + $(window).height()) { $(this).attr('src', $(this).attr('data-src')); } }); });
image.pngimage.pngpublic\css\MemosGallery.css
jsx/* 加载动画样式 */ .loading-container { text-align: center; padding: 2rem; } .loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* 瀑布流布局 */ .gallery-photos { column-count: 3; margin: 0 auto; column-gap: 15px; margin-top: 16px; } .gallery-photo { break-inside: avoid; margin-bottom: 20px; } .gallery-photo img { display: block; width: 100%; border-radius: 9px; border: 3px solid var(--vp-c-brand-lighter); box-shadow: 0 10px 50px #5f2f1182; filter: sepia(30%) saturate(30%) hue-rotate(5deg); transition: transform 0.5s; background: url('/svg/load.svg') center center no-repeat; background-size: 50px 50px; } .gallery-photo img.loaded { background: none; } .gallery-photo img:hover { transform: rotate(0deg) scale(0.99); filter: none; } /* 标题和时间样式 */ .gallery-photo span.photo-title, .gallery-photo span.photo-time { max-width: calc(100% - 7px); line-height: 1.8; position: absolute; left: 10px; font-size: 14px; background: rgba(0, 0, 0, 0.3); padding: 0px 8px; color: #fff; animation: fadeIn 1s; font-family: "LXGW WenKai Screen", sans-serif; } .gallery-photo span.photo-title { bottom: 30px; border-radius: 0 8px 0 8px; } .gallery-photo span.photo-time { top: 10px; border-radius: 8px 0 8px 0; } /* 响应式布局 */ @media screen and (max-width: 768px) { .gallery-photos { column-count: 1; } .gallery-photo span.photo-time { display: none; } } @media screen and (min-width: 768px) and (max-width: 1000px) { .gallery-photos { column-count: 2; } } /* 返回顶部按钮 */ #back-to-top { position: fixed; bottom: 20px; right: 20px; cursor: pointer; display: none; padding: 10px; background: rgba(0,0,0,0.5); color: white; border-radius: 50%; transition: all 0.3s ease; } /* 动画效果 */ @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } }
在Notion Blog Database新建Gallery菜单
在notion模板中修改:
增加gallery路径和对应参数
image.png图标:
jsxfa fa-picture-o
效果图:

📎 参考文章
memosv0.23.0 api接口有改动,旧版本的接口有所不同
其中过滤条件很其他,跟通常传参有区别
const urlStr = `creator=='users/1'&&visibilities==['PUBLIC']&&tag_search==['images}']`
const url = 'https://memos.xxx.top'+"/api/v1/memos?filter="+encodeURIComponent(urlStr);