这是我参与「第五届青训营 」伴学笔记创作活动的第 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()
}
}, [])