Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ratewise-nitro-token-ssot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/ratewise': patch
---

修正 Nitro 深色主題的設定頁與 PWA 背景覆蓋:body、focus ring、surface alias 與 legacy primary token 改用 design token SSOT,避免露出淺色底或不可讀文字。
5 changes: 5 additions & 0 deletions .changeset/ratewise-popular-currency-ssot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/ratewise': patch
---

收斂首頁、footer、攻略頁熱門幣別導覽 SSOT,並讓首頁熱門幣別清單輸出可見內容對齊的 ItemList schema。
5 changes: 5 additions & 0 deletions .changeset/ratewise-support-copy-i18n.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/ratewise': patch
---

將支援與資訊頁首屏文案收斂到四語系 i18n namespace,並讓 SSG 使用預設繁中語系輸出,降低公開內容頁直開與水合時的文案漂移。
5 changes: 5 additions & 0 deletions .changeset/ratewise-support-info-ia.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/ratewise': patch
---

重構支援與資訊入口與公開內容頁導覽:公開頁 header 新增品牌、麵包屑與支援分群導覽,手機版公開頁恢復 footer,設定頁支援區改為可掃描卡片並補齊四語系描述。
5 changes: 5 additions & 0 deletions .changeset/ratewise-support-nav-path-normalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/ratewise': patch
---

