This commit is contained in:
2026-01-18 12:32:55 +08:00
parent 828c9390ae
commit 477f6829d9
9 changed files with 60 additions and 124 deletions

2
.gitignore vendored
View File

@@ -13,6 +13,6 @@ node_modules
bun.lock
yarn.lock
.edgeone/
.vercel
.env*.local

View File

@@ -9,8 +9,8 @@ useHead({
// meta: () => siteMetaData,
});
const desktopBg = siteConfig.theme.background || "";
const mobileBg = siteConfig.theme.backgroundMobile || "";
const desktopBg = siteConfig.theme?.background || "";
const mobileBg = siteConfig.theme?.backgroundMobile || "";
// 将 siteConfig.theme.color 转换为 CSS 变量
const hexToRgb = (hex: string) => {
@@ -20,15 +20,39 @@ const hexToRgb = (hex: string) => {
: "189, 131, 243";
};
const primaryColor = siteConfig.theme.color || "#bd83f3";
const primaryColor = siteConfig.theme?.color || "#bd83f3";
const primaryRgb = hexToRgb(primaryColor);
// Parse traceConfig.script (which may contain raw <script> tags) into
// proper head script entries to avoid inserting raw HTML into a script
// element (which causes JS parse errors like "Unexpected token '<'").
const traceRaw = siteConfig.traceConfig?.script || "";
const traceScripts = [] as any[];
if (typeof traceRaw === "string" && traceRaw.trim()) {
// find all <script ... src="..."> tags and extract src
const srcRegex = /<script[^>]*src=["']([^"']+)["'][^>]*>\s*<\/script>/gi;
let m: RegExpExecArray | null;
while ((m = srcRegex.exec(traceRaw))) {
let src = m[1];
if (src?.startsWith("//")) src = `https:${src}`;
traceScripts.push({ src });
}
// find inline script contents
const inlineRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
while ((m = inlineRegex.exec(traceRaw))) {
const content = (m[1] || "").trim();
if (content) traceScripts.push({ children: content, type: "text/javascript" });
}
}
useHead({
style: [
{
innerHTML: `:root { --site-primary: ${primaryColor}; --site-primary-rgb: ${primaryRgb}; }`,
},
],
script: traceScripts,
});
</script>

View File

@@ -9,11 +9,6 @@
<span v-if="from && fromWho" class="ml-1.5"> · {{ fromWho }}</span>
</p>
<!-- 访问统计 -->
<p v-if="showStats && !statsError" class="text-text-muted text-xs m-0">
👁 {{ visitors }} · 📊 {{ pageviews }}
</p>
<!-- 备案信息 -->
<p v-if="contact?.beian" class="text-text-muted text-xs m-0">
<NuxtLink
@@ -23,19 +18,19 @@
</NuxtLink>
</p>
<div v-if="adStats && ads.length">
<template v-for="ad in ads" :key="ad.link">
<div v-if="adStats && processedAds.length">
<template v-for="ad in processedAds" :key="ad.link">
<a
v-if="isExternal(ad.link)"
:href="ad.link"
target="_blank"
rel="noreferrer"
class="text-text-muted text-sm m-0">
<span v-html="ad.body"></span>
<span v-html="ad.html"></span>
</a>
<NuxtLink v-else :to="ad.link" class="text-text-muted text-sm m-0">
<span v-html="ad.body"></span>
<span v-html="ad.html"></span>
</NuxtLink>
</template>
</div>
@@ -86,15 +81,22 @@ const contact = siteConfig.footer || {};
const quote = ref("");
const from = ref("");
const fromWho = ref("");
const pageviews = ref(0);
const visitors = ref(0);
const statsError = ref(true);
const showHitokoto = siteConfig.footer?.hitokoto?.enable;
const showStats = ref(siteConfig.umami?.enable);
const adStats = siteConfig.ad?.enable;
const ads = siteConfig.ad?.ads || [];
// processedAds: replace root-relative src ("/...") with full site URL to
// avoid issues when deployed under different hosts/base paths or when
// root-relative assets are unavailable. Keeps existing HTML otherwise.
const processedAds = (ads || []).map((ad) => {
const html =
typeof ad.body === "string"
? ad.body.replace(/src="\/(?!\/)/g, `src="${siteConfig.siteMeta.url}/`)
: ad.body;
return { ...ad, html };
});
const buildHitokotoUrl = () => {
const type = siteConfig.footer?.hitokoto?.type;
const url = new URL("https://v1.hitokoto.cn/");
@@ -122,59 +124,11 @@ const fetchHitokoto = async () => {
}
};
const fetchStats = async () => {
try {
if (!siteConfig.umami?.apiEndpoint || !siteConfig.umami?.websiteId) {
return;
}
const apiEndpoint = siteConfig.umami.apiEndpoint;
const websiteId = siteConfig.umami.websiteId;
const apiKey = siteConfig.umami.apiKey;
if (!apiKey) return;
// 获取统计数据
const startAt = new Date(siteConfig.siteMeta.startTime);
const endAt = Date.now();
const resp = await fetch(
`${apiEndpoint}/v1/websites/${websiteId}/stats?startAt=${startAt}&endAt=${endAt}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
},
},
);
if (!resp.ok) {
console.warn(`Stats API returned ${resp.status}`);
statsError.value = true;
return;
}
const data = await resp.json();
if (data) {
statsError.value = false;
pageviews.value = data.pageviews;
visitors.value = data.visitors;
}
if (pageviews.value === 0 && visitors.value === 0) {
showStats.value = false;
}
} catch (e) {
statsError.value = true;
console.debug("Stats fetch failed (this is normal if blocked by ad blocker):", e.message);
}
};
const isExternal = (url) => {
return typeof url === "string" && /^https?:\/\//.test(url);
};
onMounted(() => {
if (showHitokoto) fetchHitokoto();
if (showStats.value) fetchStats();
});
</script>

