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
3 changes: 3 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ useHead({
{ property: 'og:url', content: canonicalUrl },
],
})

// Site-wide fallback OG image; individual pages override it with their own.
defineOgImage('Default')
</script>

<template>
Expand Down
65 changes: 65 additions & 0 deletions app/components/OgImage/Default.takumi.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { OG_COLORS, OG_LOGO, ogCardBaseStyle } from './theme'

withDefaults(
defineProps<{
title?: string
subtitle?: string
}>(),
{
title: 'COSCUP 2026 x UbuCon Asia',
subtitle: '',
},
)
</script>

<template>
<div :style="{ ...ogCardBaseStyle, padding: '80px' }">
<div :style="{ display: 'flex', alignItems: 'center' }">
<svg
height="61"
:viewBox="OG_LOGO.viewBox"
width="150"
xmlns="http://www.w3.org/2000/svg"
>
<path
:d="OG_LOGO.path"
:fill="OG_COLORS.accent"
fill-rule="evenodd"
:transform="OG_LOGO.transform"
/>
</svg>
<span :style="{ marginLeft: '28px', fontSize: '34px', fontWeight: 700, color: OG_COLORS.brand }">
Comment thread
rileychh marked this conversation as resolved.
COSCUP 2026 x UbuCon Asia
</span>
</div>

<div :style="{ display: 'flex', flexDirection: 'column' }">
<div
:style="{
display: 'block',
fontSize: '60px',
fontWeight: 400,
lineHeight: 1.25,
color: OG_COLORS.text,
}"
>
{{ title }}
</div>
<div
v-if="subtitle"
:style="{
display: 'block',
marginTop: '28px',
fontSize: '32px',
lineHeight: 1.4,
color: OG_COLORS.muted,
}"
>
{{ subtitle }}
</div>
</div>

<div :style="{ display: 'flex', height: '12px', width: '180px', backgroundColor: OG_COLORS.accent, borderRadius: '6px' }" />
</div>
</template>
68 changes: 68 additions & 0 deletions app/components/OgImage/Session.takumi.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
import { OG_COLORS, OG_LOGO, ogCardBaseStyle } from './theme'

withDefaults(
defineProps<{
title?: string
speakers?: string
room?: string
time?: string
}>(),
{
title: '',
speakers: '',
room: '',
time: '',
},
)
</script>

<template>
<div :style="{ ...ogCardBaseStyle, padding: '72px' }">
<div :style="{ display: 'flex', alignItems: 'center' }">
<svg
height="54"
:viewBox="OG_LOGO.viewBox"
width="132"
xmlns="http://www.w3.org/2000/svg"
>
<path
:d="OG_LOGO.path"
:fill="OG_COLORS.accent"
fill-rule="evenodd"
:transform="OG_LOGO.transform"
/>
</svg>
<span :style="{ marginLeft: '24px', fontSize: '30px', fontWeight: 700, color: OG_COLORS.brand }">
Comment thread
rileychh marked this conversation as resolved.
COSCUP 2026 x UbuCon Asia
</span>
</div>

<div
:style="{
display: 'block',
fontSize: '60px',
fontWeight: 400,
lineHeight: 1.25,
color: OG_COLORS.text,
}"
>
{{ title }}
</div>

<div :style="{ display: 'flex', flexDirection: 'column', fontSize: '32px', color: OG_COLORS.brand }">
<div
v-if="speakers"
:style="{ display: 'flex', alignItems: 'center', color: OG_COLORS.text, fontWeight: 400 }"
>
{{ speakers }}
</div>
<div
v-if="room || time"
:style="{ display: 'flex', marginTop: '12px', color: OG_COLORS.muted }"
>
{{ [room, time].filter(Boolean).join(' · ') }}
</div>
</div>
</div>
</template>
31 changes: 31 additions & 0 deletions app/components/OgImage/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Shared style constants for the OG image templates (Default + Session).
// Takumi only supports inline styles, so brand colors are duplicated here.
export const OG_COLORS = {
background: '#191733', // primary-800
brand: '#CCC7FF', // primary-100
muted: '#B2ABFE', // primary-200
accent: '#3C9838', // cp-green
text: '#ffffff',
}

