diff --git a/community-token/README.md b/community-token/README.md index 4228f6f..ffbb72d 100644 --- a/community-token/README.md +++ b/community-token/README.md @@ -29,16 +29,16 @@ Same pattern as the main site: meta tags are injected at build time (`scripts/ge **Open Graph / Twitter image** — place your artwork here (not in git until you add it): ``` -community-token/public/og/default.png +community-token/public/og/default.webp ``` -| Requirement | Value | -| ----------- | ------------------------------------------------------------- | -| Format | **PNG** (real PNG bytes — not JPEG renamed) | -| Size | **1200 × 630** px | -| Served at | `https://community-token.feelyourprotocol.org/og/default.png` | +| Requirement | Value | +| ----------- | ----------------------------------------------------------------- | +| Format | **WebP** (1200×630; PNG/JPEG source OK — export as WebP for size) | +| Size | **1200 × 630** px | +| Served at | `https://community-token.feelyourprotocol.org/og/default.webp` | -Main site equivalent: `public/og/default.png` → `feelyourprotocol.org/og/default.png`. Each subdomain keeps its own copy under its own `public/` folder. +Main site equivalent: `public/og/default.webp` → `feelyourprotocol.org/og/default.webp`. Each subdomain keeps its own copy under its own `public/` folder. Full deploy build (main site + community token + docs, rebuilt on server — `dist/` is not in git): diff --git a/community-token/public/og/default.png b/community-token/public/og/default.png deleted file mode 100644 index e1fe9d5..0000000 Binary files a/community-token/public/og/default.png and /dev/null differ diff --git a/community-token/public/og/default.webp b/community-token/public/og/default.webp new file mode 100644 index 0000000..f9cd293 Binary files /dev/null and b/community-token/public/og/default.webp differ diff --git a/community-token/src/content/pageSeo.ts b/community-token/src/content/pageSeo.ts index 836798a..238d704 100644 --- a/community-token/src/content/pageSeo.ts +++ b/community-token/src/content/pageSeo.ts @@ -10,7 +10,7 @@ export const CT_DESCRIPTION = truncateDescription( ) /** Stable path under `community-token/public/og/` — copied to `dist/community-token/og/` on build. */ -export const CT_OG_IMAGE_PATH = '/og/default.png' as const +export const CT_OG_IMAGE_PATH = '/og/default.webp' as const export const CT_OG_IMAGE_WIDTH = 1200 export const CT_OG_IMAGE_HEIGHT = 630 diff --git a/community-token/src/content/treasury.ts b/community-token/src/content/treasury.ts index 5838fa8..5e14590 100644 --- a/community-token/src/content/treasury.ts +++ b/community-token/src/content/treasury.ts @@ -48,7 +48,7 @@ export const ALLOCATION_CHART_2026: TreasuryChartSpec = { chartId: 'treasury-allocation-2026', title: 'Claimed fees (2026)', subtitle: 'Earmarked use', - generatedAt: '2026-06-23', + generatedAt: '2026-06-24', basis: { description: 'Sum of claims in treasury/2026/claims.md', totalEur: 2650, @@ -71,7 +71,7 @@ export const ALLOCATION_CHART_2026: TreasuryChartSpec = { { id: 'work-jun', label: 'June work', - eur: 1350, + eur: 1550, color: '#06b6d4', }, { @@ -83,7 +83,7 @@ export const ALLOCATION_CHART_2026: TreasuryChartSpec = { { id: 'unallocated', label: 'Unallocated', - eur: 1238.6, + eur: 1038.6, color: '#94a3b8', }, ], diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 95b30bf..3303dba 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -17,7 +17,7 @@ const DOCS_DESCRIPTION = 'Contributor guide and architecture docs for Feel Your Protocol — interactive Ethereum protocol explorations, E-Components, and open-source development.' /** Stable path under `docs/public/og/` — copied to `dist/docs/og/` on build. */ -const DOCS_OG_IMAGE_PATH = '/og/default.png' +const DOCS_OG_IMAGE_PATH = '/og/default.webp' const DOCS_OG_IMAGE = `${DOCS_ORIGIN}${DOCS_OG_IMAGE_PATH}` const DOCS_OG_IMAGE_WIDTH = '1200' const DOCS_OG_IMAGE_HEIGHT = '630' @@ -46,6 +46,7 @@ export default defineConfig({ ['meta', { property: 'og:image:width', content: DOCS_OG_IMAGE_WIDTH }], ['meta', { property: 'og:image:height', content: DOCS_OG_IMAGE_HEIGHT }], ['meta', { property: 'og:image:alt', content: DOCS_OG_IMAGE_ALT }], + ['meta', { property: 'og:image:type', content: 'image/webp' }], ['meta', { name: 'twitter:image', content: DOCS_OG_IMAGE }], ['meta', { name: 'twitter:image:alt', content: DOCS_OG_IMAGE_ALT }], ], diff --git a/docs/contributing/adding-an-exploration.md b/docs/contributing/adding-an-exploration.md index b1ba006..b842ff8 100644 --- a/docs/contributing/adding-an-exploration.md +++ b/docs/contributing/adding-an-exploration.md @@ -66,7 +66,8 @@ export const INFO: Exploration = { | `topic` | Yes | Topic ID this exploration belongs to. Must be one of the fixed set: `scaling`, `privacy`, `ux`, `security`, `robustness`, `interoperability`. Topics are static and not added via contributions — see [Architecture](/guide/architecture#topics) for the full list. | | `timeline` | Yes | Timeline ID for this exploration (e.g. `fusaka`, `glamsterdam`, `ready`, `research`, `ideas`). See [Architecture](/guide/architecture) for details. | | `tags` | Yes | Array of `Tag` enum values (max 3–4). Tags are broader technical concepts that must be reusable across explorations. New tags can be proposed — see [Architecture](/guide/architecture#tags) for rules and the current list. | -| `image` | No | Imported image for topic overview display — see [Images](/contributing/images) for format, palette, and style guidance | +| `image` | No | Full cover image for exploration pages — see [Images](/contributing/images) | +| `imageSmall` | No | Optional thumbnail (`image_small.*`) for home/topic cards; falls back to `image` | | `introText` | No | HTML-formatted introduction paragraph | | `usageText` | No | HTML-formatted usage instructions | | `creatorName` | No | Display name of the exploration's creator | diff --git a/docs/contributing/images.md b/docs/contributing/images.md index 2c81052..98991b5 100644 --- a/docs/contributing/images.md +++ b/docs/contributing/images.md @@ -34,14 +34,35 @@ The image file goes into your exploration folder as `image.webp` (or `.png`, `.j ```typescript import image from './image.webp' +import imageSmall from './image_small.webp' export const INFO: Exploration = { // ... image, + imageSmall, // optional — see Thumbnail below // ... } ``` +### Thumbnail (`image_small`) + +Exploration pages use the full cover image; **home page topic cards**, **topic overview pages**, and similar compact layouts use a smaller variant when available. + +| File | Role | +|------|------| +| `image.webp` (or `.jpg`, …) | Full cover — exploration sidebar and detail pages | +| `image_small.webp` | Optional thumbnail — same format, `_small` suffix | + +Naming: if the cover is `image.webp`, the thumbnail is `image_small.webp`; for `image.jpg` use `image_small.jpg`. + +| Property | Recommendation | +|----------|---------------| +| **Width** | ~300px (matches max display ~144–224px at 2× retina) | +| **Aspect ratio** | Same as cover | +| **File size** | Under 40 KB | + +If `image_small` is omitted, compact layouts fall back to the full cover image. + ## Color Palette This is the one area with strict rules. The color palette for your image must be derived from the **topic color** assigned to your exploration. This keeps the site visually coherent across contributions. @@ -395,8 +416,8 @@ Colors — strict: | Rule | Detail | |------|--------| | Format | WebP preferred; PNG, JPEG, SVG also accepted | -| Orientation | Portrait (3:4) | -| Resolution | 768×1024 recommended; min 512px short side, max 1536px long side | +| Cover | `image.webp` — 768×1024 recommended | +| Thumbnail | `image_small.webp` — optional ~300px for cards | | File size | < 200 KB (WebP) or < 400 KB (PNG/JPEG) | | Colors | Greyscale + topic color shades only; keep the two clearly separated | | Composition | Abstract, one focal point, generous white margins at edges | diff --git a/docs/public/og/default.png b/docs/public/og/default.png deleted file mode 100644 index 4c9d3c2..0000000 Binary files a/docs/public/og/default.png and /dev/null differ diff --git a/docs/public/og/default.webp b/docs/public/og/default.webp new file mode 100644 index 0000000..9c07eca Binary files /dev/null and b/docs/public/og/default.webp differ diff --git a/public/og/default.png b/public/og/default.png deleted file mode 100644 index 538b062..0000000 Binary files a/public/og/default.png and /dev/null differ diff --git a/public/og/default.webp b/public/og/default.webp new file mode 100644 index 0000000..503952d Binary files /dev/null and b/public/og/default.webp differ diff --git a/src/explorations/REGISTRY.ts b/src/explorations/REGISTRY.ts index ca9aa3a..7acf414 100644 --- a/src/explorations/REGISTRY.ts +++ b/src/explorations/REGISTRY.ts @@ -34,6 +34,8 @@ export interface Exploration { timeline: string tags: Tag[] image?: string + /** Optional thumbnail (~300px) for topic cards and compact layouts; same basename as `image` with `_small` suffix. */ + imageSmall?: string /** Optional max height for the cover image in the exploration sidebar (CSS length, e.g. `12rem`). */ imageBoxHeight?: string /** When set, exploration content may Teleport into `#exploration-right-panel`. */ @@ -55,6 +57,16 @@ export function pickRandom(items: T[]): T | undefined { return items[Math.floor(Math.random() * items.length)] } +export function getExplorationCoverImage(exploration: Exploration): string | undefined { + return exploration.image +} + +/** Thumbnail for home/topic cards and other compact layouts; falls back to cover image. */ +export function getExplorationThumbnailImage(exploration: Exploration): string | undefined { + if (!exploration.image) return undefined + return exploration.imageSmall ?? exploration.image +} + export function getRandomExplorationWithImage(): Exploration | undefined { return pickRandom(Object.values(EXPLORATIONS).filter((e) => e.image)) } @@ -62,7 +74,7 @@ export function getRandomExplorationWithImage(): Exploration | undefined { export function getRandomTopicExplorationImage(topicId: string): string | undefined { const images = Object.values(EXPLORATIONS) .filter((e) => e.topic === topicId && e.image) - .map((e) => e.image!) + .map((e) => getExplorationThumbnailImage(e)!) return pickRandom(images) } diff --git a/src/explorations/__tests__/registryImages.spec.ts b/src/explorations/__tests__/registryImages.spec.ts new file mode 100644 index 0000000..ab252ab --- /dev/null +++ b/src/explorations/__tests__/registryImages.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' + +import { + EXPLORATIONS, + getExplorationCoverImage, + getExplorationThumbnailImage, + getRandomTopicExplorationImage, +} from '@/explorations/REGISTRY' + +describe('exploration images', () => { + it('uses imageSmall for thumbnails when present', () => { + const exploration = EXPLORATIONS['eip-8024']! + + expect(getExplorationCoverImage(exploration)).toBe(exploration.image) + expect(getExplorationThumbnailImage(exploration)).toBe(exploration.imageSmall) + expect(getExplorationThumbnailImage(exploration)).not.toBe(exploration.image) + }) + + it('falls back to cover image when imageSmall is missing', () => { + const exploration = { ...EXPLORATIONS['eip-8024']!, imageSmall: undefined } + + expect(getExplorationThumbnailImage(exploration)).toBe(exploration.image) + }) + + it('returns thumbnail URLs for topic cards', () => { + const image = getRandomTopicExplorationImage('scaling') + + expect(image).toBeDefined() + expect( + Object.values(EXPLORATIONS) + .filter((e) => e.topic === 'scaling') + .some((e) => getExplorationThumbnailImage(e) === image), + ).toBe(true) + }) +}) diff --git a/src/explorations/eip-7594/image_small.webp b/src/explorations/eip-7594/image_small.webp new file mode 100644 index 0000000..585c89a Binary files /dev/null and b/src/explorations/eip-7594/image_small.webp differ diff --git a/src/explorations/eip-7594/info.ts b/src/explorations/eip-7594/info.ts index 2310416..df88165 100644 --- a/src/explorations/eip-7594/info.ts +++ b/src/explorations/eip-7594/info.ts @@ -2,6 +2,7 @@ import type { Exploration } from '@/explorations/REGISTRY' import { Tag } from '@/explorations/TAGS' import image from './image.webp' +import imageSmall from './image_small.webp' export const INFO: Exploration = { id: 'eip-7594', @@ -14,6 +15,7 @@ export const INFO: Exploration = { timeline: 'fusaka', tags: [Tag.PeerDAS], image, + imageSmall, introText: 'How do blob transactions change with PeerDAS? ' + 'With the Fusaka hardfork, data availability sampling (DAS) replaces single blob proofs with ' + diff --git a/src/explorations/eip-7883/image_small.webp b/src/explorations/eip-7883/image_small.webp new file mode 100644 index 0000000..a1b5752 Binary files /dev/null and b/src/explorations/eip-7883/image_small.webp differ diff --git a/src/explorations/eip-7883/info.ts b/src/explorations/eip-7883/info.ts index 515e389..0d34b68 100644 --- a/src/explorations/eip-7883/info.ts +++ b/src/explorations/eip-7883/info.ts @@ -2,6 +2,7 @@ import type { Exploration } from '@/explorations/REGISTRY' import { Tag } from '@/explorations/TAGS' import image from './image.webp' +import imageSmall from './image_small.webp' export const INFO: Exploration = { id: 'eip-7883', @@ -14,6 +15,7 @@ export const INFO: Exploration = { timeline: 'fusaka', tags: [Tag.GasCosts, Tag.Precompiles], image, + imageSmall, introText: 'How are ModExp gas costs changing with Fusaka? ' + 'EIP-7883 replaces the ModExp precompile gas formula with one that better reflects real ' + diff --git a/src/explorations/eip-7928/image_small.webp b/src/explorations/eip-7928/image_small.webp new file mode 100644 index 0000000..67cdb12 Binary files /dev/null and b/src/explorations/eip-7928/image_small.webp differ diff --git a/src/explorations/eip-7928/info.ts b/src/explorations/eip-7928/info.ts index 17cad01..c3aeab3 100644 --- a/src/explorations/eip-7928/info.ts +++ b/src/explorations/eip-7928/info.ts @@ -2,6 +2,7 @@ import type { Exploration } from '@/explorations/REGISTRY' import { Tag } from '@/explorations/TAGS' import image from './image.webp' +import imageSmall from './image_small.webp' export const INFO: Exploration = { id: 'eip-7928', @@ -14,6 +15,7 @@ export const INFO: Exploration = { timeline: 'glamsterdam', tags: [Tag.BAL, Tag.EVM], image, + imageSmall, imageBoxHeight: '19rem', rightPanel: true, introText: diff --git a/src/explorations/eip-7951/image_small.webp b/src/explorations/eip-7951/image_small.webp new file mode 100644 index 0000000..02f7153 Binary files /dev/null and b/src/explorations/eip-7951/image_small.webp differ diff --git a/src/explorations/eip-7951/info.ts b/src/explorations/eip-7951/info.ts index a0b4d97..f16bf5a 100644 --- a/src/explorations/eip-7951/info.ts +++ b/src/explorations/eip-7951/info.ts @@ -2,6 +2,7 @@ import type { Exploration } from '@/explorations/REGISTRY' import { Tag } from '@/explorations/TAGS' import image from './image.webp' +import imageSmall from './image_small.webp' export const INFO: Exploration = { id: 'eip-7951', @@ -14,6 +15,7 @@ export const INFO: Exploration = { timeline: 'fusaka', tags: [Tag.Precompiles, Tag.Signatures], image, + imageSmall, introText: 'Why add a secp256r1 precompile? ' + 'The curve (also known as P-256) is the native signing algorithm on ' + diff --git a/src/explorations/eip-8024/image_small.jpg b/src/explorations/eip-8024/image_small.jpg new file mode 100644 index 0000000..8469db6 Binary files /dev/null and b/src/explorations/eip-8024/image_small.jpg differ diff --git a/src/explorations/eip-8024/info.ts b/src/explorations/eip-8024/info.ts index c5c0850..5abf0eb 100644 --- a/src/explorations/eip-8024/info.ts +++ b/src/explorations/eip-8024/info.ts @@ -2,6 +2,7 @@ import type { Exploration } from '@/explorations/REGISTRY' import { Tag } from '@/explorations/TAGS' import image from './image.jpg' +import imageSmall from './image_small.jpg' export const INFO: Exploration = { id: 'eip-8024', @@ -14,6 +15,7 @@ export const INFO: Exploration = { timeline: 'glamsterdam', tags: [Tag.EVM], image, + imageSmall, imageBoxHeight: '19rem', rightPanel: true, introText: diff --git a/src/libs/__tests__/pageSeo.spec.ts b/src/libs/__tests__/pageSeo.spec.ts index a397806..6afa8d5 100644 --- a/src/libs/__tests__/pageSeo.spec.ts +++ b/src/libs/__tests__/pageSeo.spec.ts @@ -124,6 +124,7 @@ describe('pageSeo', () => { ) expect(html).toContain('') expect(html).toContain('') + expect(html).toContain('') expect(html).toContain('') expect(html).toContain('application/ld+json') }) diff --git a/src/libs/applyPageSeo.ts b/src/libs/applyPageSeo.ts index 3dafb8b..6b01db6 100644 --- a/src/libs/applyPageSeo.ts +++ b/src/libs/applyPageSeo.ts @@ -1,4 +1,5 @@ import type { PageSeo } from './seoCore' +import { inferOgImageType } from './seoCore' const JSON_LD_ID = 'page-seo-jsonld' @@ -78,6 +79,9 @@ export function applyPageSeo(seo: PageSeo): void { setProperty('og:image:height', String(seo.imageHeight)) setProperty('og:image:alt', seo.imageAlt) + const imageType = inferOgImageType(seo.imageUrl) + setProperty('og:image:type', imageType) + setMeta('twitter:card', 'summary_large_image') setMeta('twitter:title', seo.title) setMeta('twitter:description', seo.description) diff --git a/src/libs/pageSeo.ts b/src/libs/pageSeo.ts index 6ecca47..a0d9cb8 100644 --- a/src/libs/pageSeo.ts +++ b/src/libs/pageSeo.ts @@ -27,7 +27,7 @@ export const DEFAULT_DESCRIPTION = 'Interactive open-source explorations of Ethereum protocol changes. Real EVM and crypto libraries running in your browser.' /** Stable path under `public/og/` — copied verbatim to `dist/website/og/` on build. */ -export const DEFAULT_OG_IMAGE_PATH = '/og/default.png' +export const DEFAULT_OG_IMAGE_PATH = '/og/default.webp' export const DEFAULT_OG_IMAGE_WIDTH = 1200 export const DEFAULT_OG_IMAGE_HEIGHT = 630 diff --git a/src/libs/seoCore.ts b/src/libs/seoCore.ts index faafab7..40bc3a4 100644 --- a/src/libs/seoCore.ts +++ b/src/libs/seoCore.ts @@ -38,6 +38,14 @@ export function escapeHtml(text: string): string { .replace(/"/g, '"') } +export function inferOgImageType(imageUrl: string): string | undefined { + const path = imageUrl.split('?')[0]?.toLowerCase() ?? '' + if (path.endsWith('.webp')) return 'image/webp' + if (path.endsWith('.png')) return 'image/png' + if (path.endsWith('.jpg') || path.endsWith('.jpeg')) return 'image/jpeg' + return undefined +} + /** Inject title, meta, canonical, Open Graph, and JSON-LD into a Vite-built index.html shell. */ export function injectSeoIntoHtml( html: string, @@ -56,12 +64,20 @@ export function injectSeoIntoHtml( ``, ``, ``, + ] + + const imageType = inferOgImageType(seo.imageUrl) + if (imageType) { + headTags.push(``) + } + + headTags.push( ``, ``, ``, ``, ``, - ] + ) if (seo.noindex) { headTags.push('') diff --git a/src/views/NotFoundView.vue b/src/views/NotFoundView.vue index 4d3d817..e13893d 100644 --- a/src/views/NotFoundView.vue +++ b/src/views/NotFoundView.vue @@ -1,5 +1,8 @@