diff --git a/e2e/og-meta.spec.ts b/e2e/og-meta.spec.ts new file mode 100644 index 0000000..57bfd65 --- /dev/null +++ b/e2e/og-meta.spec.ts @@ -0,0 +1,46 @@ +import { test, expect, type Page } from '@playwright/test'; + +const meta = (page: Page, selector: string) => page.locator(`head > meta[${selector}]`).getAttribute('content'); + +test('工具頁的分享卡指向該工具、該語系的圖', async ({ page }) => { + await page.goto('/tools/url-audit'); + expect(await meta(page, 'property="og:image"')).toBe('https://tiglet.vercel.app/og/zh/url-audit.png'); + expect(await meta(page, 'name="twitter:image"')).toBe('https://tiglet.vercel.app/og/zh/url-audit.png'); +}); + +test('英文頁用英文卡', async ({ page }) => { + await page.goto('/en/tools/url-audit'); + expect(await meta(page, 'property="og:image"')).toBe('https://tiglet.vercel.app/og/en/url-audit.png'); +}); + +test('首頁用首頁卡,雙語各一張', async ({ page }) => { + await page.goto('/'); + expect(await meta(page, 'property="og:image"')).toBe('https://tiglet.vercel.app/og/zh/home.png'); + await page.goto('/en/'); + expect(await meta(page, 'property="og:image"')).toBe('https://tiglet.vercel.app/og/en/home.png'); +}); + +test('大圖分享卡:twitter:card 與尺寸標註', async ({ page }) => { + await page.goto('/tools/gear-calculator'); + expect(await meta(page, 'name="twitter:card"')).toBe('summary_large_image'); + expect(await meta(page, 'property="og:image:width"')).toBe('1200'); + expect(await meta(page, 'property="og:image:height"')).toBe('630'); + expect(await meta(page, 'property="og:image:alt"')).toBe('齒比計算器 — Tiglet'); +}); + +test('分享卡圖真的存在(不是 404 的連結)', async ({ page, request }) => { + await page.goto('/tools/stage-profile'); + const src = await meta(page, 'property="og:image"'); + const res = await request.get(new URL(src!).pathname); + expect(res.status()).toBe(200); + expect(res.headers()['content-type']).toContain('image/png'); +}); + +test('GEO:作者標註與夠長的首頁描述', async ({ page }) => { + await page.goto('/'); + expect(await meta(page, 'name="author"')).toBe('TTigger'); + // 健檢工具自己的規則:description 落在 50–160 字才算 pass + const desc = await meta(page, 'name="description"'); + expect(desc!.length).toBeGreaterThanOrEqual(50); + expect(desc!.length).toBeLessThanOrEqual(160); +}); diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 9c892c6..4332d12 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -5,18 +5,22 @@ import Footer from '../components/Footer.astro'; import CommandPalette from '../components/CommandPalette.tsx'; import TrackVisit from '../components/TrackVisit.tsx'; import { localeFromPath, switchLocalePath } from '../lib/i18n'; +import { ogImagePath } from '../lib/og'; interface Props { title?: string; description?: string; toolId?: string; } const locale = localeFromPath(Astro.url.pathname); const { title = locale === 'en' ? 'Tiglet — small browser tools' : 'Tiglet — 小工具站', - description = locale === 'en' ? 'A clean, no-login collection of small browser tools.' : 'TTigger 的小工具集與作品集。', + description = locale === 'en' + ? 'A clean, no-login collection of small browser tools.' + : '一組乾淨、免登入的瀏覽器小工具:計算、文字處理、圖片、單車與遊戲,全部在你的瀏覽器裡完成,資料不上傳。', toolId, } = Astro.props; const site = Astro.site ?? new URL('https://tiglet.vercel.app'); const canonical = new URL(Astro.url.pathname, site); -const ogImage = new URL('/pwa-512x512.png', site); +// 每個工具、每個語系一張 1200×630 分享卡;由 `npm run og` 產生 +const ogImage = new URL(ogImagePath(toolId, locale), site); // hreflang 對照連結(zh 為 x-default) const zhUrl = new URL(switchLocalePath(Astro.url.pathname, 'zh'), site); const enUrl = new URL(switchLocalePath(Astro.url.pathname, 'en'), site); @@ -50,6 +54,7 @@ const jsonLd = {