// Mirrors the fonts in nuxt.config.ts; SC first for widest Han coverage.
export const OG_FONT_FAMILY = '\'Noto Sans SC\', \'Noto Sans TC\', \'Noto Sans JP\', \'Noto Sans KR\', sans-serif'
Comment thread
pan93412 marked this conversation as resolved.

// COSCUP wordmark. viewBox/path/transform are fixed; callers set width/height.
export const OG_LOGO = {
viewBox: '0 0 92.993 38',
path: 'M145.315,50.538l-6.406-19.789-3.327,5.666C95.4,17.41,78.007,54.746,78.007,54.746s14.47-27.308,49.942-5.39L124.631,55ZM171,36.779,164.6,17l-3.332,5.641C121.094,3.648,103.7,40.989,103.7,40.989s14.465-27.3,49.941-5.4l-3.327,5.648Z',
transform: 'translate(-78.007 -17)',
}

// Base card layout shared by both templates; callers add their own padding.
export const ogCardBaseStyle = {
width: '100%',
height: '100%',
Comment on lines +23 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep padded OG cards within the canvas

Both OG templates spread this base style and then add 72–80px of padding on the same root element. Because these templates use inline styles rather than a CSS reset/preflight, the root keeps the default content-box sizing, so width: '100%'/height: '100%' plus padding renders larger than the 1200x600 OG canvas and can clip the footer bar or session metadata at the bottom/right; add boxSizing: 'border-box' to the shared base style.

Useful? React with 👍 / 👎.

display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: OG_COLORS.background,
color: OG_COLORS.text,
fontFamily: OG_FONT_FAMILY,
} as const
6 changes: 6 additions & 0 deletions app/pages/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ useSeoMeta({
twitterTitle: () => page.value?.title,
twitterDescription: () => page.value?.description,
})

const ogImageProps = {
title: page.value?.title || 'COSCUP 2026 x UbuCon Asia',
subtitle: page.value?.description || '',
}
defineOgImage('Default', ogImageProps)
</script>

