Skip to content

Commit

Permalink
Merge pull request #1336 from tangly1024/feat/algolia
Browse files Browse the repository at this point in the history
Feat/algolia
  • Loading branch information
tangly1024 committed Jul 24, 2023
2 parents 06dca2f + cc59e5c commit a110f17
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 57 deletions.
6 changes: 6 additions & 0 deletions blog.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ const BLOG = {
POSTS_PER_PAGE: 12, // post counts per page
POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制

ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY,不要暴露在代码中,在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_SEARCH_ONLY_APP_KEY: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY
ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库
ALGOLIA_RECREATE_DATA: process.env.ALGOLIA_RECREATE_DATA || process.env.npm_lifecycle_event === 'build', // 为true时重新构建索引数据; 默认在build时会构建

PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量,0为不限制
PREVIEW_TAG_COUNT: 16, // 首页最多展示的标签数量,0为不限制

Expand Down
92 changes: 92 additions & 0 deletions components/AlgoliaSearchModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useState, useImperativeHandle } from 'react'
import BLOG from '@/blog.config'
import algoliasearch from 'algoliasearch'
import replaceSearchResult from '@/components/Mark'

/**
* 结合 Algolia 实现的弹出式搜索框
* 打开方式 cRef.current.openSearch()
* https://www.algolia.com/doc/api-reference/search-api-parameters/
*/
export default function AlgoliaSearchModal({ cRef }) {
const [searchResults, setSearchResults] = useState([])
const [isModalOpen, setIsModalOpen] = useState(false)
/**
* 对外暴露方法
*/
useImperativeHandle(cRef, () => {
return {
openSearch: () => {
setIsModalOpen(true)
}
}
})

if (!BLOG.ALGOLIA_APP_ID) {
return <></>
}

const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_SEARCH_ONLY_APP_KEY)
const index = client.initIndex(BLOG.ALGOLIA_INDEX)

const handleSearch = async (query) => {
try {
const res = await index.search(query)
console.log(res)
const { hits } = res
setSearchResults(hits)
const doms = document.getElementById('search-wrapper').getElementsByClassName('replace')
replaceSearchResult({
doms,
search: query,
target: {
element: 'span',
className: 'text-blue-600 border-b border-dashed'
}
})
} catch (error) {
console.error('Algolia search error:', error)
}
}

const closeModal = () => {
setIsModalOpen(false)
}

return (
<div id='search-wrapper' className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'} fixed h-screen w-screen left-0 top-0 flex items-center justify-center`}>
{/* 内容 */}
<div className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10'} flex flex-col justify-between w-full min-h-[10rem] max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}>

<div className='flex justify-between items-center'>
<div className='text-2xl text-blue-600 font-bold'>搜索</div>
<div><i class="text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600" onClick={closeModal} ></i></div>
</div>

<input type="text" placeholder="在这里输入搜索关键词..." onChange={(e) => handleSearch(e.target.value)}
className="bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md" />

{/* 标签组 */}
<div>

</div>

<ul>
{searchResults.map((result) => (
<li key={result.objectID} className="replace my-2">
<a href={`${BLOG.SUB_PATH}/${result.slug}`} className="font-bold hover:text-blue-600 ">
{result.title}
</a>
</li>
))}
</ul>

<div className='text-gray-600'><i class="fa-brands fa-algolia"></i> Algolia 提供搜索服务</div>
</div>

{/* 遮罩 */}
<div onClick={closeModal} className="z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism" />

</div>
)
}
42 changes: 42 additions & 0 deletions lib/algolia.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import BLOG from '@/blog.config'
import { getPageContentText } from '@/pages/search/[keyword]'
import algoliasearch from 'algoliasearch'

/**
* 生成全文索引
* @param {*} allPages
*/
const generateAlgoliaSearch = async({ allPages, force = false }) => {
allPages?.forEach(p => {
// 判断这篇文章是否需要重新创建索引
if (p && !p.password) {
uploadDataToAlgolia(p)
}
})
}

