diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..971ce54 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,13 @@ +export {} + +declare global { + namespace NodeJS { + interface ProcessEnv { + NEXT_PUBLIC_WORK_EXP: string + NEXT_PUBLIC_ALGOLIA_APP_ID: string + NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY: string + NEXT_PUBLIC_ALGOLIA_INDEX_NAME: string + ALGOLIA_SEARCH_ADMIN_KEY: string + } + } +} diff --git a/package.json b/package.json index f6bcfdd..cb05cdd 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { + "algolia": "node ./scripts/build-algolia-search.js", "dev": "next dev -p 3470", "build": "next build", "postbuild": "ts-node --esm scripts/generateFeed.ts", @@ -13,8 +14,11 @@ "dependencies": { "@kxxoling/react-github": "0.2.0", "@leafac/rehype-shiki": "^2.2.1", + "@docsearch/css": "^3.3.3", + "@docsearch/react": "3", "@tabler/icons-react": "^2.15.0", "@vercel/analytics": "^1.0.1", + "algoliasearch": "^4.17.0", "disqus-react": "^1.1.5", "feed": "^4.2.2", "framer-motion": "^10.12.16", @@ -31,6 +35,7 @@ "rehype-slug": "^5.1.0", "remark-gfm": "^3.0.1", "shiki": "^0.14.2", + "remove-markdown": "^0.5.0", "styled-components": "^5.3.9", "twin.macro": "^3.3.0" }, @@ -44,6 +49,7 @@ "@typescript-eslint/parser": "^5.58.0", "autoprefixer": "^10.4.14", "babel-plugin-styled-components": "^2.1.1", + "dotenv": "^16.0.3", "eslint": "8.38.0", "eslint-config-next": "13.3.0", "eslint-config-prettier": "^8.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 543c68f..6a29ce7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6 +1,12 @@ lockfileVersion: '6.0' dependencies: + '@docsearch/css': + specifier: ^3.3.3 + version: 3.3.3 + '@docsearch/react': + specifier: '3' + version: 3.0.0(@algolia/client-search@4.17.0)(@types/react@18.0.35)(react-dom@18.2.0)(react@18.2.0) '@kxxoling/react-github': specifier: 0.2.0 version: 0.2.0(react-dom@18.2.0)(react@18.2.0) @@ -13,6 +19,9 @@ dependencies: '@vercel/analytics': specifier: ^1.0.1 version: 1.0.1 + algoliasearch: + specifier: ^4.17.0 + version: 4.17.0 disqus-react: specifier: ^1.1.5 version: 1.1.5(react-dom@18.2.0)(react@18.2.0) @@ -58,6 +67,9 @@ dependencies: remark-gfm: specifier: ^3.0.1 version: 3.0.1 + remove-markdown: + specifier: ^0.5.0 + version: 0.5.0 shiki: specifier: ^0.14.2 version: 0.14.2 @@ -96,6 +108,9 @@ devDependencies: babel-plugin-styled-components: specifier: ^2.1.1 version: 2.1.1(styled-components@5.3.9) + dotenv: + specifier: ^16.0.3 + version: 16.0.3 eslint: specifier: 8.38.0 version: 8.38.0 @@ -147,6 +162,117 @@ devDependencies: packages: + /@algolia/autocomplete-core@1.5.2: + resolution: {integrity: sha512-DY0bhyczFSS1b/CqJlTE/nQRtnTAHl6IemIkBy0nEWnhDzRDdtdx4p5Uuk3vwAFxwEEgi1WqKwgSSMx6DpNL4A==} + dependencies: + '@algolia/autocomplete-shared': 1.5.2 + dev: false + + /@algolia/autocomplete-preset-algolia@1.5.2(@algolia/client-search@4.17.0)(algoliasearch@4.17.0): + resolution: {integrity: sha512-3MRYnYQFJyovANzSX2CToS6/5cfVjbLLqFsZTKcvF3abhQzxbqwwaMBlJtt620uBUOeMzhdfasKhCc40+RHiZw==} + peerDependencies: + '@algolia/client-search': ^4.9.1 + algoliasearch: ^4.9.1 + dependencies: + '@algolia/autocomplete-shared': 1.5.2 + '@algolia/client-search': 4.17.0 + algoliasearch: 4.17.0 + dev: false + + /@algolia/autocomplete-shared@1.5.2: + resolution: {integrity: sha512-ylQAYv5H0YKMfHgVWX0j0NmL8XBcAeeeVQUmppnnMtzDbDnca6CzhKj3Q8eF9cHCgcdTDdb5K+3aKyGWA0obug==} + dev: false + + /@algolia/cache-browser-local-storage@4.17.0: + resolution: {integrity: sha512-myRSRZDIMYB8uCkO+lb40YKiYHi0fjpWRtJpR/dgkaiBlSD0plRyB6lLOh1XIfmMcSeBOqDE7y9m8xZMrXYfyQ==} + dependencies: + '@algolia/cache-common': 4.17.0 + dev: false + + /@algolia/cache-common@4.17.0: + resolution: {integrity: sha512-g8mXzkrcUBIPZaulAuqE7xyHhLAYAcF2xSch7d9dABheybaU3U91LjBX6eJTEB7XVhEsgK4Smi27vWtAJRhIKQ==} + dev: false + + /@algolia/cache-in-memory@4.17.0: + resolution: {integrity: sha512-PT32ciC/xI8z919d0oknWVu3kMfTlhQn3MKxDln3pkn+yA7F7xrxSALysxquv+MhFfNAcrtQ/oVvQVBAQSHtdw==} + dependencies: + '@algolia/cache-common': 4.17.0 + dev: false + + /@algolia/client-account@4.17.0: + resolution: {integrity: sha512-sSEHx9GA6m7wrlsSMNBGfyzlIfDT2fkz2u7jqfCCd6JEEwmxt8emGmxAU/0qBfbhRSuGvzojoLJlr83BSZAKjA==} + dependencies: + '@algolia/client-common': 4.17.0 + '@algolia/client-search': 4.17.0 + '@algolia/transporter': 4.17.0 + dev: false + + /@algolia/client-analytics@4.17.0: + resolution: {integrity: sha512-84ooP8QA3mQ958hQ9wozk7hFUbAO+81CX1CjAuerxBqjKIInh1fOhXKTaku05O/GHBvcfExpPLIQuSuLYziBXQ==} + dependencies: + '@algolia/client-common': 4.17.0 + '@algolia/client-search': 4.17.0 + '@algolia/requester-common': 4.17.0 + '@algolia/transporter': 4.17.0 + dev: false + + /@algolia/client-common@4.17.0: + resolution: {integrity: sha512-jHMks0ZFicf8nRDn6ma8DNNsdwGgP/NKiAAL9z6rS7CymJ7L0+QqTJl3rYxRW7TmBhsUH40wqzmrG6aMIN/DrQ==} + dependencies: + '@algolia/requester-common': 4.17.0 + '@algolia/transporter': 4.17.0 + dev: false + + /@algolia/client-personalization@4.17.0: + resolution: {integrity: sha512-RMzN4dZLIta1YuwT7QC9o+OeGz2cU6eTOlGNE/6RcUBLOU3l9tkCOdln5dPE2jp8GZXPl2yk54b2nSs1+pAjqw==} + dependencies: + '@algolia/client-common': 4.17.0 + '@algolia/requester-common': 4.17.0 + '@algolia/transporter': 4.17.0 + dev: false + + /@algolia/client-search@4.17.0: + resolution: {integrity: sha512-x4P2wKrrRIXszT8gb7eWsMHNNHAJs0wE7/uqbufm4tZenAp+hwU/hq5KVsY50v+PfwM0LcDwwn/1DroujsTFoA==} + dependencies: + '@algolia/client-common': 4.17.0 + '@algolia/requester-common': 4.17.0 + '@algolia/transporter': 4.17.0 + dev: false + + /@algolia/logger-common@4.17.0: + resolution: {integrity: sha512-DGuoZqpTmIKJFDeyAJ7M8E/LOenIjWiOsg1XJ1OqAU/eofp49JfqXxbfgctlVZVmDABIyOz8LqEoJ6ZP4DTyvw==} + dev: false + + /@algolia/logger-console@4.17.0: + resolution: {integrity: sha512-zMPvugQV/gbXUvWBCzihw6m7oxIKp48w37QBIUu/XqQQfxhjoOE9xyfJr1KldUt5FrYOKZJVsJaEjTsu+bIgQg==} + dependencies: + '@algolia/logger-common': 4.17.0 + dev: false + + /@algolia/requester-browser-xhr@4.17.0: + resolution: {integrity: sha512-aSOX/smauyTkP21Pf52pJ1O2LmNFJ5iHRIzEeTh0mwBeADO4GdG94cAWDILFA9rNblq/nK3EDh3+UyHHjplZ1A==} + dependencies: + '@algolia/requester-common': 4.17.0 + dev: false + + /@algolia/requester-common@4.17.0: + resolution: {integrity: sha512-XJjmWFEUlHu0ijvcHBoixuXfEoiRUdyzQM6YwTuB8usJNIgShua8ouFlRWF8iCeag0vZZiUm4S2WCVBPkdxFgg==} + dev: false + + /@algolia/requester-node-http@4.17.0: + resolution: {integrity: sha512-bpb/wDA1aC6WxxM8v7TsFspB7yBN3nqCGs2H1OADolQR/hiAIjAxusbuMxVbRFOdaUvAIqioIIkWvZdpYNIn8w==} + dependencies: + '@algolia/requester-common': 4.17.0 + dev: false + + /@algolia/transporter@4.17.0: + resolution: {integrity: sha512-6xL6H6fe+Fi0AEP3ziSgC+G04RK37iRb4uUUqVAH9WPYFI8g+LYFq6iv5HS8Cbuc5TTut+Bwj6G+dh/asdb9uA==} + dependencies: + '@algolia/cache-common': 4.17.0 + '@algolia/logger-common': 4.17.0 + '@algolia/requester-common': 4.17.0 + dev: false + /@babel/code-frame@7.21.4: resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} engines: {node: '>=6.9.0'} @@ -265,6 +391,32 @@ packages: dependencies: '@jridgewell/trace-mapping': 0.3.9 + /@docsearch/css@3.0.0: + resolution: {integrity: sha512-1kkV7tkAsiuEd0shunYRByKJe3xQDG2q7wYg24SOw1nV9/2lwEd4WrUYRJC/ukGTl2/kHeFxsaUvtiOy0y6fFA==} + dev: false + + /@docsearch/css@3.3.3: + resolution: {integrity: sha512-6SCwI7P8ao+se1TUsdZ7B4XzL+gqeQZnBc+2EONZlcVa0dVrk0NjETxozFKgMv0eEGH8QzP1fkN+A1rH61l4eg==} + dev: false + + /@docsearch/react@3.0.0(@algolia/client-search@4.17.0)(@types/react@18.0.35)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-yhMacqS6TVQYoBh/o603zszIb5Bl8MIXuOc6Vy617I74pirisDzzcNh0NEaYQt50fVVR3khUbeEhUEWEWipESg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 18.0.0' + react: '>= 16.8.0 < 18.0.0' + react-dom: '>= 16.8.0 < 18.0.0' + dependencies: + '@algolia/autocomplete-core': 1.5.2 + '@algolia/autocomplete-preset-algolia': 1.5.2(@algolia/client-search@4.17.0)(algoliasearch@4.17.0) + '@docsearch/css': 3.0.0 + '@types/react': 18.0.35 + algoliasearch: 4.17.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@algolia/client-search' + dev: false + /@emotion/is-prop-valid@0.8.8: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} requiresBuild: true @@ -999,6 +1151,25 @@ packages: uri-js: 4.4.1 dev: true + /algoliasearch@4.17.0: + resolution: {integrity: sha512-JMRh2Mw6sEnVMiz6+APsi7lx9a2jiDFF+WUtANaUVCv6uSU9UOLdo5h9K3pdP6frRRybaM2fX8b1u0nqICS9aA==} + dependencies: + '@algolia/cache-browser-local-storage': 4.17.0 + '@algolia/cache-common': 4.17.0 + '@algolia/cache-in-memory': 4.17.0 + '@algolia/client-account': 4.17.0 + '@algolia/client-analytics': 4.17.0 + '@algolia/client-common': 4.17.0 + '@algolia/client-personalization': 4.17.0 + '@algolia/client-search': 4.17.0 + '@algolia/logger-common': 4.17.0 + '@algolia/logger-console': 4.17.0 + '@algolia/requester-browser-xhr': 4.17.0 + '@algolia/requester-common': 4.17.0 + '@algolia/requester-node-http': 4.17.0 + '@algolia/transporter': 4.17.0 + dev: false + /ansi-regex@2.1.1: resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} engines: {node: '>=0.10.0'} @@ -1602,6 +1773,11 @@ packages: esutils: 2.0.3 dev: true + /dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} + dev: true + /electron-to-chromium@1.4.361: resolution: {integrity: sha512-VocVwjPp05HUXzf3xmL0boRn5b0iyqC7amtDww84Jb1QJNPBc7F69gJyEeXRoriLBC4a5pSyckdllrXAg4mmRA==} dev: true @@ -4473,6 +4649,10 @@ packages: unified: 10.1.2 dev: false + /remove-markdown@0.5.0: + resolution: {integrity: sha512-x917M80K97K5IN1L8lUvFehsfhR8cYjGQ/yAMRI9E7JIKivtl5Emo5iD13DhMr+VojzMCiYk8V2byNPwT/oapg==} + dev: false + /require-relative@0.8.7: resolution: {integrity: sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==} dev: true diff --git a/scripts/build-algolia-search.js b/scripts/build-algolia-search.js new file mode 100644 index 0000000..02c8d5b --- /dev/null +++ b/scripts/build-algolia-search.js @@ -0,0 +1,94 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const fs = require('fs') +const path = require('path') + +const algoliasearch = require('algoliasearch') +// eslint-disable-next-line node/no-unpublished-require +const dotenv = require('dotenv') +const matter = require('gray-matter') +const removeMd = require('remove-markdown') + +const CONTENT_PATH = path.join(process.cwd(), 'posts/') + +async function getFiles(dir) { + const dirents = await fs.promises.readdir(dir, { withFileTypes: true }) + // @ts-ignore + const files = await Promise.all( + dirents + .filter((dirent) => !dirent.name.startsWith('.')) + .map((dirent) => { + return dirent.isDirectory() + ? getFiles(path.join(dir, dirent.name)) + : path.join(dir, dirent.name) + }) + ) + return Array.prototype.concat(...files) +} +const contentFilePaths = await getFiles(CONTENT_PATH) + +async function getAllBlogPosts() { + const articles = contentFilePaths.map((filePath) => { + const source = fs.readFileSync(filePath) + const { content, data } = matter(source) + + return { + content, + data, + filePath: filePath.replace(CONTENT_PATH, ''), + } + }) + + return articles +} + +function transformPostsToSearchObjects(articles) { + const transformed = articles.map((article) => { + return { + objectID: article.filePath, + title: + article.data.title || + article.content.split('\n')[0].replace('#', '').trim(), + description: article.data.description, + slug: article.filePath.replace(/\.(md|mdx)$/, ''), + tagsCollection: { tags: article.data.tags }, + date: article.data.createdAt || article.data.updatedAt, + type: 'article', + content: removeMd(article.content).slice(0, 3000), + } + }) + + return transformed +} + +// @ts-ignore @typescript-eslint/no-extra-semi +// eslint-disable-next-line @typescript-eslint/no-extra-semi +;(async function () { + dotenv.config() + + try { + const articles = await getAllBlogPosts() + + const transformed = transformPostsToSearchObjects(articles) + + const client = algoliasearch( + process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, + process.env.ALGOLIA_SEARCH_ADMIN_KEY + ) + + // initialize the index with your index name + const index = client.initIndex('blog.windrunner.me') + + // add the data to the index + const algoliaResponse = await index.saveObjects(transformed) + + console.log( + `Successfully added ${ + algoliaResponse.objectIDs.length + } records to Algolia search! Object IDs:\n${algoliaResponse.objectIDs.join( + '\n' + )}` + ) + } catch (err) { + console.error(err) + } +})() diff --git a/utils/feed.ts b/utils/feed.ts index 526662b..bc07c78 100644 --- a/utils/feed.ts +++ b/utils/feed.ts @@ -4,8 +4,6 @@ import path from 'path' import { Feed } from 'feed' import matter from 'gray-matter' -import { serializeDatetime } from './datetime' - export async function getSortedPosts() { // @ts-ignore async function getFiles(dir) { @@ -55,8 +53,6 @@ export async function getSortedPosts() { // 对于直接修改后缀名没有设置 title 的文章,默认使用首行大标题作为 title frontMatter.title = fileContent.split('\n')[0].replace('#', '').trim() } - frontMatter.createdAt = serializeDatetime(frontMatter.createdAt) - frontMatter.updatedAt = serializeDatetime(frontMatter.updatedAt) return { frontMatter, slug: slug.replace(/\/(README|index)$/, '') } }) .sort((a: any, b: any) => {