<template>
Expand Down
2 changes: 1 addition & 1 deletion app/pages/session.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const {
prerenderRoutes(
Object.values(data.value ?? {})
.flat()
.map((s) => `/session/${s.id}`),
.flatMap((s) => [`/session/${s.id}`, `/en/session/${s.id}`]),
)

useSeoMeta({
Expand Down
8 changes: 8 additions & 0 deletions app/pages/session/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ useSeoMeta({
twitterDescription: () => sessionInfo.value?.description,
})

const ogImageProps = {
title: sessionInfo.value?.title ?? '',
speakers: sessionInfo.value?.speakers.map((speaker) => speaker.name).join(', ') ?? '',
room: sessionInfo.value?.room ?? '',
time: sessionInfo.value?.time ?? '',
}
defineOgImage('Session', ogImageProps)

function close() {
router.push(localePath('/session'))
}
Expand Down
36 changes: 34 additions & 2 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,10 @@ export default defineNuxtConfig({
{ property: 'og:description', content: DESC },
{ property: 'og:site_name', content: TITLE },
{ property: 'og:type', content: 'website' },
{ property: 'og:image', content: `${URL}/coscup_logo.png` },
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: TITLE },
{ name: 'twitter:description', content: DESC },
{ name: 'twitter:site', content: '@coscup' },
{ name: 'twitter:image', content: `${URL}/coscup_logo.png` },
],
script: [
{
Expand Down Expand Up @@ -96,6 +94,16 @@ export default defineNuxtConfig({
output: {
publicDir: process.env.NUXT_OUTPUT_DIR || '.output/public',
},
prerender: {
concurrency: 4,
},
},

experimental: {
// nuxt-og-image breaks payload-route rendering on pages that call defineOgImage
// under baseURL "/2026" (its isInternalRoute misses "/2026/.../_payload.json"),
// making the renderer return no response (500). Disable payload extraction.
payloadExtraction: false,
Comment on lines +102 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does nuxt-og-image6.5.2 on Nuxt 4.4.x still requireexperimental.payloadExtraction = falsewhenapp.baseURLis a subpath like/2026, or is there a supported route-scoped workaround?

💡 Result:

As of nuxt-og-image version 6.5.2 running on Nuxt 4.4.x, setting experimental.payloadExtraction = false is no longer a documented or required workaround for handling app.baseURL subpaths. The module has received significant updates to its runtime configuration and path resolution logic [1][2][3], specifically improving support for custom baseURLs [4]. In modern versions of the module, the runtime configuration correctly consumes the app's baseURL [1], ensuring that OG image paths are generated with the correct base prefix automatically. If you are encountering issues with OG image path resolution or prerendering while using a subpath (e.g., /2026), ensure you are using the latest version of the module and that your site's baseURL is configured correctly in your nuxt.config.ts: export default defineNuxtConfig({ app: { baseURL: '/2026/', // Ensure trailing slash is included for consistency }, }) There is no supported route-scoped workaround for payload extraction because the issue you are referencing historically stemmed from how Nuxt itself handled base paths during static generation [5][6], which has been largely resolved in later Nuxt versions. If you face persistent 404s or path resolution errors, it is recommended to verify your environment configuration rather than disabling payload extraction, as disabling it may negatively impact performance and functionality [7].

Citations:


🏁 Script executed:

git ls-files nuxt.config.ts package.json pnpm-lock.yaml package-lock.json yarn.lock README.md docs 2>/dev/null

Repository: COSCUP/2026

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## files\n'
git ls-files nuxt.config.ts package.json pnpm-lock.yaml package-lock.json yarn.lock README.md docs 2>/dev/null || true

printf '\n## search\n'
rg -n "nuxt-og-image|payloadExtraction|baseURL|app\.baseURL|site\.url|site\.name" nuxt.config.ts package.json README.md docs 2>/dev/null || true

Repository: COSCUP/2026

Length of output: 916


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' nuxt.config.ts | cat -n

Repository: COSCUP/2026

Length of output: 7430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' package.json | cat -n

Repository: COSCUP/2026

Length of output: 2103


🌐 Web query:

nuxt-og-image 6.5.2 payloadExtraction baseURL subpath defineOgImage Nuxt 4.4.8

💡 Result:

In nuxt-og-image v6.5.2, configuration and behavior regarding base paths and payloads are handled through Nuxt's standard runtime configuration and internal module routing. Regarding the technical aspects mentioned: 1. payloadExtraction: While not explicitly defined as a top-level module option in documentation, nuxt-og-image relies on Nuxt's built-in payload extraction (used during build/prerendering). The module automatically handles the necessary context for generated images [1][2]. 2. baseURL and subpaths: When deploying a Nuxt application with a custom baseURL (e.g., set via app.baseURL in nuxt.config.ts), nuxt-og-image dynamically respects this configuration [1][3]. The module ensures that internal routes (such as the OG image generation endpoints, typically prefixed with /_og/) are correctly resolved relative to the defined base path [1][4]. If your application is hosted behind a reverse proxy, you must ensure the proxy correctly forwards the subpath to your Nuxt server to prevent 404 errors for these internal routes [5]. 3. defineOgImage: This composable remains the primary API for defining Open Graph images on a per-page basis [6][7]. In version 6.5.2, it has been refined to better support runtime configuration and path resolution [8][1]. You can use it as follows: defineOgImage({ component: 'MyComponent', props: { title: 'Hello' } }) For performance, if you are using an existing static image instead of generating one, use useSeoMeta instead to bypass the OG image generation logic: useSeoMeta({ ogImage: '/my-static-image.png' }) 4. Nuxt 4.4.8 Compatibility: nuxt-og-image v6.5.2 is designed to be compatible with modern Nuxt 4 releases. Ensure your environment matches the Nuxt 4 configurations for app.baseURL [3] and that no conflicting middleware is intercepting the /_og/ route path [4]. Top results: [1][3][7][4]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "payloadExtraction|nuxt-og-image|defineOgImage|baseURL|/2026" README.md nuxt.config.ts pnpm-lock.yaml

Repository: COSCUP/2026

Length of output: 1011


Remove the global payloadExtraction: false workaround. nuxt-og-image 6.5.2 already respects app.baseURL, so this disables payload splitting for the whole site unnecessarily.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nuxt.config.ts` around lines 102 - 106, Remove the global experimental
payloadExtraction override in nuxt.config.ts so the site no longer disables
payload splitting everywhere. Update the Nuxt config around the experimental
block to stop forcing payloadExtraction false, and rely on nuxt-og-image 6.5.2’s
baseURL-aware behavior instead; keep the config change localized to the existing
experimental settings in the Nuxt configuration.

},

imports: {
Expand All @@ -122,12 +130,34 @@ export default defineNuxtConfig({
'@unocss/nuxt',
'@nuxt/content',
'@nuxt/eslint',
'@nuxt/fonts',
'@nuxt/image',
'@nuxt/icon',
'@nuxtjs/i18n',
'nuxt-gtag',
'nuxt-og-image',
],

// Origin only — app.baseURL ('/2026') is appended automatically by nuxt-site-config.
site: {
url: 'https://coscup.org',
name: TITLE,
},

fonts: {
// global: true required so nuxt-og-image's renderer can use these fonts.
// Only weight 400: the Takumi renderer corrupts the 700-weight CJK glyphs
// (double-draw artifacts), so OG templates render CJK at regular weight.
// Order matters: Takumi picks one font per script run, so the widest-coverage
// CJK font (SC) must be first.
families: [
{ name: 'Noto Sans SC', weights: [400], global: true },
{ name: 'Noto Sans TC', weights: [400], global: true },
{ name: 'Noto Sans JP', weights: [400], global: true },
{ name: 'Noto Sans KR', weights: [400], global: true },
],
},

content: {
experimental: { nativeSqlite: true },
renderer: { anchorLinks: false },
Expand All @@ -145,6 +175,8 @@ export default defineNuxtConfig({
},

i18n: {
// Match site.url so nuxt-site-config can build absolute hreflang/canonical links.
baseUrl: 'https://coscup.org',
locales: [
{ code: 'en', name: 'English', language: 'en-US' },
{ code: 'zh', name: '中文', language: 'zh-Hant-TW' },
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"node": ">=24.0.0"
},
"scripts": {
"build": "nuxt generate",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 nuxt generate",
"dev": "nuxt dev",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
Expand All @@ -21,28 +21,35 @@
"@nuxt/image": "2.0.0",
"@nuxtjs/i18n": "10.4.0",
"@nuxtjs/mdc": "0.22.0",
"@resvg/resvg-js": "2.6.2",
"@vueuse/core": "14.3.0",
"csv-parse": "6.2.1",
"eslint": "10.5.0",
"leaflet": "1.9.4",
"nuxt": "4.4.8",
"nuxt-gtag": "4.1.0",
"satori": "0.26.0",
"ufo": "1.6.4",
"vue": "3.5.38",
"vue-router": "5.1.0",
"zod": "4.4.3"
},
"devDependencies": {
"@antfu/eslint-config": "9.0.0",
"@iconify-json/noto": "1.2.7",
"@iconify-json/tabler": "1.2.35",
"@nuxt/fonts": "0.14.0",
"@takumi-rs/core": "1.7.0",
"@types/geojson": "7946.0.16",
"@types/leaflet": "1.9.21",
"@unocss/eslint-plugin": "66.7.2",
"@unocss/extractor-mdc": "66.7.2",
"@unocss/nuxt": "66.7.2",
"@unocss/preset-wind4": "66.7.2",
"@unocss/transformer-directives": "66.7.2",
"cross-env": "10.1.0",
"eslint-plugin-format": "2.0.1",
"nuxt-og-image": "6.5.2",
"typescript": "6.0.3",
"unocss": "66.7.2",
"unocss-preset-theme": "0.14.1",
Expand Down
Loading