Nextjs SEO 优化和多媒体适配| 青训营笔记

247 阅读2分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天。我们 Bocchi 小组采用 Nextjs 开发这次掘金站点的前端,因为是面向 C 端的站点,那对于 SEO 优化和多媒体适配就显得尤其重要了,这篇文章就来简要记录下实现过程(具体代码看下方仓库)。

仓库地址: github.com/Bocchi-Deve…

思路

SEO

如果只是对首页进行 SEO 配置的话那样还是挺简单的,但当项目页面越来越多时就显得麻烦起来了。关于 SEO 这里这里推荐使用 next-seo这个库,它可以通过一个组件的方式,对页面 SEO 进行方便配置。

pnpm i next-seo

多媒体适配

这里主要讲述下用 JavaScript 进行适配,我们项目因为使用了 mobx 状态管理库,我们只需要注册下就可以使用了。

实现

next-seo

这里我们会对其封装一下,让它更适合我们这次项目:

import merge from 'lodash-es/merge'
import { observer } from 'mobx-react-lite'
import type { NextSeoProps } from 'next-seo'
import { NextSeo } from 'next-seo'
import type { OpenGraph } from 'next-seo/lib/types'
import type { FC } from 'react'
import { useContext } from 'react'

import { InitialContext } from '~/context/initial-data'

type SEOProps = {
  title: string
  description?: string
  openGraph?: { type?: 'website' | 'article' } & OpenGraph
} & NextSeoProps

export const SEO: FC<SEOProps> = observer((props) => {
  const { title, description, openGraph, ...rest } = props
  const Title = `${title}`
  const { seo } = useContext(InitialContext) // 后台的 seo 配置
  return (
    <NextSeo
      {...{
        title,
        titleTemplate: `%s - ${seo.title}`,

        openGraph: merge(
          {
            type: 'article',
            locale: 'zh-cn',
            site_name: seo.title || '',
            description: description || seo.description || '',
            title: Title,
          } as OpenGraph,
          openGraph,
        ),
        description: description || seo.description || '',
        ...rest,
      }}
    />
  )
})

export const Seo = SEO

然后我们只需要传入对应的参数就可以了

<SEO title={'文章'} />

多媒体适配

import { makeAutoObservable } from 'mobx'

import { isClientSide } from '~/utils/env'

import type { ViewportRecord } from './types'

export default class AppUIStore {
  constructor() {
    makeAutoObservable(this)
  }

  viewport: ViewportRecord = {} as any

  private position = 0
  scrollDirection: 'up' | 'down' | null = null

  colorMode: 'light' | 'dark' = 'light'
  mediaType: 'screen' | 'print' = 'screen'

  headerNav = {
    title: '',
    meta: '',
    show: false,
  }

  shareData: { title: string; text?: string; url: string } | null = null

  updatePosition(direction: 'up' | 'down' | null, y: number) {
    if (typeof document !== 'undefined') {
      this.position = y
      this.scrollDirection = direction
    }
  }

  get headerOpacity() {
    const threshold = 50
    return this.position >= threshold
      ? 1
      : Math.floor((this.position / threshold) * 100) / 100
  }

  get isOverFirstScreenHeight() {
    if (!isClientSide()) {
      return
    }
    return this.position > window.innerHeight || this.position > screen.height
  }

  get isOverPostTitleHeight() {
    if (!isClientSide()) {
      return
    }

    return this.position > 126 || this.position > screen.height / 3
  }

  updateViewport() {
    const innerHeight = window.innerHeight
    const width = document.documentElement.getBoundingClientRect().width
    const { hpad, pad, mobile } = this.viewport

    // 忽略移动端浏览器 上下滚动 导致的视图大小变化
    if (
      this.viewport.h &&
      // chrome mobile delta == 56
      Math.abs(innerHeight - this.viewport.h) < 80 &&
      width === this.viewport.w &&
      (hpad || pad || mobile)
    ) {
      return
    }
    this.viewport = {
      w: width,
      h: innerHeight,
      mobile: window.screen.width <= 568 || window.innerWidth <= 568,
      pad: window.innerWidth <= 768 && window.innerWidth > 568,
      hpad: window.innerWidth <= 1100 && window.innerWidth > 768,
      wider: window.innerWidth > 1100 && window.innerWidth < 1920,
      widest: window.innerWidth >= 1920,
    }
  }

  get isPadOrMobile() {
    return this.viewport.pad || this.viewport.mobile
  }

  /**
   * < 1100
   */
  get isNarrowThanLaptop() {
    return this.isPadOrMobile || this.viewport.hpad
  }
}

_app.tsx 里注册

  useEffect(() => {
    store.appUIStore.updateViewport()

    window.onresize = () => {
      store.appUIStore.updateViewport()
    }
  }, [])

参考