From 3c344e1035354dce407573602616247137dc069e Mon Sep 17 00:00:00 2001 From: kuanpo Date: Thu, 9 Jul 2026 17:40:11 +0800 Subject: [PATCH] feat(og): point every page at its own share card The 66 cards from #70 existed but nothing referenced them. BaseLayout resolves og:image and twitter:image through ogImagePath(toolId, locale), so /tools/gear-calculator advertises /og/zh/gear-calculator.png and its English twin advertises /og/en/gear-calculator.png. Pages with no toolId fall back to the home card. - twitter:card summary -> summary_large_image. The old value rendered a small square thumbnail, which is what the 512px logo was shaped for; a 1200x630 card needs the large variant or X crops it. - og:image:width / height / alt added. Facebook and LINE size the card from the tags before fetching the image, and skip the preview entirely on a cold cache if they have to guess. - meta name=author added. - The Chinese home description was 14 characters. Our own URL Audit scores anything under 50 as a warning, and it was right: the old text ("TTigger's tool collection and portfolio") said nothing about what the site does. Rewritten to name the tool families and the fact that nothing is uploaded. The English default was already 51 and stands. Tool pages already pass their own descriptions, so only the home default needed lengthening. e2e covers what unit tests can't reach here, since BaseLayout is an Astro component: the resolved og:image URL per locale, the card dimensions and twitter:card value, that the advertised image actually returns 200 image/png rather than a 404, and that the home description lands inside the 50-160 window the audit tool checks for. Tests: typecheck:api, 508 unit, 54 e2e (6 new). Co-Authored-By: Claude Opus 4.8 --- e2e/og-meta.spec.ts | 46 ++++++++++++++++++++++++++++++++++++ src/layouts/BaseLayout.astro | 14 ++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 e2e/og-meta.spec.ts 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 = { {title} + @@ -62,10 +67,13 @@ const jsonLd = { + + + - +