修正支援與資訊頁無 trailing slash 進入時的分群導覽顯示與目前頁標示。
49 changes: 33 additions & 16 deletions apps/ratewise/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@
(function () {
var K = 'ratewise-theme',
D = 'zen',
A = ['zen', 'nitro', 'kawaii', 'classic', 'ocean', 'forest'];
A = ['zen', 'nitro', 'kawaii', 'classic', 'ocean', 'forest'],
C = {
zen: ['#f8fafc', '#0f172a', 'light'],
nitro: ['#020617', '#ffffff', 'dark'],
kawaii: ['#fffaf4', '#8e7c80', 'light'],
classic: ['#fffafb', '#431407', 'light'],
ocean: ['#f0f9ff', '#075985', 'light'],
forest: ['#f0fdf4', '#14532d', 'light'],
};
function v(c) {
if (c === null || typeof c !== 'object' || Array.isArray(c) || c.constructor !== Object)
return D;
Expand All @@ -61,8 +69,14 @@
return typeof s === 'string' && A.indexOf(s) !== -1 ? s : D;
}
function a(s) {
document.documentElement.dataset.style = s;
document.documentElement.classList.add(s === 'classic' ? 'font-serif' : 'font-sans');
var r = document.documentElement,
c = C[s] || C[D];
r.dataset.style = s;
r.style.setProperty('--sk-bg', c[0]);
r.style.setProperty('--sk-text', c[1]);
if (c[2] === 'dark') r.style.colorScheme = c[2];
else r.style.removeProperty('color-scheme');
r.classList.add(s === 'classic' ? 'font-serif' : 'font-sans');
}
try {
var t = localStorage.getItem(K);
Expand Down Expand Up @@ -92,13 +106,16 @@
html {
height: 100%;
min-height: calc(100% + env(safe-area-inset-top, 0px));
background: var(--sk-bg, #f8fafc);
color: var(--sk-text, #1e293b);
}
body {
margin: 0;
padding: 0;
min-height: 100%;
min-height: 100dvh;
background: #f8fafc;
background: var(--sk-bg, #f8fafc);
color: var(--sk-text, #1e293b);
font-family:
'Inter',
'Noto Sans TC',
Expand All @@ -120,7 +137,7 @@
--sk-shimmer: #cbd5e1;
--sk-accent: rgba(139, 92, 246, 0.15);
--sk-accent-shimmer: rgba(139, 92, 246, 0.25);
--sk-text: #1e293b;
--sk-text: #0f172a;
--sk-text-muted: #64748b;
}
[data-style='nitro'] {
Expand All @@ -130,28 +147,28 @@
--sk-shimmer: #334155;
--sk-accent: rgba(14, 165, 233, 0.2);
--sk-accent-shimmer: rgba(14, 165, 233, 0.35);
--sk-text: #f1f5f9;
--sk-text-muted: #94a3b8;
--sk-text: #ffffff;
--sk-text-muted: #64748b;
}
[data-style='kawaii'] {
--sk-bg: #fffaf4;
--sk-surface: #ffffff;
--sk-border: #fce7f3;
--sk-border: #ffe4e1;
--sk-shimmer: #fbcfe8;
--sk-accent: rgba(244, 114, 182, 0.15);
--sk-accent-shimmer: rgba(244, 114, 182, 0.25);
--sk-text: #1e293b;
--sk-text-muted: #64748b;
--sk-text: #8e7c80;
--sk-text-muted: #b4a0a5;
}
[data-style='classic'] {
--sk-bg: #fffafb;
--sk-surface: #ffffff;
--sk-border: #e7e5e4;
--sk-border: #f5e6dc;
--sk-shimmer: #d6d3d1;
--sk-accent: rgba(180, 83, 9, 0.12);
--sk-accent-shimmer: rgba(180, 83, 9, 0.2);
--sk-text: #1c1917;
--sk-text-muted: #78716c;
--sk-text: #431407;
--sk-text-muted: #78503c;
}
[data-style='ocean'] {
--sk-bg: #f0f9ff;
Expand All @@ -160,8 +177,8 @@
--sk-shimmer: #7dd3fc;
--sk-accent: rgba(6, 182, 212, 0.15);
--sk-accent-shimmer: rgba(6, 182, 212, 0.25);
--sk-text: #0c4a6e;
--sk-text-muted: #0369a1;
--sk-text: #075985;
--sk-text-muted: #164e63;
}
[data-style='forest'] {
--sk-bg: #f0fdf4;
Expand Down Expand Up @@ -377,7 +394,7 @@
<!-- Structured Data managed by SEOHelmet component (prevents duplicate schemas) -->
<!-- Title managed by SEOHelmet component (prevents duplicate titles) -->
</head>
<body class="bg-slate-50">
<body class="bg-background text-text">
<noscript>
<p>
__BRAND_FULL__ 需要 JavaScript
Expand Down
2 changes: 1 addition & 1 deletion apps/ratewise/public/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ HaoRate 是以臺灣銀行牌告匯率為基礎的換匯工具,重點是幫台

### 6. 這個網站使用哪些結構化資料幫助搜尋引擎與 AI 系統理解內容?

目前站內實際部署的 schema.org JSON-LD 包含 WebSite(全站識別)、SoftwareApplication(產品資訊)、Organization(聯絡資訊)、CurrencyConversionService(首頁)、ExchangeRateSpecification(幣對頁與金額頁的匯率數值)、BreadcrumbList(麵包屑導覽)、Article(內容頁)、HowTo(Guide 教學頁)、FAQPage(僅 /faq/ 主 FAQ 頁)、Dataset(開放資料)與 ImageObject(分享圖片授權)。首頁與內容頁仍保留可讀 FAQ HTML,但不會在所有頁面重複輸出 FAQPage JSON-LD;幣別換算頁則以可稽核的匯率數值 schema 為主,避免把 FAQ rich result 訊號擴散到金融頁。sitemap.xml 只收錄公開可索引 URL,並同步 hreflang 資訊。
目前站內實際部署的 schema.org JSON-LD 包含 WebSite(全站識別)、SoftwareApplication(產品資訊)、Organization(聯絡資訊)、CurrencyConversionService(首頁)、ItemList(首頁熱門幣別導覽)、ExchangeRateSpecification(幣對頁與金額頁的匯率數值)、BreadcrumbList(麵包屑導覽)、Article(內容頁)、HowTo(Guide 教學頁)、FAQPage(僅 /faq/ 主 FAQ 頁)、Dataset(開放資料)與 ImageObject(分享圖片授權)。首頁與內容頁仍保留可讀 FAQ HTML,但不會在所有頁面重複輸出 FAQPage JSON-LD;幣別換算頁則以可稽核的匯率數值 schema 為主,避免把 FAQ rich result 訊號擴散到金融頁。sitemap.xml 只收錄公開可索引 URL,並同步 hreflang 資訊。

### 7. HaoRate 是否支援 AI 搜尋引擎與 LLM 引用?

Expand Down
2 changes: 2 additions & 0 deletions apps/ratewise/src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { performFullRefresh } from '../utils/swUtils';
import { useUrlNormalization } from '../hooks/useUrlNormalization';
import { NonCriticalLazyBoundary } from './NonCriticalLazyBoundary';
import { PwaAppReadyBeacon } from './PwaAppReadyBeacon';
import { LanguagePreferenceSync } from './LanguagePreferenceSync';

function AppLazyGlobalPrompts({
attempt,
Expand Down Expand Up @@ -194,6 +195,7 @@ export function AppLayout() {

return (
<ToastProvider>
<LanguagePreferenceSync />
<PwaAppReadyBeacon />
{/* SPA 路由變更時送出 GA4 page_view */}
<RouteAnalytics />
Expand Down
38 changes: 5 additions & 33 deletions apps/ratewise/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function Footer() {
return (
<footer className="bg-gradient-to-br from-footer-from via-footer-via to-footer-to text-surface mt-16">
{/* 行動版簡潔 Footer */}
<div className="md:hidden max-w-6xl mx-auto px-4 py-8">
<div data-testid="footer-mobile" className="md:hidden max-w-6xl mx-auto px-4 py-8">
{/* 匯率來源與更新時間 */}
<div className="flex flex-col items-center justify-center gap-4 mb-6">
<a
Expand Down Expand Up @@ -254,7 +254,10 @@ export function Footer() {
</div>

{/* 電腦版完整 Footer - 整合簡潔風格 + SEO 連結 */}
<div className="hidden md:block container mx-auto px-4 md:px-6 py-12 max-w-6xl">
<div
data-testid="footer-desktop"
className="hidden md:block container mx-auto px-4 md:px-6 py-12 max-w-6xl"
>
{/* 匯率來源與更新時間 */}
<div className="flex flex-row items-center justify-center gap-4 mb-6">
<a
Expand Down Expand Up @@ -310,37 +313,6 @@ export function Footer() {
<p className="text-xs text-white/70 leading-relaxed">{t('footer.disclaimer')}</p>
</div>

{/* 快速連結 */}
<div className="flex flex-wrap items-center justify-center gap-5 text-xs text-white/80 mb-6">
<Link
to="/faq/"
className="inline-flex items-center gap-1.5 hover:text-white transition-colors duration-200"
>
<span aria-hidden="true" className="text-white/50">
?
</span>
{t('footer.faq')}
</Link>
<Link
to="/about/"
className="inline-flex items-center gap-1.5 hover:text-white transition-colors duration-200"
>
<span aria-hidden="true" className="text-white/50">
i
</span>
{t('footer.about')}
</Link>
<Link
to="/privacy/"
className="inline-flex items-center gap-1.5 hover:text-white transition-colors duration-200"
>
<span aria-hidden="true" className="text-white/50">
🔒
</span>
{t('footer.privacyPolicy')}
</Link>
</div>

{/* 分隔線 */}
<div className="w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent mb-8" />

Expand Down
71 changes: 25 additions & 46 deletions apps/ratewise/src/components/HomepageSEOSection.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
import { Link } from 'react-router-dom';
import { CURRENCY_DEFINITIONS } from '../features/ratewise/constants';
import { POPULAR_FROM_TWD_LINKS, POPULAR_TO_TWD_LINKS } from '../config/popular-currency-links';
import { HOMEPAGE_SEO } from '../config/seo-metadata';
import { AnswerCapsule } from './AnswerCapsule';

/** 熱門幣對:外幣換台幣(依台灣旅遊熱度排序)。 */
const HOT_TO_TWD = ['JPY', 'KRW', 'USD', 'EUR', 'HKD', 'SGD', 'THB', 'VND', 'AUD', 'GBP'] as const;

/** 熱門幣對:台幣換外幣(出國換匯常見方向)。 */
const HOT_FROM_TWD = [
'JPY',
'KRW',
'USD',
'EUR',
'HKD',
'SGD',
'THB',
'VND',
'AUD',
'GBP',
] as const;

export function HomepageSEOSection() {
const { content, howTo, faqContent, answerCapsule } = HOMEPAGE_SEO;

return (
<section
aria-labelledby="homepage-seo-heading"
aria-labelledby="homepage-seo-section-heading"
className="px-5 pt-5 pb-3 max-w-md mx-auto w-full"
>
<div className="rounded-[28px] border border-black/5 bg-surface shadow-card p-5">
Expand Down Expand Up @@ -72,40 +55,36 @@ export function HomepageSEOSection() {

{/* 熱門幣別換算:內部連結區塊,提升幣對落地頁 PageRank 傳遞。 */}
<div className="mt-4 rounded-[28px] border border-black/5 bg-surface shadow-card p-5">
<h2 className="text-lg font-black text-text">熱門幣別換算</h2>
<h2 id="popular-currency-heading" className="text-lg font-black text-text">
熱門幣別換算
</h2>

<p className="mt-1 text-xs text-text-muted">外幣換台幣</p>
<div className="mt-2 grid grid-cols-2 gap-1.5">
{HOT_TO_TWD.map((code) => {
const def = CURRENCY_DEFINITIONS[code];
return (
<Link
key={`to-twd-${code}`}
to={`/${code.toLowerCase()}-twd/`}
className="flex items-center gap-2 rounded-2xl border border-primary/10 bg-primary/5 px-3 py-2 text-xs font-bold text-primary transition hover:bg-primary/10"
>
<span aria-hidden="true">{def.flag}</span>
<span>{def.name}換台幣</span>
</Link>
);
})}
{POPULAR_TO_TWD_LINKS.map((link) => (
<Link
key={`to-twd-${link.code}`}
to={link.href}
className="flex items-center gap-2 rounded-2xl border border-primary/10 bg-primary/5 px-3 py-2 text-xs font-bold text-primary transition hover:bg-primary/10"
>
<span aria-hidden="true">{link.flag}</span>
<span>{link.label}</span>
</Link>
))}
</div>

<p className="mt-4 text-xs text-text-muted">台幣換外幣</p>
<div className="mt-2 grid grid-cols-2 gap-1.5">
{HOT_FROM_TWD.map((code) => {
const def = CURRENCY_DEFINITIONS[code];
return (
<Link
key={`from-twd-${code}`}
to={`/twd-${code.toLowerCase()}/`}
className="flex items-center gap-2 rounded-2xl border border-primary/10 bg-primary/5 px-3 py-2 text-xs font-bold text-primary transition hover:bg-primary/10"
>
<span aria-hidden="true">{def.flag}</span>
<span>台幣換{def.name}</span>
</Link>
);
})}
{POPULAR_FROM_TWD_LINKS.map((link) => (
<Link
key={`from-twd-${link.code}`}
to={link.href}
className="flex items-center gap-2 rounded-2xl border border-primary/10 bg-primary/5 px-3 py-2 text-xs font-bold text-primary transition hover:bg-primary/10"
>
<span aria-hidden="true">{link.flag}</span>
<span>{link.label}</span>
</Link>
))}
</div>
</div>

Expand Down
10 changes: 10 additions & 0 deletions apps/ratewise/src/components/LanguagePreferenceSync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { syncClientLanguagePreference } from '../i18n';

export function LanguagePreferenceSync() {
React.useEffect(() => {
syncClientLanguagePreference();
}, []);

return null;
}
8 changes: 4 additions & 4 deletions apps/ratewise/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Footer } from './Footer';
import { useUrlNormalization } from '../hooks/useUrlNormalization';
import { NonCriticalLazyBoundary } from './NonCriticalLazyBoundary';
import { PwaAppReadyBeacon } from './PwaAppReadyBeacon';
import { LanguagePreferenceSync } from './LanguagePreferenceSync';

const DecemberTheme = React.lazy(() => import('../features/calculator/easter-eggs/DecemberTheme'));

Expand Down Expand Up @@ -63,6 +64,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<React.StrictMode>
<HelmetProvider context={helmetContext}>
<ErrorBoundary>
<LanguagePreferenceSync />
<PwaAppReadyBeacon />
<div
data-scroll-container="layout"
Expand All @@ -71,10 +73,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
{/* 主要內容 */}
<main className="min-h-full [overscroll-behavior-y:contain]">{children}</main>

{/* 頁尾 - Stage 6:內部連結結構(僅桌面版顯示) */}
<div className="hidden md:block">
<Footer />
</div>
{/* 頁尾 - Stage 6:內部連結結構(Footer 內含行動版與桌面版佈局) */}
<Footer />
</div>
</ErrorBoundary>

Expand Down
Loading