fix: 修复博客静态资源兜底与头部结构

This commit is contained in:
sunlei 2026-07-01 14:28:17 +08:00
parent 27f5987b25
commit 41da304569
22 changed files with 242 additions and 106 deletions

View File

@ -8,7 +8,7 @@ KT 博客前台 demo基于 Argon WordPress 主题的视觉资产重新实现
- 组件库antdv-next。
- 样式SCSS类名按 BEM 组织。
- 路由hash 模式,便于静态部署。
- 静态资产:只从 Argon 主题包抽取 demo 需要的图片,不引入 WordPress PHP 与主题脚本
- 静态资产:默认使用旧线上博客真实资源,`public/blog-assets/` 保留本地备份兜底;不得把 Argon demo 图片作为默认背景、头像或 fallback 封面
## 本地运行
@ -28,14 +28,15 @@ pnpm exec playwright test e2e/argon-parity/interactions.spec.ts --project=chromi
pnpm exec playwright test e2e/argon-parity/baseline.spec.ts --project=chromium
```
`baseline.spec.ts` 只在需要重新抓线上 WordPress Argon 基准时运行;常规本地回归跑 `pages.spec.ts``interactions.spec.ts`线上 `https://blog.kwitsukasa.top/` 是只读视觉/交互基准,不是 Admin iframe 预览目标
`baseline.spec.ts` 只在需要重新抓 WordPress Argon 基准时运行;常规本地回归跑 `pages.spec.ts``interactions.spec.ts`公开域名 `https://blog.kwitsukasa.top/` 当前是 KT Blog Web 静态站入口,不是只读基准站;旧 WordPress 端口只作为视觉/交互基准和回滚入口
## Argon 还原范围
- `e2e/argon-parity` 保存与线上 `https://blog.kwitsukasa.top/` 对齐的页面、视口和交互矩阵
- `e2e/argon-parity` 保存与旧 WordPress Argon 基准对齐的页面、视口和交互矩阵;公开域名切到 KT Blog Web 后,基准抓取必须显式使用旧 WordPress 入口
- 本地 hash 路由按语义映射 WordPress query 路由:文章、分类、标签、搜索和月份归档都用同一套矩阵验证。
- 页面根节点通过 `kt-blog--home/search/category/tag/archive/post` 暴露 Argon 页面语义,方便样式、测试和 Admin iframe 预览复用。
- 公开 Blog API 返回非空文章列表时优先使用 APIAPI 不可用或返回空列表时保留内置 WordPress 抓取文章种子,避免数据未迁移期间线上静态站变成空站。
- 主题接口若仍返回 `/argon/theme/*` 历史 demo 占位图,前端必须映射回旧线上博客资源;本地备份只用于兜底,避免静态站重新露出模板图。
- 文章正文以线上 WordPress `#post_content` 渲染结果和 Argon `style.css` / `argontheme.js` 为准;代码块控制条、复制 toast、Fancybox 图片预览、正文链接 hover、分隔线和图片 lazyload 都必须有 Playwright 断言。
- `hljs-codeblock` 静态快照需要恢复 Argon `highlightjs-line-numbers` 运行时生成的 `data-line-number``.hljs-ln-n::before`,否则行号列会坍塌成 0px。
- 左栏 overview sticky/relative 切换用 no-headroom 回归用例固定,解除 fixed 后不得重放卡片入场动画或产生缩放闪烁。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -126,6 +126,20 @@ describe('App', () => {
expect(wrapper.text()).toContain('KwiTsukasa的小站');
});
it('renders Argon header without the old collapse container', async () => {
await router.push('/');
await router.isReady();
const wrapper = mount(App, {
global: {
plugins: [router],
},
});
expect(wrapper.find('.kt-blog__header-collapse').exists()).toBe(false);
expect(wrapper.find('.kt-blog__header-nav').exists()).toBe(true);
});
it('loads local blog theme config by default', async () => {
const fetchMock = vi.fn((url: string | URL | Request) => {
const target = `${url}`;
@ -225,10 +239,10 @@ describe('App', () => {
expect(styleText).toContain('--themecolor: #C3A1ED;');
expect(styleText).toContain('--radius: 6px;');
expect(styleText).toContain(
"--argon-background-dark-image: url('https://s3.kwitsukasa.top/images/bg-冬滚滚.png');",
"--argon-background-dark-image: url('https://s3.kwitsukasa.top/images/bg-冬滚滚.png'), url('/blog-assets/bg-donggungun.png');",
);
expect(styleText).toContain(
"--argon-author-avatar: url('http://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg');",
"--argon-author-avatar: url('http://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg'), url('/blog-assets/avatar-tsukasa-1.jpg');",
);
});
@ -359,10 +373,10 @@ describe('App', () => {
expect(styleText).toContain('--radius: 10px;');
expect(styleText).toContain('--argon-background-opacity: 0.8;');
expect(styleText).toContain(
"--argon-background-dark-image: url('https://s3.kwitsukasa.top/images/wp-dark.png');",
"--argon-background-dark-image: url('https://s3.kwitsukasa.top/images/wp-dark.png'), url('/blog-assets/bg-donggungun.png');",
);
expect(styleText).toContain(
"--argon-author-avatar: url('http://s3.kwitsukasa.top/images/wp-avatar.jpg');",
"--argon-author-avatar: url('http://s3.kwitsukasa.top/images/wp-avatar.jpg'), url('/blog-assets/avatar-tsukasa-1.jpg');",
);
});
});

View File

@ -145,6 +145,40 @@ describe('useBlogArticles', () => {
expect(blogArticles.articles.value).toEqual(fallbackArticles);
});
it('keeps fallback article covers on previous blog assets instead of Argon demo images', async () => {
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/landing.jpg');
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/img-2-1200x1000.jpg');
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/img-1-1200x1000.jpg');
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/promo-1.png');
expect(fallbackArticles.every((article) => article.cover.startsWith('https://s3.kwitsukasa.top/images/'))).toBe(true);
});
it('uses the previous blog cover when public API articles have no cover', async () => {
mockFetch([
{
body: {
code: 200,
data: {
list: [
{
...publicArticle,
cover: '',
},
],
total: 1,
},
},
status: 200,
},
]);
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
const blogArticles = useBlogArticles();
await blogArticles.loadArticles();
expect(blogArticles.articles.value[0]?.cover).toBe('https://s3.kwitsukasa.top/images/bg-冬滚滚.png');
});
it('fetches public article detail when cached list data does not include html content', async () => {
const fetchMock = mockFetch([
{

View File

@ -140,13 +140,34 @@ describe('useBlogTheme', () => {
expect(themeRootClass.value).toContain('kt-blog--shadow-big');
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--themecolor: #C3A1ED;');
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain("url('https://s3.kwitsukasa.top/images/bg-冬滚滚.png')");
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-dark-image: url(\'https://s3.kwitsukasa.top/images/bg-冬滚滚.png\');');
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-dark-image: url(\'https://s3.kwitsukasa.top/images/bg-冬滚滚.png\'), url(\'/blog-assets/bg-donggungun.png\');');
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-filter: brightness(0.65);');
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-opacity: 1;');
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-dark-opacity: 1;');
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain("url('http://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg')");
});
it('maps legacy Argon placeholder image config back to previous blog static assets', async () => {
const { applyWordpressThemeConfig, siteConfig } = useBlogTheme();
applyWordpressThemeConfig({
backgroundDarkImage: '/argon/theme/img-2-1200x1000.jpg',
backgroundImage: '/argon/theme/img-2-1200x1000.jpg',
site: {
authorAvatar: '/argon/theme/profile.jpg',
},
});
await nextTick();
const styleText = document.querySelector('#kt-blog-theme-style')?.textContent || '';
expect(styleText).toContain("url('https://s3.kwitsukasa.top/images/bg-冬滚滚.png')");
expect(styleText).toContain("url('/blog-assets/bg-donggungun.png')");
expect(styleText).toContain("url('https://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg')");
expect(styleText).toContain("url('/blog-assets/avatar-tsukasa-1.jpg')");
expect(styleText).not.toContain('/argon/theme/');
expect(siteConfig.value.authorAvatar).toBe('https://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg');
});
it('mirrors remote layout class switches instead of keeping stale defaults', async () => {
const { applyWordpressThemeConfig, themeRootClass, wordpressThemeConfig } = useBlogTheme();

View File

@ -88,9 +88,36 @@ export default defineComponent({
</RouterLink>
</div>
<div class="kt-blog__header-collapse">
<div class="kt-blog__header-collapse-head">
<div class="kt-blog__header-mobile-search">
<ul class="kt-blog__header-nav kt-blog__header-nav--hover">
{siteConfig.value.headerMenu.map((item) => (
<li key={item.label} class="kt-blog__header-nav-item">
{item.external ? (
<a
class="kt-blog__header-nav-link"
href={item.href}
rel="noopener noreferrer"
target="_blank"
>
{item.label}
</a>
) : (
<RouterLink class="kt-blog__header-nav-link" to={item.href}>
{item.label}
</RouterLink>
)}
</li>
))}
</ul>
<ul class="kt-blog__header-nav kt-blog__header-nav--end">
<li class="kt-blog__header-search-item">
<div
class={['kt-blog__header-search', navSearchOpen.value && 'kt-blog__header-search--open']}
onClick={() => {
navSearchOpen.value = true;
nextTick(() => focusInput(navSearchInputRef.value));
}}
>
<div class="kt-blog__input-group">
<div class="kt-blog__input-addon-wrap">
<span class="kt-blog__input-addon">
@ -98,72 +125,26 @@ export default defineComponent({
</span>
</div>
<BlogInput
class="kt-blog__header-mobile-search-input kt-blog__input"
ref={navSearchInputRef}
class="kt-blog__header-search-input kt-blog__input"
placeholder="搜索什么..."
autocomplete="off"
v-model:value={keyword.value}
onClick={(event: MouseEvent) => event.stopPropagation()}
onBlur={() => {
navSearchOpen.value = false;
}}
onKeydown={(event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
submitSearch();
}
}}
/>
</div>
</div>
</div>
<ul class="kt-blog__header-nav kt-blog__header-nav--hover">
{siteConfig.value.headerMenu.map((item) => (
<li key={item.label} class="kt-blog__header-nav-item">
{item.external ? (
<a
class="kt-blog__header-nav-link"
href={item.href}
rel="noopener noreferrer"
target="_blank"
>
{item.label}
</a>
) : (
<RouterLink class="kt-blog__header-nav-link" to={item.href}>
{item.label}
</RouterLink>
)}
</li>
))}
</ul>
<ul class="kt-blog__header-nav kt-blog__header-nav--end">
<li class="kt-blog__header-search-item">
<div
class={['kt-blog__header-search', navSearchOpen.value && 'kt-blog__header-search--open']}
onClick={() => {
navSearchOpen.value = true;
nextTick(() => focusInput(navSearchInputRef.value));
}}
>
<div class="kt-blog__input-group">
<div class="kt-blog__input-addon-wrap">
<span class="kt-blog__input-addon">
<SearchOutlined />
</span>
</div>
<BlogInput
ref={navSearchInputRef}
class="kt-blog__header-search-input kt-blog__input"
placeholder="搜索什么..."
autocomplete="off"
v-model:value={keyword.value}
onClick={(event: MouseEvent) => event.stopPropagation()}
onBlur={() => {
navSearchOpen.value = false;
}}
onKeydown={(event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
submitSearch();
}
}}
/>
</div>
</div>
</li>
</ul>
</div>
</li>
</ul>
<div class="kt-blog__header-menu-mask" />
<BlogButton

View File

@ -1,4 +1,5 @@
import { articleContentHtml } from './blogArticleContent';
import { PREVIOUS_BLOG_BACKGROUND_IMAGE } from './blogStaticAssets';
export interface BlogArticle {
id: number;
@ -252,7 +253,7 @@ export const articles: BlogArticle[] = [
category: 'public',
categorySlug: 'public',
tags: [],
cover: '/argon/theme/landing.jpg',
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
date: '2026-06-18 11:11',
readTime: '18 分钟',
author: 'KwiTsukasa',
@ -274,7 +275,7 @@ export const articles: BlogArticle[] = [
category: 'NAS',
categorySlug: 'nas',
tags: ['NAS'],
cover: '/argon/theme/img-2-1200x1000.jpg',
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
date: '2026-05-16 16:43',
readTime: '14 分钟',
author: 'KwiTsukasa',
@ -298,7 +299,7 @@ export const articles: BlogArticle[] = [
category: 'NAS',
categorySlug: 'nas',
tags: ['NAS'],
cover: '/argon/theme/img-1-1200x1000.jpg',
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
date: '2026-05-12 12:00',
readTime: '16 分钟',
author: 'KwiTsukasa',
@ -321,7 +322,7 @@ export const articles: BlogArticle[] = [
category: 'NAS',
categorySlug: 'nas',
tags: ['NAS'],
cover: '/argon/theme/landing.jpg',
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
date: '2026-05-12 11:58',
readTime: '14 分钟',
author: 'KwiTsukasa',
@ -344,7 +345,7 @@ export const articles: BlogArticle[] = [
category: 'Vue',
categorySlug: 'vue',
tags: ['Vue'],
cover: '/argon/theme/promo-1.png',
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
date: '2025-10-31 16:16',
readTime: '11 分钟',
author: 'KwiTsukasa',
@ -369,7 +370,7 @@ export const articles: BlogArticle[] = [
{ slug: 'vue', label: 'Vue' },
],
tags: ['MQTT', 'Vue'],
cover: '/argon/theme/img-2-1200x1000.jpg',
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
date: '2025-10-29 16:50',
readTime: '17 分钟',
author: 'KwiTsukasa',
@ -392,7 +393,7 @@ export const articles: BlogArticle[] = [
category: 'Node',
categorySlug: 'node',
tags: ['Node'],
cover: '/argon/theme/img-1-1200x1000.jpg',
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
date: '2025-10-29 15:38',
readTime: '13 分钟',
author: 'KwiTsukasa',

View File

@ -0,0 +1,59 @@
export const PREVIOUS_BLOG_BACKGROUND_IMAGE = 'https://s3.kwitsukasa.top/images/bg-冬滚滚.png';
export const PREVIOUS_BLOG_AUTHOR_AVATAR = 'https://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg';
export const LOCAL_BLOG_BACKGROUND_IMAGE = '/blog-assets/bg-donggungun.png';
export const LOCAL_BLOG_AUTHOR_AVATAR = '/blog-assets/avatar-tsukasa-1.jpg';
const LEGACY_ARGON_ASSET_REPLACEMENTS: Record<string, string> = {
'/argon/theme/img-1-1200x1000.jpg': PREVIOUS_BLOG_BACKGROUND_IMAGE,
'/argon/theme/img-2-1200x1000.jpg': PREVIOUS_BLOG_BACKGROUND_IMAGE,
'/argon/theme/landing.jpg': PREVIOUS_BLOG_BACKGROUND_IMAGE,
'/argon/theme/profile.jpg': PREVIOUS_BLOG_AUTHOR_AVATAR,
'/argon/theme/promo-1.png': PREVIOUS_BLOG_BACKGROUND_IMAGE,
};
/**
* Resolves theme/article image values while keeping the old real online resources as the primary source.
*
* @param value Image URL from API data, static captured article data, or a CSS `url(...)` value.
* @param fallback Primary fallback to use when the value is empty; callers pass the matching online old asset.
* @returns A usable online/local image URL with legacy Argon demo placeholders mapped back to previous blog assets.
*/
export function resolveBlogStaticAsset(value?: null | string, fallback = PREVIOUS_BLOG_BACKGROUND_IMAGE) {
const asset = unwrapBlogCssImage(value);
if (!asset) return fallback;
const replacementKey = getAssetPath(asset);
return LEGACY_ARGON_ASSET_REPLACEMENTS[replacementKey] || asset;
}
/**
* Extracts the raw asset URL from plain URLs and CSS `url(...)` tokens.
*
* @param value Candidate image token from the theme API or generated CSS.
* @returns Trimmed URL/path without surrounding `url(...)` syntax, or an empty string for unusable values.
*/
export function unwrapBlogCssImage(value?: null | string) {
const normalized = `${value || ''}`.trim();
if (!normalized) return '';
const cssImage = /^url\((.*)\)$/i.exec(normalized)?.[1]?.trim();
return (cssImage ? cssImage.replace(/^['"]|['"]$/g, '') : normalized).trim();
}
/**
* Normalizes absolute same-path assets to their pathname so legacy placeholder matching is stable.
*
* @param asset URL or root-relative path to compare against known Argon demo placeholder files.
* @returns Pathname for absolute URLs and the original value for root-relative paths.
*/
function getAssetPath(asset: string) {
if (asset.startsWith('/')) return asset;
try {
return new URL(asset).pathname;
} catch {
return asset;
}
}

View File

@ -16,14 +16,9 @@ import {
type BlogCategory,
type BlogTag,
} from '@/data/blog';
import { PREVIOUS_BLOG_BACKGROUND_IMAGE, resolveBlogStaticAsset } from '@/data/blogStaticAssets';
const coverPool = [
'/argon/theme/img-2-1200x1000.jpg',
'/argon/theme/img-1-1200x1000.jpg',
'/argon/theme/landing.jpg',
'/argon/theme/promo-1.png',
];
const defaultCover = coverPool[0] || '/argon/theme/img-2-1200x1000.jpg';
const defaultCover = PREVIOUS_BLOG_BACKGROUND_IMAGE;
const colorPool = ['blue', 'purple', 'green', 'orange', 'geekblue', 'cyan', 'volcano', 'magenta'];
const blogArticles = ref<BlogArticle[]>(fallbackArticles);
const loading = ref(false);
@ -222,7 +217,7 @@ function normalizeWordpressArticle(article: WordpressPublicArticle): BlogArticle
comments: 0,
content: contentText ? [contentText] : [],
contentHtml,
cover: article.cover || coverPool[article.id % coverPool.length] || defaultCover,
cover: resolveBlogStaticAsset(article.cover, defaultCover),
date: formatDate(article.date || article.modified),
excerpt,
id: article.id,

View File

@ -1,6 +1,14 @@
import { theme } from 'antdv-next';
import { computed, reactive, watch } from 'vue';
import {
PREVIOUS_BLOG_AUTHOR_AVATAR,
PREVIOUS_BLOG_BACKGROUND_IMAGE,
LOCAL_BLOG_AUTHOR_AVATAR,
LOCAL_BLOG_BACKGROUND_IMAGE,
resolveBlogStaticAsset,
unwrapBlogCssImage,
} from '@/data/blogStaticAssets';
import { createBlogMotionCssVariables } from '@/factories/blogAnimationFactory';
import { BLOG_META_NAMES, blogDomId, blogDomSelector, blogMetaSelector } from '@/factories/blogDomFactory';
@ -107,8 +115,8 @@ const ARGON_SERIF_FONT_FAMILY = 'Georgia, "Times New Roman", "Noto Serif SC", se
const ARGON_DEFAULT_COLOR_PRIMARY = '#c3a1ed';
const ARGON_PRIMARY_SOFT = '#4a4058';
const ARGON_CARD_SHADOW = '0 2px 4px rgba(0, 0, 0, 0.075)';
const ARGON_DEFAULT_BACKGROUND = 'https://s3.kwitsukasa.top/images/bg-冬滚滚.png';
const ARGON_DEFAULT_AUTHOR_AVATAR = 'https://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg';
const ARGON_DEFAULT_BACKGROUND = PREVIOUS_BLOG_BACKGROUND_IMAGE;
const ARGON_DEFAULT_AUTHOR_AVATAR = PREVIOUS_BLOG_AUTHOR_AVATAR;
const defaultHeaderMenu: BlogThemeMenuItem[] = [];
const defaultSidebarMenu: BlogThemeMenuItem[] = [
{ href: '/', icon: 'fa-home', label: '首页' },
@ -304,7 +312,8 @@ function applyWordpressThemeConfig(config: WordpressArgonThemeConfig) {
typeof nextBackgroundDarkBrightness === 'number'
? nextBackgroundDarkBrightness
: runtimeConfig.backgroundDarkBrightness;
runtimeConfig.backgroundDarkImage = normalizeCssImage(config.backgroundDarkImage) || runtimeConfig.backgroundDarkImage;
runtimeConfig.backgroundDarkImage =
normalizeCssImage(config.backgroundDarkImage) || runtimeConfig.backgroundDarkImage;
runtimeConfig.backgroundDarkOpacity =
typeof nextBackgroundDarkOpacity === 'number'
? nextBackgroundDarkOpacity
@ -432,12 +441,14 @@ function applyCssVariables(currentPreferences: BlogThemePreferences) {
const palette = createThemePalette(currentPreferences.colorPrimary, currentPreferences.mode);
const fontFamily = getThemeFontFamily(currentPreferences.font);
const argonColorVariables = createArgonColorVariables(primaryRgb, currentPreferences.mode);
const backgroundImage =
const backgroundImage = appendCssImageFallback(
normalizeCssImage(
currentPreferences.mode === 'dark'
? runtimeConfig.backgroundDarkImage || runtimeConfig.backgroundImage
: runtimeConfig.backgroundImage,
) || `url('${ARGON_DEFAULT_BACKGROUND}')`;
) || `url('${ARGON_DEFAULT_BACKGROUND}')`,
LOCAL_BLOG_BACKGROUND_IMAGE,
);
const backgroundOpacity =
currentPreferences.mode === 'dark'
? runtimeConfig.backgroundDarkOpacity
@ -505,13 +516,13 @@ ${serializeCssVariables(argonColorVariables.current)}
--radius: ${currentPreferences.radius}px;
--card-radius: ${currentPreferences.radius}px;
--argon-background-image: ${backgroundImage};
--argon-background-light-image: ${normalizeCssImage(runtimeConfig.backgroundImage) || `url('${ARGON_DEFAULT_BACKGROUND}')`};
--argon-background-dark-image: ${normalizeCssImage(runtimeConfig.backgroundDarkImage) || backgroundImage};
--argon-background-light-image: ${appendCssImageFallback(normalizeCssImage(runtimeConfig.backgroundImage) || `url('${ARGON_DEFAULT_BACKGROUND}')`, LOCAL_BLOG_BACKGROUND_IMAGE)};
--argon-background-dark-image: ${appendCssImageFallback(normalizeCssImage(runtimeConfig.backgroundDarkImage) || backgroundImage, LOCAL_BLOG_BACKGROUND_IMAGE)};
--argon-background-filter: ${currentPreferences.mode === 'dark' ? `brightness(${runtimeConfig.backgroundDarkBrightness})` : 'none'};
--argon-background-opacity: ${backgroundOpacity};
--argon-background-light-opacity: ${runtimeConfig.backgroundOpacity};
--argon-background-dark-opacity: ${runtimeConfig.backgroundDarkOpacity};
--argon-author-avatar: ${normalizeCssImage(runtimeConfig.siteAuthorAvatar) || `url('${ARGON_DEFAULT_AUTHOR_AVATAR}')`};
--argon-author-avatar: ${appendCssImageFallback(normalizeCssImage(runtimeConfig.siteAuthorAvatar) || `url('${ARGON_DEFAULT_AUTHOR_AVATAR}')`, LOCAL_BLOG_AUTHOR_AVATAR)};
--argon-font-family: ${fontFamily};
--argon-shadow: ${ARGON_CARD_SHADOW};
--argon-primary-soft: ${ARGON_PRIMARY_SOFT};
@ -904,9 +915,12 @@ function normalizePositiveNumber(value?: number | string) {
return Number.isFinite(numericValue) ? Math.max(0, numericValue) : null;
}
/**
* @param value Theme image URL returned by the API or static defaults.
* @returns CSS `url(...)` token after replacing legacy Argon demo placeholders with previous blog assets.
*/
function normalizeCssImage(value?: string) {
if (!value) return '';
const normalized = value.trim();
const normalized = resolveBlogStaticAsset(value, '');
if (normalized.startsWith('url(')) return normalized;
if (/^https?:\/\//i.test(normalized) || normalized.startsWith('/')) {
@ -916,13 +930,27 @@ function normalizeCssImage(value?: string) {
return '';
}
/**
* @param primaryCssImage CSS `url(...)` token for the preferred remote image.
* @param localFallback Local packaged image path used when the remote image cannot load.
* @returns A CSS background-image layer list with the remote image first and local backup second.
*/
function appendCssImageFallback(primaryCssImage: string, localFallback: string) {
const fallbackCssImage = normalizeCssImage(localFallback);
if (!fallbackCssImage || primaryCssImage.includes(fallbackCssImage)) {
return primaryCssImage;
}
return `${primaryCssImage}, ${fallbackCssImage}`;
}
/**
* @param value Theme asset URL returned by WordPress-compatible config.
* @returns Plain URL/path after replacing legacy Argon demo placeholders, suitable for component state.
*/
function normalizeThemeAsset(value?: string) {
if (!value) return '';
const normalized = value.trim();
const cssImage = /^url\((.*)\)$/i.exec(normalized)?.[1]?.trim();
const asset = cssImage
? cssImage.replace(/^['"]|['"]$/g, '')
: normalized;
const asset = resolveBlogStaticAsset(unwrapBlogCssImage(value), '');
return /^https?:\/\//i.test(asset) || asset.startsWith('/') ? asset : '';
}

View File

@ -240,8 +240,11 @@
height: 100px;
margin: 0 auto;
border-radius: 50%;
background: var(--argon-author-avatar, url('/argon/theme/profile.jpg')) center / cover no-repeat;
background-color: rgba(127, 127, 127, 0.1);
background-image: var(--argon-author-avatar, url('/blog-assets/avatar-tsukasa-1.jpg'));
background-position: center;
background-repeat: no-repeat;
background-size: cover;
box-shadow: var(--argon-shadow);
}

View File

@ -46,11 +46,10 @@
font-weight: 700;
}
&__header-collapse {
&__header-nav--hover {
display: flex;
flex: 1;
align-items: center;
justify-content: space-between;
}
&__header-nav {