Next.js搭建静态博客

使用 next.jsnextra 搭建博客失败后,进而尝试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的模块fspath

执行 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 && (
                           
                     )}
                 
            
          
    )

}

同时,项目使用的是tailwindCSS 框架,项目根目录,新建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: {},
  },
}

tailwindCSS框架依赖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}
{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 = (
    {`Cover
  )
  return (
    
{slug ? ( {image} ) : ( image )}
) } export default CoverImage

更多组件代码可至GitHub仓库。

你可能感兴趣的:(Next.js搭建静态博客)