在 2023 年的年底,我終于有時間下定決心把我的 UtilMeta 項目官網(wǎng) 進行翻新,主要的原因是之前的官網(wǎng)是用 Vue2 實現(xiàn)的一個 SPA 應(yīng)用,對搜索引擎 SEO 很不友好,這對于介紹項目的官網(wǎng)來說是一個硬傷
所以在調(diào)研一圈后,我準備用 Vite-SSG + Vue3 + Vuetify3 把官網(wǎng)重新來過,前后花了兩周左右的時間,本文記錄著開發(fā)過程中的思考和總結(jié),要點主要有
- 為什么 SPA 應(yīng)用不應(yīng)該用于搭建項目官網(wǎng)?
- SSG 項目的結(jié)構(gòu)是怎樣的,如何配置頁面的路由?
- 如何搭建多語言的靜態(tài)站,編寫支持多語言的頁面組件,以及使用
lang
/hreflang
為頁面指定不同的語言版本? - 如何用
unhead
庫為每個頁面配置不同的 html 頭部元信息,優(yōu)化搜索引擎收錄? - 如何使用
@media
CSS 媒體規(guī)則處理響應(yīng)式頁面在不同設(shè)備的首屏加載問題? - 如何優(yōu)雅處理 404 問題,避免 soft 404 對搜索收錄的影響?
為什么不應(yīng)該用 SPA 開發(fā)官網(wǎng)
這里我們先收窄一下定義,把【官網(wǎng)】定義為一個介紹性質(zhì)為主的網(wǎng)站,比如產(chǎn)品介紹,定價方案,關(guān)于我們等等,而不是一個直接交互的動態(tài)產(chǎn)品(比如各種各樣的 2C 內(nèi)容平臺,社交平臺),對于動態(tài)產(chǎn)品而言使用 SPA 其實無妨,如果想優(yōu)化搜索收錄可以定期把一些固定的 profile 頁面或者文章頁面提交給搜索引擎
所以就是一個原因,SEO。這是老生常談的問題,SPA 只會生成單個 index.html,爬取你網(wǎng)站上的任何 URL 都只會返回同樣的內(nèi)容,其中還往往不包括即將渲染出的文本,關(guān)鍵詞和鏈接等信息,這就導(dǎo)致搜索引擎呈現(xiàn)的結(jié)果一塌糊涂,不僅如此,在 Twiiter, Discord 等社交媒體直接抓取鏈接元信息(標題,描述,插圖)并渲染的平臺上,你的每個網(wǎng)頁都只會呈現(xiàn)一樣的信息
對于一個需要在互聯(lián)網(wǎng)上獲客的項目,我們都不應(yīng)該忽視來自搜索引擎的流量,尤其是國際化的項目。即使我們來到了 AIGC 紀元,以 ChatGPT 為代表的大模型訓(xùn)練語料獲取仍然以爬取網(wǎng)頁數(shù)據(jù)為主,這時你的項目各頁面如果能夠提供清晰的,包含足夠準確的關(guān)鍵詞和信息的,符合 Web 規(guī)范的 HTML 結(jié)果,你的項目或文檔也有可能會被 AI 收錄并整合到它們的輸出結(jié)果中,所以我認為對網(wǎng)頁結(jié)構(gòu)和渲染的優(yōu)化其實就是可以統(tǒng)稱為 Agent Optimization,即【對來自搜索引擎或大模型的】網(wǎng)絡(luò)爬取優(yōu)化,依然十分重要
合適的姿勢是?
SSR(服務(wù)端渲染) / SSG(服務(wù)端生成) 都是介紹性官網(wǎng)開發(fā)的合適姿勢,對于不需要太多渲染邏輯的靜態(tài)頁面來說,SSG 就足矣,你只需要把生成出來的 HTML 扔到任何頁面托管網(wǎng)站上都可以直接提供訪問,對 CDN 也足夠友好,如果自己喜歡折騰也可以搞自己的服務(wù)器來部署,我自己就是使用 nginx 來部署 SSG 生成的靜態(tài)頁面作為 CDN 的回源
SSG 項目結(jié)構(gòu)
與 SPA 應(yīng)用相比,SSG 項目最主要的區(qū)別是:路由與對應(yīng)的頁面模板是固定的,并且在構(gòu)建階段會直接生成每個頁面的 html 文件,而不是像 SPA 一樣只生成一個 index.html
反映到 Vue 項目的文件結(jié)構(gòu)上,SPA 應(yīng)用往往需要一個 router 文件來定義 vue-router 的路由和對應(yīng)的組件,而 SSG 應(yīng)用則可以把每個頁面的路由和對應(yīng)的 Vue 頁面組件直接定義在一個文件夾中(往往命名為 pages
)
所以 Vite-SSG 項目的 main.js
一般長這個樣子:
import App from './App.vue'
import { ViteSSG } from 'vite-ssg'
import routes from '~pages';
import vuetify from './plugins/vuetify';
export const createApp = ViteSSG(
App,
// vue-router options
{routes, scrollBehavior: () => ({ top: 0 }) },
// function to have custom setups
({ app, router, routes, isClient, initialState }) => {
// install plugins etc.
app.use(vuetify)
},
)
我們用 vite-ssg 定義的 ViteSSG
來代替 Vue 默認的 createApp
,在導(dǎo)入路由時,我們使用了
import routes from '~pages';
這是來自 vite-plugin-pages
插件的支持,你可以直接把一個文件夾下的 Vue 組件轉(zhuǎn)化為對應(yīng)的頁面路由,只需要在 vite.config.js
中配置
// Plugins
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import Pages from 'vite-plugin-pages'
export default defineConfig(
({command, mode}) => {
return {
plugins: [
Pages({
extensions: ['vue', 'md'],
}),
...
],
...
}
}
)
處理多語言頁面路由
如果你的官網(wǎng)需要給來自世界各地的用戶介紹你們的項目,多語言就基本上是一個必選項了,我們以支持中文與英文為例,其他的語言支持方式可以依此類推
之前我對于多語言的處理是根據(jù) IP 屬地返回語言然后前端直接設(shè)置語言,并沒有反應(yīng)到 URL 上,這其實是一種 Bad Practice,對于用戶訪問的時候看到的是什么語言版本的頁面完全不可控(因為他們可能使用了代理),用戶在分享頁面時他的受眾也是同理,搜索引擎也無法完全抓取所有的語言版本(因為 Google 的爬蟲主要在美國),所以 Google 也在 文檔 中說明很不建議這樣的做法
對于 SSG 的頁面路由,我的多語言實現(xiàn)實踐是:為每個頁面實現(xiàn)一個通用的頁面組件,其中定義一個屬性 lang
,組件中展示的所有文字都可以根據(jù)這個 lang
屬性選擇對應(yīng)的語言版本,由于頁面的屬性在 SSG 構(gòu)建時會直接傳入,所以會生成不同語言版本的 HTML 頁面文件,一個最簡化的頁面組件示例如下
<script setup>
const props = defineProps({
lang: {
type: String,
default() {
return 'en'
}
},
})
const messages = {
zh: {
title: '構(gòu)建數(shù)字世界的基礎(chǔ)設(shè)施'
},
en: {
title: 'Building the infrastructure of the digital world'
},
}
const msg = messages[props.lang];
</script>
<template>
<div>
{{ msg.title }}
</div>
</template>
接下來我們就可以搭建我們多語言頁面的文件夾結(jié)構(gòu)了,你可以選擇把不同的語言都作為不同的子路由,比如
/pages
/en
index.vue
/zh
index.vue
/ja
index.vue
/..
這樣訪問 /en
會進入英文頁面,訪問 /zh
會進入中文頁面
還有一種方式是選擇一種語言作為默認語言,如英語,然后將它的子路由置于與其他語言目錄平行的位置,比如
/pages
/zh
index.vue
/ja
index.vue
index.vue # en
utilmeta.com 采用的是第二種模式,因為我想讓官網(wǎng)的域名是可以直接訪問和鏈接的,保持簡潔,所以我對它的路由是這樣規(guī)劃的
/pages
/zh
index.vue ------ 首頁(中文)
about.vue ------ 關(guān)于我們(中文)
solutions.vue -- 解決方案(中文)
py.vue --------- UtilMeta Python 框架介紹(中文)
index.vue ------- 首頁(英語)
about.vue ---------- 關(guān)于我們(英語)
solutions.vue ------ 解決方案(英語)
py.vue ------------- UtilMeta Python 框架介紹(英語)
按照 JavaScript 的慣例,index
就會被處理為與它的目錄一致的路由,其他的名稱會根據(jù)名稱分配路由
其中,每個語言的頁面組件都可以直接引入它對應(yīng)的通用頁面組件,然后將 lang
屬性傳入通用頁面組件中,比如 /zh/about.vue
是中文的 “關(guān)于我們” 頁面組件
<script setup>
import About from "@/views/About.vue";
import AppWrapper from "@/components/AppWrapper.vue";
</script>
<template>
<AppWrapper lang="zh" route="about">
<About lang="zh"></About>
</AppWrapper>
</template>
其中 @/views/About.vue
是 “關(guān)于我們” 頁面的通用組件,我們傳入了 lang="zh"
,而 AppWrapper 是我編寫的一個通用的頁面骨架組件,包含著每個頁面都需要的頂欄,底欄,邊欄等頁面架構(gòu)
語言切換
對于支持多語言的官網(wǎng),我們可以需要在其中添加一個讓用戶主動切換語言的按鈕,它的邏輯也非常簡單,只需要將用戶展示一個支持的語言列表,然后每個語言按鈕都能將用戶切換到對應(yīng)的頁面路由,比如
<template>
<v-menu open-on-click>
<template v-slot:activator="{ props }">
<v-btn v-bind="props">
<v-icon>mdi-translate</v-icon>
</v-btn>
</template>
<v-list color="primary">
<v-list-item
v-for="(l, i) in languages"
:to="getLanguageRoute(l.value)"
:active="lang === l.value"
:key="i"
>
<v-list-item-title>{{ l.text }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<script setup>
const props = defineProps({
lang: {
type: String,
default(){
return 'en'
}
},
route: {
type: String,
default(){
return ''
}
}
});
const languages = [{
value: 'en',
text: 'English'
}, {
value: 'zh',
text: '中文'
}];
function getLanguageRoute(l){
if(l === 'en'){
return '/' + props.route;
}
if(!props.route){
return `/${l}`
}
return `/${l}/` + props.route
}
</script>
還是以上面的 About 頁面為例,如果用戶目前處于 https://utilmeta.com/about 路由(英語),而點擊了 中文 語言,就需要被引導(dǎo)到 https://utilmeta.com/zh/about 頁面,從用戶視角看來,頁面的結(jié)構(gòu)完全一致,只不過語言從英語切換到了中文
使用 unhead 為頁面注入元信息
對于靜態(tài)頁面而言,<head>
中的頭信息與頁面元信息非常重要,它決定著搜索引擎收錄的索引與關(guān)鍵詞,也決定著頁面鏈接在社交媒體分享時渲染的信息,一般來說 Vue 的頁面組件只是編寫 <body>
中的元素,但只需要使用一個名為 unhead
的庫,你就可以為不同的頁面編寫不同的頭信息了,比如以下是我在 UtilMeta 中文首頁的頁面組件中編寫的元信息
<script setup>
import { useHead } from '@unhead/vue'
const title = 'UtilMeta | 全周期后端 API 應(yīng)用 DevOps 解決方案';
const description = '面向后端 API 應(yīng)用的全生命周期解決方案,助力每個創(chuàng)造者,我們的產(chǎn)品有 UtilMeta Python 框架,一個面向后端 API 開發(fā)的漸進式元框架,API 管理平臺,以及 utype';
useHead({
title: title,
htmlAttrs: {
lang: 'zh'
},
link: [
{
hreflang: 'en',
rel: 'alternate',
href: 'https://utilmeta.com'
}
],
meta: [
{
name: 'description',
content: description,
},
{
property: 'og:title',
content: title
},
{
property: 'og:image',
content: 'https://utilmeta.com/img/zh.index.png'
},
{
property: 'og:description',
content: description
}
],
})
import Index from '@/views/Index.vue'
import AppWrapper from "@/components/AppWrapper.vue";
</script>
<template>
<AppWrapper lang="zh">
<Index lang="zh"></Index>
</AppWrapper>
</template>
其中重要的屬性有
-
title
:頁面的標題,直接影響著用戶在瀏覽器中看到的頁面標題與搜索引擎收錄的網(wǎng)頁中的標題 -
htmlAttrs.lang
:可以直接在html
根元素中編輯語言屬性lang
的值 -
hreflang
:通過插入含有hreflang
屬性的<link>
元素,你可以為頁面指定不同的語言版本,這里我們就指定了首頁的英文版本的鏈接,這樣的屬性能夠更好地為搜索引擎的多語言呈現(xiàn)提供便利 -
meta.description
:元信息中的描述, -
og:*
按照社交媒體渲染鏈接所通用的 Open Graph 協(xié)議 規(guī)定的屬性,可以決定著你在把鏈接分享到如 Twitter(X), Discord 等社交媒體或聊天軟件中時,它們的標題,描述和插圖
元信息的注入應(yīng)該是頁面級的,也就是對于不同語言的頁面,你也應(yīng)該注入該語言版本的元信息
實現(xiàn)靜態(tài)頁面的響應(yīng)式
你當然希望你的官網(wǎng)在寬屏電腦,平板和手機中都能有著不錯的顯示效果(或者至少不要出現(xiàn)元素錯亂重疊),想要做到這些,就需要開發(fā)響應(yīng)式的網(wǎng)頁
我開發(fā) UtilMeta 官網(wǎng)使用的是 Vue 組件庫是 Vuetify,Vuetify 已經(jīng)提供了一套 Display 系統(tǒng)和 breakpoints 機制,能夠提供一系列響應(yīng)式的斷點,讓我們在開發(fā)時為不同的設(shè)備指定不同的顯示效果
比如
<v-row>
<v-col :cols="display.xs.value ? 12 : 6">
</v-col>
<v-col :cols="display.xs.value ? 12 : 6">
</v-col>
<v-row>
這樣你就可以通過行列調(diào)節(jié)內(nèi)容在不同尺寸設(shè)備上的顯示了,示意如下
模板語法的問題
一切看起來都不錯吧?你發(fā)現(xiàn)本地調(diào)試時確實能夠做到響應(yīng)式,但是當網(wǎng)站上線時卻發(fā)現(xiàn)了問題
那就是,網(wǎng)頁在電腦端加載時,也會默認保持移動端的樣式,直到 js 加載完畢后,才會根據(jù)屏幕尺寸調(diào)整到合適的樣式,這樣在加載或刷新時,用戶會看到網(wǎng)頁的元素在幾秒內(nèi)發(fā)生了跳變,這是很奇怪的體驗,那么為什么會造成這樣的問題呢?
我打開了 vite-ssg 生成的 html 后發(fā)現(xiàn),SSG 在生成時會直接把模板中的配置進行固定和渲染,對于類似下面的響應(yīng)式代碼
<v-col :cols="display.xs.value ? 12 : 6">
<h1 :style='{fontSize: display.xs.value ? "32px" : "48px"}'></h1>
</v-col>
其實在構(gòu)建成 HTML 文件時就會渲染成
<div class="v-col-12">
<h1 :style="font-size: 32px"></h1>
</div>
渲染程序會直接把 display.xs.value
(以及其他的響應(yīng)式條件)作為 true 來處理,得到的 HTML 文件就會把某一個設(shè)備的樣式給固定,所以用戶在加載時就只能等到控制響應(yīng)式的 js 代碼加載完畢才能夠根據(jù)設(shè)備尺寸重新渲染,就會造成短暫的元素跳變的問題
救星 - @media CSS 媒體規(guī)則
那么如何正確處理靜態(tài)頁面的響應(yīng)式樣式呢?我探索出的答案是使用 @media
媒體規(guī)則,它可以讓你根據(jù)屏幕的大小創(chuàng)建不同的樣式規(guī)則,這樣你的響應(yīng)式樣式就 完全由 CSS 控制 了,當頁面渲染出來的時候(依賴的 css 加載完畢)就會完全按照 CSS 規(guī)則進行渲染,在不同設(shè)備刷新時也都會直接呈現(xiàn)適配對應(yīng)設(shè)備尺寸的渲染結(jié)果,不會出現(xiàn)元素跳變的問題
比如我把 About 頁面的標題添加了 about-title
類,然后在對應(yīng)的 CSS 中編寫
.about-title{
font-size: 60px;
line-height: 72px;
max-width: 800px;
margin: 6rem auto 0;
}
@media (max-width: 600px){
.about-title{
font-size: 36px;
line-height: 48px;
margin: 3rem auto 0;
}
}
這樣,About 頁面的標題在尺寸小于 600px
的設(shè)備中就可以按照 @media
塊中定義的樣式展現(xiàn)了
處理 v-row / v-col
Vuetify 提供的網(wǎng)格(v-row 控制行,v-col 控制列)系統(tǒng)可以很大程度提升響應(yīng)式網(wǎng)頁開發(fā)的效率,但是我們往往需要讓行列的顯示在不同的設(shè)備上保持響應(yīng)式,然而 @media
屬性尚不支持為不同的設(shè)備尺寸賦予不同的 HTML class,那么如何處理網(wǎng)格系統(tǒng)在 SSG 應(yīng)用中的響應(yīng)式呢?
下面是我的實踐,僅供參考:對于需要在移動端切換行數(shù)的 v-col
組件,我們可以直接把它在移動端對應(yīng)的行數(shù)命名為一個類,比如 xs-12-col
<v-row>
<v-col :cols="6" class="xs-12-col">
</v-col>
<v-col :cols="6" class="xs-12-col">
</v-col>
</v-row>
然后我們使用 @media
規(guī)則,在移動端尺寸的設(shè)備中直接為這些類指定網(wǎng)格樣式參數(shù),比如
@media (max-width: 600px) {
.xs-12-col{
flex: 0 0 100%!important;
max-width: 100%!important;
}
.xs-10-col{
flex: 0 0 83.3%!important;
max-width: 83.3%!important;
}
.xs-2-col{
flex: 0 0 16.6%!important;
max-width: 16.6%!important;
}
}
這樣,我們的網(wǎng)格系統(tǒng)也可以支持 SSG 中的響應(yīng)式樣式,而不會出現(xiàn)加載跳變了
部署靜態(tài)網(wǎng)站
優(yōu)雅處理 404
在 SSG 靜態(tài)頁面中,我們的網(wǎng)站支持的路由是預(yù)先定義和生成好的,其他的路徑訪問都應(yīng)該直接返回 404,但為了給用戶更好的體驗,一般常見的做法是單獨制作一個 404 Notfound
頁面,在訪問路徑?jīng)]有頁面時展示給用戶,讓他能方便地轉(zhuǎn)回首頁或其他頁面,比如 UtilMeta 官網(wǎng)的 404 頁面如下
使用 Vite-SSG 實現(xiàn)這樣的效果并不困難,你只需要在 pages
文件夾中增加兩個組件
404.vue
[...all].vue
這兩個組件中的內(nèi)容都是相同的,都放置著 404 頁面的組件代碼,[...all].vue
會作為所有沒有匹配到路由的頁面請求的返回頁面,而 404.vue
會輸出一個顯式的路由 404.html
,方便在 nginx 中直接進行重定向
完成我們的 SSG 頁面開發(fā)后,我們可以調(diào)用下面的命令將頁面構(gòu)建出對應(yīng)的 HTML 文件
vite-ssg build
對于我的 UtilMeta 官網(wǎng)而言,生成的文件如下
/dist
/zh
about.html
py.html
solutions.html
404.html
zh.html
about.html
index.html
py.html
solutions.html
接著,你就可以將這些靜態(tài)文件上傳到頁面托管服務(wù)或者自行搭建的靜態(tài)服務(wù)器上即可提供訪問了,我搭建 UtilMeta 官網(wǎng)的靜態(tài)服務(wù)器使用的 nginx 配置如下
server{
listen 80;
server_name utilmeta.com;
rewrite ^/(.*)/$ /$1 permanent;
location ~ /(css|js|img|font|assets)/{
root /srv/utilmeta/dist;
try_files $uri =404;
}
location /{
root /srv/utilmeta/dist;
index index.html;
try_files $uri $uri.html $uri/index.html =404;
}
error_page 404 403 500 502 503 504 /404.html;
location = /404.html {
root /srv/utilmeta/dist;
}
}
配置中監(jiān)聽 80 而非 443 端口是因為我的官網(wǎng)作為靜態(tài)站,官網(wǎng)需要的靜態(tài)資源已經(jīng)全部托管給 CDN 了(包括 SSL 證書),這里的 nginx 配置的是 CDN 的回源服務(wù)器,所以提供 HTTP 訪問就 ok 了
nginx 配置中 rewrite ^/(.*)/$ /$1 permanent
的作用是將目錄的訪問映射到對相應(yīng) HTML 文件的訪問,比如將 https://utilmeta.com/zh/ 映射到 https://utilmeta.com/zh,否則 Nginx 會出現(xiàn) 403 Forbidden 的錯誤
因為 vite-ssg 默認的生成策略會把位于目錄路徑的 index.vue
文件生成為與目錄同名的 html 文件,而不是放置于目錄中的 index.html
文件,所以如果不進行 rewrite
去掉路徑結(jié)尾的 /
的話,https://utilmeta.com/zh/ 就會直接訪問到 /zh/
目錄上,這對于 nginx 來說是禁止的行為
值得注意的是,對于 404 頁面的返回,最好需要伴隨著一個真正的 404 響應(yīng)碼(Status Code),而不是使用 200 OK 的響應(yīng)(那樣一般稱為軟 404),因為對于搜索引擎而言,只有檢測到 404 響應(yīng)碼,才會把這個路由視為無效,而不是判斷返回頁面中的文字,尤其當你的站點進行翻新時,老站點的一些路由就會失效了,如果它們一直留在搜索引擎的結(jié)果中誤導(dǎo)用戶,也會給訪客造成很大的困擾
在上面的 nginx 配置中,我們把所有 try_files
指令最后都附上了 =404
,也就是在匹配不到任何文件時生成 404 的響應(yīng)碼,然后使用 error_page
把包括 404 在內(nèi)的常見的錯誤或故障響應(yīng)碼的錯誤頁面指定為 /404.html
,也就是我們之前編寫的 404 頁面,這樣我們就解決了軟 404 的問題,所有無法匹配的路徑都會返回正確的 404 響應(yīng)碼以及制作好的 404 頁面
總結(jié)
總結(jié)一下我們學(xué)到和完成的東西文章來源:http://www.zghlxwxcb.cn/news/detail-838590.html
- 用 Vite-SSG 編寫一個 SSG 官網(wǎng)項目,了解了 SSG 項目的頁面路由方式
- 編寫可復(fù)用的多語言的 SSG 頁面組件,通過路由切換實現(xiàn)語言切換功能
- 使用
unhead
為每個頁面注入頭部元信息,使得每個頁面在搜索引擎與社交媒體上都能正確美觀地展示 - 使用
@media
解決實現(xiàn) SSG 靜態(tài)頁面的響應(yīng)式中的問題,以及 Vuetify 網(wǎng)格布局在 SSG 響應(yīng)式中的實踐 - 優(yōu)雅處理靜態(tài)頁面的 404 問題,避免軟 404,提高頁面收錄質(zhì)量和用戶體驗
如果你覺得這篇文章有幫助,可以逛一下這篇文章中我最終構(gòu)建的項目官網(wǎng) utilmeta.com ,也可以關(guān)注一下我的 X(Twitter) ,我會不定期分享一些技術(shù)實踐和項目文章來源地址http://www.zghlxwxcb.cn/news/detail-838590.html
到了這里,關(guān)于實現(xiàn)一個 SEO 友好的響應(yīng)式多語言官網(wǎng) (Vite-SSG + Vuetify3) 我的踩坑之旅的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!