使用
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
---
<h1>Hello world!</h1>
转换的数据对象:
{
content: '<h1>Hello world!</h1>',
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 (
<>
<Layout>
<Head>
<title>Next.js Blog Example with {CMS_NAME}</title>
</Head>
<Container>
<Intro />
{heroPost && (
<HeroPost
title={heroPost.title}
coverImage={heroPost.coverImage}
data={heroPost.data}
anthor={heroPost.author}
slug={heroPost.slug}
excerpt={heroPost.excerpt}
/>
)}
</Container>
</Layout>
</>
)
}
同时,项目使用的是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 (
<>
<Meta />
<div className="min-h-screen">
<Alert preview={preview} />
<main>{children}</main>
</div>
<Footer />
</>
)
}
export default Layout
//Container.tsx
type Props = {
children?: React.ReactNode
}
const Container = ({ children }: Props) => {
return <div className="container mx-auto px-5">{children}</div>
}
export default Container
//Intro.tsx
import { CMS_NAME } from '../lib/constants'
const Intro = () => {
return (
<section className="flex-col md:flex-row flex items-center md:justify-between mt-16 mb-16 md:mb-12">
<h1 className="text-5xl md:text-8xl font-bold tracking-tighter leading-tight md:pr-8">
Blog.
</h1>
<h4 className="text-center md:text-left text-lg mt-5 md:pl-8">
A statically generated blog example using{' '}
<a
href="https://nextjs.org/"
className="underline hover:text-blue-600 duration-200 transition-colors"
>
Next.js
</a>{' '}
and {CMS_NAME}.
</h4>
</section>
)
}
export default Intro
//Meta.tsx
import Head from 'next/head'
import { CMS_NAME, HOME_OG_IMAGE_URL } from '../lib/constants'
const Meta = () => {
return (
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon/favicon-16x16.png"
/>
<link rel="manifest" href="/favicon/site.webmanifest" />
<link
rel="mask-icon"
href="/favicon/safari-pinned-tab.svg"
color="#000000"
/>
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
<meta name="theme-color" content="#000" />
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
<meta
name="description"
content={`A statically generated blog example using Next.js and ${CMS_NAME}.`}
/>
<meta property="og:image" content={HOME_OG_IMAGE_URL} />
</Head>
)
}
export default Meta
//Footer.tsx
import Container from './container'
import { EXAMPLE_PATH } from '../lib/constants'
const Footer = () => {
return (
<footer className="bg-neutral-50 border-t border-neutral-200">
<Container>
<div className="py-28 flex flex-col lg:flex-row items-center">
<h3 className="text-4xl lg:text-[2.5rem] font-bold tracking-tighter leading-tight text-center lg:text-left mb-10 lg:mb-0 lg:pr-4 lg:w-1/2">
Statically Generated with Next.js.
</h3>
<div className="flex flex-col lg:flex-row justify-center items-center lg:pl-4 lg:w-1/2">
<a
href="https://nextjs.org/docs/basic-features/pages"
className="mx-3 bg-black hover:bg-white hover:text-black border border-black text-white font-bold py-3 px-12 lg:px-8 duration-200 transition-colors mb-6 lg:mb-0"
>
Read Documentation
</a>
<a
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="mx-3 font-bold hover:underline"
>
View on GitHub
</a>
</div>
</div>
</Container>
</footer>
)
}
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 (
<div
className={cn('border-b', {
'bg-neutral-800 border-neutral-800 text-white': preview,
'bg-neutral-50 border-neutral-200': !preview,
})}
>
<Container>
<div className="py-2 text-center text-sm">
{preview ? (
<>
This page is a preview.{' '}
<a
href="/api/exit-preview"
className="underline hover:text-teal-300 duration-200 transition-colors"
>
Click here
</a>{' '}
to exit preview mode.
</>
) : (
<>
The source code for this blog is{' '}
<a
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="underline hover:text-blue-600 duration-200 transition-colors"
>
available on GitHub
</a>
.
</>
)}
</div>
</Container>
</div>
)
}
export default Alert
//Avatar.tsx
type Props = {
name: string
picture: string
}
const Avatar = ({ name, picture }: Props) => {
return (
<div className="flex items-center">
<img src={picture} className="w-12 h-12 rounded-full mr-4" alt={name} />
<div className="text-xl font-bold">{name}</div>
</div>
)
}
export default Avatar
import PostPreview from './post-preview'
import type Post from '../interfaces/post'
type Props = {
posts: Post[]
}
const MoreStories = ({ posts }: Props) => {
return (
<section>
<h2 className="mb-8 text-5xl md:text-7xl font-bold tracking-tighter leading-tight">
More Stories
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
{posts.map((post) => (
<PostPreview
key={post.slug}
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
slug={post.slug}
excerpt={post.excerpt}
/>
))}
</div>
</section>
)
}
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 (
<div>
<div className="mb-5">
<CoverImage slug={slug} title={title} src={coverImage} />
</div>
<h3 className="text-3xl mb-3 leading-snug">
<Link
as={`/posts/${slug}`}
href="/posts/[slug]"
className="hover:underline"
>
{title}
</Link>
</h3>
<div className="text-lg mb-4">
<DateFormatter dateString={date} />
</div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
<Avatar name={author.name} picture={author.picture} />
</div>
)
}
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 = (
<img
src={src}
alt={`Cover Image for ${title}`}
className={cn('shadow-sm', {
'hover:shadow-lg transition-shadow duration-200': slug,
})}
/>
)
return (
<div className="sm:mx-0">
{slug ? (
<Link as={`/posts/${slug}`} href="/posts/[slug]" aria-label={title}>
{image}
</Link>
) : (
image
)}
</div>
)
}
export default CoverImage
更多组件代码可至GitHub仓库。
网友评论