美文网首页
Next.js搭建静态博客

Next.js搭建静态博客

作者: 贪恋冬天的幸福 | 来源:发表于2022-11-11 18:08 被阅读0次

    使用 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
    ---
    <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>
              </>
        )
    
    }
    

    同时,项目使用的是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 (
        <>
          <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仓库

    相关文章

      网友评论

          本文标题:Next.js搭建静态博客

          本文链接:https://www.haomeiwen.com/subject/nabfxdtx.html