feat: implement SEO optimization with relative URLs and structured data
- Add SEO meta tags (title, description, keywords, robots) - Add Open Graph and Twitter Card meta tags - Add JSON-LD structured data (WebPage schema) - Use relative URLs for og:url, og:image, and canonical links to avoid hardcoded localhost addresses in prerendered HTML - Support internationalized keywords with English defaults and Chinese translations - Optimize for Chinese search engines (Baidu, 360, Sogou) This ensures SEO metadata is properly captured in prerendered static HTML files without hardcoding development server URLs.
This commit is contained in:
parent
bbde769b49
commit
c49fa9feee
@ -87,7 +87,7 @@ export function vitePluginPrerender(options: PrerenderPluginOptions = {}): Plugi
|
|||||||
timeout: rendererOptions.timeout || 30000,
|
timeout: rendererOptions.timeout || 30000,
|
||||||
waitUntil: rendererOptions.waitUntil || 'networkidle0',
|
waitUntil: rendererOptions.waitUntil || 'networkidle0',
|
||||||
// 渲染选项:等待页面加载完成
|
// 渲染选项:等待页面加载完成
|
||||||
renderAfterTime: 2000, // 等待 2 秒确保内容加载完成
|
renderAfterTime: 3000, // 等待 3 秒确保Vue组件挂载和SEO标签注入完成
|
||||||
// 注入一些配置,确保页面正确渲染
|
// 注入一些配置,确保页面正确渲染
|
||||||
inject: {
|
inject: {
|
||||||
__PRERENDER__: true
|
__PRERENDER__: true
|
||||||
|
|||||||
@ -204,6 +204,9 @@ const router = createRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach(async (to, _from, next) => {
|
router.beforeEach(async (to, _from, next) => {
|
||||||
|
// 检测预渲染环境
|
||||||
|
const isPrerendering = typeof window !== 'undefined' && navigator.userAgent.includes('HeadlessChrome')
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
if (!authStore.isAuthenticated) {
|
if (!authStore.isAuthenticated) {
|
||||||
@ -254,6 +257,12 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预渲染时跳过认证检查
|
||||||
|
if (isPrerendering) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
||||||
next('/login')
|
next('/login')
|
||||||
} else if ((to.path === '/login' || to.path === '/signup') && authStore.isLoggedIn) {
|
} else if ((to.path === '/login' || to.path === '/signup') && authStore.isLoggedIn) {
|
||||||
|
|||||||
@ -1112,6 +1112,8 @@
|
|||||||
"Route not found: ": "路由未找到:",
|
"Route not found: ": "路由未找到:",
|
||||||
"Remove Background": "图片去背景",
|
"Remove Background": "图片去背景",
|
||||||
"Remove background from images using AI technology": "使用AI技术去除图片背景",
|
"Remove background from images using AI technology": "使用AI技术去除图片背景",
|
||||||
|
"Remove background from images using AI technology. Free online tool to remove image backgrounds instantly. Supports JPG, PNG, WebP formats.": "使用AI技术去除图片背景。免费在线工具,即时去除图片背景。支持 JPG、PNG、WebP 格式。",
|
||||||
|
"remove background, AI background removal, online background remover, image processing, background removal tool, transparent background, free tool, JPG background removal, PNG background removal": "图片去背景,AI去背景,在线去背景,图片处理,背景移除,透明背景,免费工具,JPG去背景,PNG去背景",
|
||||||
"Upload Image": "上传图片",
|
"Upload Image": "上传图片",
|
||||||
"Drag and drop your image here, or click to browse": "拖拽图片到此处,或点击浏览",
|
"Drag and drop your image here, or click to browse": "拖拽图片到此处,或点击浏览",
|
||||||
"Supports JPG, PNG, WebP formats": "支持 JPG、PNG、WebP 格式",
|
"Supports JPG, PNG, WebP formats": "支持 JPG、PNG、WebP 格式",
|
||||||
|
|||||||
129
apps/jingrow/frontend/src/shared/composables/useSEO.ts
Normal file
129
apps/jingrow/frontend/src/shared/composables/useSEO.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* SEO工具函数 - 符合国内搜索引擎和未来趋势
|
||||||
|
* 在组件中直接定义SEO信息,立即同步设置到HTML
|
||||||
|
*/
|
||||||
|
const DEFAULT_SITE_NAME = 'Jingrow'
|
||||||
|
const DEFAULT_OG_IMAGE = '/logo.svg'
|
||||||
|
|
||||||
|
function setMetaTag(name: string, content: string, attribute: 'name' | 'property' = 'name') {
|
||||||
|
if (!content || typeof document === 'undefined') return
|
||||||
|
|
||||||
|
const selector = `meta[${attribute}="${name}"]`
|
||||||
|
let tag = document.querySelector(selector) as HTMLMetaElement
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
tag = document.createElement('meta')
|
||||||
|
tag.setAttribute(attribute, name)
|
||||||
|
document.head.appendChild(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.setAttribute('content', content)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLinkTag(rel: string, href: string) {
|
||||||
|
if (!href || typeof document === 'undefined') return
|
||||||
|
|
||||||
|
const selector = `link[rel="${rel}"]`
|
||||||
|
let tag = document.querySelector(selector) as HTMLLinkElement
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
tag = document.createElement('link')
|
||||||
|
tag.setAttribute('rel', rel)
|
||||||
|
document.head.appendChild(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.setAttribute('href', href)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setJSONLD(data: object) {
|
||||||
|
if (typeof document === 'undefined') return
|
||||||
|
|
||||||
|
// 移除旧的 JSON-LD
|
||||||
|
const existingScript = document.querySelector('script[type="application/ld+json"]')
|
||||||
|
if (existingScript) {
|
||||||
|
existingScript.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新的 JSON-LD
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.type = 'application/ld+json'
|
||||||
|
script.textContent = JSON.stringify(data, null, 2)
|
||||||
|
document.head.appendChild(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置SEO标签 - 立即同步执行
|
||||||
|
* 符合国内搜索引擎(百度、360、搜狗)和未来趋势
|
||||||
|
* @param data SEO数据
|
||||||
|
*/
|
||||||
|
export function useSEO(data: {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
keywords?: string
|
||||||
|
image?: string
|
||||||
|
url?: string
|
||||||
|
robots?: string
|
||||||
|
}) {
|
||||||
|
if (typeof window === 'undefined' || typeof document === 'undefined') return
|
||||||
|
|
||||||
|
const pathname = window.location.pathname
|
||||||
|
const url = data.url || pathname
|
||||||
|
const image = data.image || DEFAULT_OG_IMAGE
|
||||||
|
const title = data.title ? `${data.title} - ${DEFAULT_SITE_NAME}` : DEFAULT_SITE_NAME
|
||||||
|
|
||||||
|
// Title - 立即设置(百度推荐:前20字核心信息+品牌词)
|
||||||
|
if (data.title) document.title = title
|
||||||
|
|
||||||
|
// Meta description - 立即设置(百度推荐:150字符以内)
|
||||||
|
if (data.description) setMetaTag('description', data.description)
|
||||||
|
|
||||||
|
// Keywords - 国内搜索引擎仍在使用
|
||||||
|
if (data.keywords) setMetaTag('keywords', data.keywords)
|
||||||
|
|
||||||
|
// Robots - 控制爬虫行为
|
||||||
|
if (data.robots) {
|
||||||
|
setMetaTag('robots', data.robots)
|
||||||
|
} else {
|
||||||
|
// 默认允许索引
|
||||||
|
setMetaTag('robots', 'index,follow')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open Graph - 立即设置
|
||||||
|
if (data.title) setMetaTag('og:title', data.title, 'property')
|
||||||
|
if (data.description) setMetaTag('og:description', data.description, 'property')
|
||||||
|
setMetaTag('og:image', image, 'property')
|
||||||
|
setMetaTag('og:url', url, 'property')
|
||||||
|
setMetaTag('og:type', 'website', 'property')
|
||||||
|
setMetaTag('og:site_name', DEFAULT_SITE_NAME, 'property')
|
||||||
|
setMetaTag('og:locale', 'zh_CN', 'property')
|
||||||
|
|
||||||
|
// Twitter Card - 立即设置
|
||||||
|
setMetaTag('twitter:card', 'summary_large_image')
|
||||||
|
if (data.title) setMetaTag('twitter:title', data.title)
|
||||||
|
if (data.description) setMetaTag('twitter:description', data.description)
|
||||||
|
setMetaTag('twitter:image', image)
|
||||||
|
|
||||||
|
// Canonical - 立即设置
|
||||||
|
setLinkTag('canonical', url)
|
||||||
|
|
||||||
|
// 结构化数据(JSON-LD)
|
||||||
|
if (data.title && data.description) {
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
name: data.title,
|
||||||
|
description: data.description,
|
||||||
|
url: url,
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: DEFAULT_SITE_NAME,
|
||||||
|
logo: {
|
||||||
|
'@type': 'ImageObject',
|
||||||
|
url: image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setJSONLD(jsonLd)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -185,13 +185,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { t } from '@/shared/i18n'
|
import { t } from '@/shared/i18n'
|
||||||
import { get_session_api_headers } from '@/shared/api/auth'
|
import { get_session_api_headers } from '@/shared/api/auth'
|
||||||
import { useAuthStore } from '@/shared/stores/auth'
|
import { useAuthStore } from '@/shared/stores/auth'
|
||||||
|
import { useSEO } from '@/shared/composables/useSEO'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// SEO配置 - 直接在组件中定义,立即同步设置到HTML
|
||||||
|
// 符合国内搜索引擎(百度、360、搜狗)和未来趋势
|
||||||
|
useSEO({
|
||||||
|
title: t('Remove Background'),
|
||||||
|
description: t('Remove background from images using AI technology. Free online tool to remove image backgrounds instantly. Supports JPG, PNG, WebP formats.'),
|
||||||
|
keywords: t('remove background, AI background removal, online background remover, image processing, background removal tool, transparent background, free tool, JPG background removal, PNG background removal')
|
||||||
|
})
|
||||||
|
|
||||||
interface HistoryItem {
|
interface HistoryItem {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user