使用
next.js
的nextra
搭建博客失败后,进而尝试next examples 中的 [blog-starter] 搭建,顺便看了遍代码。
原理:博客页面框架需要前端搭建,使用next.js的getStaticProps
实现ssr.
项目的主要依赖,如下所示:
//package.json
{
...
"dependencies": {
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remark": "^14.0.2",
"remark-html": "^15.0.1",
"typescript": "^4.7.4",
"classnames": "^2.3.1",
"date-fns": "^2.28.0",
"gray-matter": "^4.0.3"
}
"devDependencies": {
"@types/node": "^18.0.3",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@autoprefixer": "^10.4.7",
"@postcss": "^8.4.14",
"@tailwindcss": "^3.1.4"
}
}
执行 npm install
,安装所需框架后。在Next.js中是约定式路由,pages 文件夹下的文件均是路由组件,因此在根目录(与 package.json 同级的目录)新建 pages
文件夹,然后新建index.tsx
文件(本项目支持TypeScript)。这样就相当于应用的'/'路由指向'index.tsx'文件。
我们在首页列出所有的博客,为了生成静态页面,因此使用getStaticProps
来获取页面所有数据,这个页面会在应用构建时生成index.html
文件。
那页面首页数据是所有的博客数据,我们的博客放在_posts
文件下(对于_
是约定式前缀),读取 _post
下的所有文件,需要一个函数,因此新建一个lib
文件夹(同样在根目录),新建文件 api.ts
。
首先引入node
的模块fs
与 path
。
执行 process.cwd() 得到的路径,是指向 node 应用根目录的,与
__dirname
不同,后者指向文件当前路径。__dirname
在不同文件里,得到的不同的值,而process.cwd()
在应用的任何文件任何位置,得到的值都是一致的。
//lib/api.ts
import fs from 'fs''
import { join } from 'path'
const postsDirectory = join(process.cwd(), '_posts')
添加 getPostSlugs
函数,fs.readdirSync
是读取文件夹,Sync
代表同步执行。
export function getPostSlugs() {
return fs.readdirSync(postsDirectory)
}
export function getAllPosts(fields: string[] = []) {
const slugs = getPostSlugs()
...
}
异步执行示例:
export function async getPostSlugs() {
return await fs.readdir(postsDirectory)
}
export function getAllPosts(fields: string[] = []) {
const slugs = await getPostSlugs()
}
接下来获取单个博客文件的数据,使用 gray-matter
库。
import matter from 'gray-matter'
这是一款可以解析文件的库,据我所知,几乎博客站点都会用到它来做文件的解析。官方示例:
---
title: Hello
slug: home
---
Hello world!
转换的数据对象:
{
content: 'Hello world!
',
data: {
title: 'Hello',
slug: 'home'
}
}
获取数据的函数 getPostBySlug
:
export function getPostBySlug(slug: string, fields: string[] = []) {
...//见接下来的代码
}
使用 path
模块的 join
得到文件路径,
const realSlug = slug.replace(/.md$/, '')
const fullPath = join(postsDirectory, `${realSlug}.md`)
使用 fs
模块的 readFileSync
得到文件内容,
const fileContents = fs.readFileSync(fullPath, 'utf8')
使用安装(执行 npm install )并引用(执行 import )的 gray
模块,
const { data, content } = matter(fileContents)
type Items = {
[key: string]: string
}
const items: Items = {}
//确保导出最少的数据
fields.forEach((field) => {
if (field === 'slug') {
items[field] = realSlug
}
if (field === 'content') {
items[field] = content
}
if (typeof data[field] !== 'undefined') {
items[field] = data[field]
}
})
return items
以上就完成了单个博客文件的读取。
在getAllPosts
中对每一个博客文件执行 getPostBySlug
,代码如下:
export function getAllPosts(fields: string[] = []) {
const slugs = getPostSlugs()
const posts = slugs.map((slug) => getPostBySlug(slug, fields)).sort((post1, post2) => (post1.date > post2.date ? -1 " 1))
return posts
}
这样博客数据我们都读取完成了,接下来我们需要在首页的getStaticProps
中添加代码:
//pages/index.tsx
import { getAllPosts } from '../lib/api';
export const getStaticProps = async () => {
const allPosts = getAllPosts([
'title',
'date',
'slug',
'author',
'coverImage',
'excerpt',
])
return {
props: { allPosts },
}
}
然后首页的编写就类似于React中的无状态组件(stateless)了。
Head 组件是从 next/head 导出的,其它组件是 components 下的组件。
//pages/index.tsx
import Container from '../components/container'
import MoreStories from '../components/more-stories'
import HeroPost from '../components/hero-post'
import Intro from '../components/intro'
import Layout from '../components/layout'
import { getAllPosts } from '../lib/api'
import Head from 'next/head'
import { CMS_NAME } from '../lib/constants'
import Post from '../interfaces/post'
type Props = {
allPosts: Post[]
}
export default function Index({ allPosts }: Props) {
const heroPost = allPosts[0]
const morePosts = allPosts.slice(1)
return (
<>
Next.js Blog Example with {CMS_NAME}
{heroPost && (
)}
>
)
}
同时,项目使用的是tailwind
CSS 框架,项目根目录,新建tailwind.config.js
,
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./components/**/*.tsx', './pages/**/*.tsx'],
theme: {
extend: {
colors: {
'accent-1': '#FAFAFA',
'accent-2': '#EAEAEA',
'accent-7': '#333',
success: '#0070f3',
cyan: '#79FFE1',
},
spacing: {
28: '7rem',
},
letterSpacing: {
tighter: '-.04em',
},
lineHeight: {
tight: 1.2,
},
fontSize: {
'5xl': '2.5rem',
'6xl': '2.75rem',
'7xl': '4.5rem',
'8xl': '6.25rem',
},
boxShadow: {
sm: '0 5px 10px rgba(0, 0, 0, 0.12)',
md: '0 8px 30px rgba(0, 0, 0, 0.12)',
},
},
},
plugins: [],
}
与postcss.config.js
:
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
tailwind
CSS框架依赖postcss
库与autoprefixer
库,前面在package.json
文件中已经声明在devDependencies
了,会执行安装。
配置文件是我们需要额外添加的。
同样,根目录新建components
文件夹,放置应用中可以重用的组件:
//Layout.tsx
import Alert from './alert'
import Footer from './footer'
import Meta from './meta'
type Props = {
preview?: boolean
children: React.ReactNode
}
const Layout = ({ preview, children }: Props) => {
return (
<>
{children}
>
)
}
export default Layout
//Container.tsx
type Props = {
children?: React.ReactNode
}
const Container = ({ children }: Props) => {
return {children}
}
export default Container
//Intro.tsx
import { CMS_NAME } from '../lib/constants'
const Intro = () => {
return (
Blog.
A statically generated blog example using{' '}
Next.js
{' '}
and {CMS_NAME}.
)
}
export default Intro
//Meta.tsx
import Head from 'next/head'
import { CMS_NAME, HOME_OG_IMAGE_URL } from '../lib/constants'
const Meta = () => {
return (
)
}
export default Meta
//Footer.tsx
import Container from './container'
import { EXAMPLE_PATH } from '../lib/constants'
const Footer = () => {
return (
)
}
export default Footer
//Alter.tsx
import Container from './container'
import cn from 'classnames'
import { EXAMPLE_PATH } from '../lib/constants'
type Props = {
preview?: boolean
}
const Alert = ({ preview }: Props) => {
return (
{preview ? (
<>
This page is a preview.{' '}
Click here
{' '}
to exit preview mode.
>
) : (
<>
The source code for this blog is{' '}
available on GitHub
.
>
)}
)
}
export default Alert
//Avatar.tsx
type Props = {
name: string
picture: string
}
const Avatar = ({ name, picture }: Props) => {
return (
{name}
)
}
export default Avatar
import PostPreview from './post-preview'
import type Post from '../interfaces/post'
type Props = {
posts: Post[]
}
const MoreStories = ({ posts }: Props) => {
return (
More Stories
{posts.map((post) => (
))}
)
}
export default MoreStories
import Avatar from './avatar'
import DateFormatter from './date-formatter'
import CoverImage from './cover-image'
import Link from 'next/link'
import type Author from '../interfaces/author'
type Props = {
title: string
coverImage: string
date: string
excerpt: string
author: Author
slug: string
}
const PostPreview = ({
title,
coverImage,
date,
excerpt,
author,
slug,
}: Props) => {
return (
{title}
{excerpt}
)
}
export default PostPreview
//CoverImage.tsx
import cn from 'classnames'
import Link from 'next/link'
type Props = {
title: string
src: string
slug?: string
}
const CoverImage = ({ title, src, slug }: Props) => {
const image = (
)
return (
{slug ? (
{image}
) : (
image
)}
)
}
export default CoverImage
更多组件代码可至GitHub仓库。