View File

@@ -78,6 +78,12 @@ const siteConfig: SiteConfig = {
},
},
traceConfig: {
enable: true,
script: `<script charset="UTF-8" id="MXA_COLLECT" src="//mxana.tacool.com/sdk.js"></script>
<script>MXA.init({ id: "c2-G6ouenNf" })</script>`,
},
comment: {
twikoo: {
enable: true,
@@ -85,14 +91,6 @@ const siteConfig: SiteConfig = {
},
},
umami: {
enable: false,
scriptUrl: "https://cloud.umami.is/script.js",
apiKey: "api_MGcpRPYMcBmTKZOKdUVpr7mlBoWkck5g",
websiteId: "b33dfd14-7e62-498b-a199-de0ac38a1d44",
apiEndpoint: "https://api.umami.is",
},
ad: {
enable: true,
ads: [

View File

@@ -1,29 +0,0 @@
import { defineNuxtPlugin } from "#app";
import { VueUmamiPlugin } from "@jaseeey/vue-umami-plugin";
import type { Router } from "vue-router";
import siteConfig from "~/config";
export default defineNuxtPlugin((nuxtApp) => {
if (!import.meta.client) return;
if (!siteConfig.umami?.enable) return;
// 跳过在 localhost 环境下加载 Umami
if (
typeof window !== "undefined" &&
(window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1")
) {
console.log("Umami plugin skipped on localhost");
return;
}
const router = nuxtApp.$router as Router | undefined;
if (!router) return;
nuxtApp.vueApp.use(
VueUmamiPlugin({
websiteID: siteConfig.umami.websiteId,
scriptSrc: siteConfig.umami.scriptUrl,
router,
}),
);
});

View File

@@ -63,6 +63,11 @@ export interface Footer {
};
}
export interface TraceConfig {
enable?: boolean;
script?: string;
}
export interface CommentConfig {
twikoo?: {
enable?: boolean;
@@ -70,14 +75,6 @@ export interface CommentConfig {
};
}
export interface UmamiConfig {
enable?: boolean;
scriptUrl?: string;
apiKey?: string;
websiteId?: string;
apiEndpoint?: string;
}
export interface AdConfig {
enable?: boolean;
ads?: [{ body?: string; link?: string }];
@@ -94,7 +91,7 @@ export interface SiteConfig {
lines?: LineInfo[];
theme?: Theme;
footer?: Footer;
traceConfig?: TraceConfig;
comment?: CommentConfig;
umami?: UmamiConfig;
ad?: AdConfig;
}

View File

@@ -1,6 +1,6 @@
# 关于我
大家好!👋 我是 **RhenCloud**,一名热爱技术和开源的开发者。
大家好!👋 我是 **@RhenCloud**、**@泠云**,一名热爱技术和开源的开发者。
## 个人简介
@@ -66,7 +66,7 @@
- ✅ SEO 优化Meta、OG、Sitemap、RSS
- ✅ 文章搜索Fuse.js
- ✅ 评论系统Twikoo
- ✅ 访客统计Umami
- ✅ 访客统计
- ✅ 响应式设计
- ✅ 快速加载速度

View File

@@ -26,7 +26,7 @@ categories: ["Technology"]
### Firefox
是我 ~~抛弃 360 极速浏览器后~~最早使用的浏览器,后来由于缺少插件以及兼容性不佳放弃使用
是我~~抛弃 360 极速浏览器后~~最早使用的浏览器,后来由于缺少插件以及兼容性不佳放弃使用
优点:

View File

@@ -26,7 +26,6 @@ export default defineNuxtConfig({
"@nuxt/content",
"@nuxtjs/color-mode",
"@nuxt/ui",
// "nuxt-umami",
"@formkit/auto-animate",
],
@@ -117,13 +116,6 @@ export default defineNuxtConfig({
minify: true,
},
// umami: {
// id: siteConfig.umami.websiteId,
// host: siteConfig.umami.apiBase,
// autoTrack: true,
// enabled: siteConfig.umami.enable,
// },
colorMode: {
classSuffix: "",
preference: "system",