/**
* 上传数据
*/
const uploadDataToAlgolia = (post) => {
// Connect and authenticate with your Algolia app
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_ADMIN_APP_KEY)

// Create a new index and add a record
const index = client.initIndex(BLOG.ALGOLIA_INDEX)
const record = {
objectID: post.id,
title: post.title,
category: post.category,
tags: post.tags,
pageCover: post.pageCover,
slug: post.slug,
summary: post.summary,
content: getPageContentText(post, post.blockMap)
}
index.saveObject(record).wait().then(r => {
console.log('Algolia索引', r, record)
})
}

export { uploadDataToAlgolia, generateAlgoliaSearch }
4 changes: 2 additions & 2 deletions lib/notion/getNotionData.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,10 @@ export function getNavPages({ allPages }) {
const result = allNavPages.map(item => ({ id: item.id, title: item.title || '', category: item.category || null, tags: item.tags || null, summary: item.summary || null, slug: item.slug }))

const groupedArray = result.reduce((groups, item) => {
const categoryName = item.category ? item.category.join('/') : '' // 将category转换为字符串
const categoryName = item?.category ? item?.category : '' // 将category转换为字符串
const lastGroup = groups[groups.length - 1] // 获取最后一个分组

if (!lastGroup || lastGroup.category !== categoryName) { // 如果当前元素的category与上一个元素不同,则创建新分组
if (!lastGroup || lastGroup?.category !== categoryName) { // 如果当前元素的category与上一个元素不同,则创建新分组
groups.push({ category: categoryName, items: [] })
}

Expand Down
7 changes: 4 additions & 3 deletions lib/notion/getPageProperties.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ export default async function getPageProperties(id, block, schema, authToken, ta
})
}

// type\status是下拉选框 取数组第一个
properties.type = properties.type?.[0]
properties.status = properties.status?.[0]
// type\status\category 是单选下拉框 取数组第一个
properties.type = properties.type?.[0] || ''
properties.status = properties.status?.[0] || ''
properties.category = properties.category?.[0] || ''

// 映射值:用户个性化type和status字段的下拉框选项,在此映射回代码的英文标识
mapProperties(properties)
Expand Down
6 changes: 6 additions & 0 deletions lib/sitemap.xml.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export async function generateSitemapXml({ allPages }) {
console.warn('无法写入文件', error)
}
}

/**
* 生成站点地图
* @param {*} urls
* @returns
*/
function createSitemapXml(urls) {
let urlsXml = ''
urls.forEach(u => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@headlessui/react": "^1.7.15",
"@next/bundle-analyzer": "^12.1.1",
"@vercel/analytics": "^1.0.0",
"algoliasearch": "^4.18.0",
"animejs": "^3.2.1",
"aos": "^3.0.0-beta.6",
"axios": ">=0.21.1",
Expand Down
37 changes: 1 addition & 36 deletions pages/[prefix]/[slug].js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getPostBlocks } from '@/lib/notion'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { idToUuid } from 'notion-utils'
import { getNotion } from '@/lib/notion/getNotion'
import Slug from '.'
import Slug, { getRecommendPost } from '.'

/**
* 根据notion的slug访问页面
Expand Down Expand Up @@ -84,39 +84,4 @@ export async function getStaticProps({ params: { prefix, slug } }) {
}
}

/**
* 获取文章的关联推荐文章列表,目前根据标签关联性筛选
* @param post
* @param {*} allPosts
* @param {*} count
* @returns
*/
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
}

export default PrefixSlug
8 changes: 6 additions & 2 deletions pages/[prefix]/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
import { getLayoutByTheme } from '@/themes/theme'
import md5 from 'js-md5'
import { isBrowser } from '@/lib/utils'
import { uploadDataToAlgolia } from '@/lib/algolia'

/**
* 根据notion的slug访问页面
Expand Down Expand Up @@ -95,7 +96,6 @@ export async function getStaticPaths() {
}

export async function getStaticProps({ params: { prefix } }) {
// let fullSlug = slug.join('/')
let fullSlug = prefix
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
if (!fullSlug.endsWith('.html')) {
Expand Down Expand Up @@ -129,6 +129,10 @@ export async function getStaticProps({ params: { prefix } }) {
props.post.blockMap = await getPostBlocks(props.post.id, from)
}

if (BLOG.ALGOLIA_APP_ID && BLOG.ALGOLIA_APP_KEY) {
uploadDataToAlgolia(props?.post)
}

// 推荐关联文章处理
const allPosts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
if (allPosts && allPosts.length > 0) {
Expand Down Expand Up @@ -156,7 +160,7 @@ export async function getStaticProps({ params: { prefix } }) {
* @param {*} count
* @returns
*/
function getRecommendPost(post, allPosts, count = 6) {
export function getRecommendPost(post, allPosts, count = 6) {
let recommendPosts = []
const postIds = []
const currentTags = post?.tags || []
Expand Down
7 changes: 7 additions & 0 deletions pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { generateRobotsTxt } from '@/lib/robots.txt'

import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { generateAlgoliaSearch } from '@/lib/algolia'

/**
* 首页布局
* @param {*} props
Expand Down Expand Up @@ -61,6 +63,11 @@ export async function getStaticProps() {
generateRss(props?.latestPosts || [])
}

// 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build'
if (BLOG.ALGOLIA_APP_ID && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)) {
generateAlgoliaSearch({ allPages: props.allPages })
}

delete props.allPages

return {
Expand Down
25 changes: 15 additions & 10 deletions pages/search/[keyword]/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,7 @@ async function filterByMemCache(allPosts, keyword) {
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
const articleInfo = post.title + post.summary + tagContent + categoryContent
let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
let indexContent = [post.summary]
// 防止搜到加密文章的内容
if (page && page.block && !post.password) {
const contentIds = Object.keys(page.block)
contentIds.forEach(id => {
const properties = page?.block[id]?.value?.properties
indexContent = appendText(indexContent, properties, 'title')
indexContent = appendText(indexContent, properties, 'caption')
})
}
const indexContent = getPageContentText(post, page)
// console.log('全文搜索缓存', cacheKey, page != null)
post.results = []
let hitCount = 0
Expand All @@ -157,4 +148,18 @@ async function filterByMemCache(allPosts, keyword) {
return filterPosts
}

export function getPageContentText(post, pageBlockMap) {
let indexContent = []
// 防止搜到加密文章的内容
if (pageBlockMap && pageBlockMap.block && !post.password) {
const contentIds = Object.keys(pageBlockMap.block)
contentIds.forEach(id => {
const properties = pageBlockMap?.block[id]?.value?.properties
indexContent = appendText(indexContent, properties, 'title')
indexContent = appendText(indexContent, properties, 'caption')
})
}
return indexContent.join('')
}

export default Index
29 changes: 25 additions & 4 deletions themes/heo/components/SearchButton.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import { useRouter } from 'next/router'
import AlgoliaSearchModal from '@/components/AlgoliaSearchModal'
import { useRef } from 'react'

/**
* 搜索按钮
* @returns
*/
export default function SearchButton() {
const { locale } = useGlobal()
return <Link href="/search" title={locale.NAV.SEARCH} alt={locale.NAV.SEARCH} className='cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-full w-10 h-10 flex justify-center items-center duration-200 transition-all'>
<i title={locale.NAV.SEARCH} className="fa-solid fa-magnifying-glass"/>
</Link>
const router = useRouter()
const searchModal = useRef(null)

function handleSearch() {
if (BLOG.ALGOLIA_APP_ID) {
searchModal.current.openSearch()
} else {
router.push('/search')
}
}

return <>
<div onClick={handleSearch} title={locale.NAV.SEARCH} alt={locale.NAV.SEARCH} className='cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-full w-10 h-10 flex justify-center items-center duration-200 transition-all'>
<i title={locale.NAV.SEARCH} className="fa-solid fa-magnifying-glass" />
</div>
<AlgoliaSearchModal cRef={searchModal} />
</>
}

0 comments on commit a110f17

Please sign in to comment.