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
2 changes: 1 addition & 1 deletion docs/content/7.releases/3.v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ To avoid hoisting issues with the new Unhead v2, the Nuxt OG Image v5 requires N

Please upgrade your Nuxt to continue using OG Image.

```ts
```sh
nuxi upgrade --force
```
54 changes: 54 additions & 0 deletions issues/#155.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Offload Fonts To Custom Storage Handler #155
Open
Open
Offload Fonts To Custom Storage Handler
#155
@harlan-zw
Description
harlan-zw
opened on Feb 1, 2024
With #151 we move fonts to Nitro's Server Assets, this solves several issues of making fetch requests at rutime.

However, this always bundles the font files, which is an issue on edge runtimes that have limited size constraints (vercel gives 2mb).

We need to offload the fonts to a custom storage driver that the end user would provide.

Activity
ptdev
ptdev commented on Apr 2, 2024
ptdev
(Luis Gomes)
on Apr 2, 2024
Hi, I came across this as well.

We are not using dynamic og image/text generation (we have ready made images) but the bundle always includes the Inter font which increases the bundle size tremendously.

And, even if we select another font on nuxt config (as described in the docs) it then apparently includes both the selected font and also the Inter font. (see attached screenshots, one with default configuration that includes the huge Inter font, and another one where I've selected the Poppins font on nuxt config but where it still includes the Inter font in the server bundle anyway, even though the documentation states that if we select another font on nuxt config then the Inter font will be disabled)

I suggest that it should be possible to (for example) set the fonts parameter on nuxt config to false to prevent including any fonts at all for these cases where dynamic image/text generation is not required.

Cheers.

Screenshot 2024-04-02 at 09 54 48 Screenshot 2024-04-02 at 09 55 08

ptdev
mentioned this on Jul 2, 2024
Adding a custom font doesn't disable the previous added fonts #229

harlan-zw
added
enhancement
New feature or request
on Sep 11, 2024
medz
medz commented 3 weeks ago
medz
(Seven Du)
3 weeks ago
Hi, is there any progress? Cloudflare worker limits the size to 10M, because we need to generate og images for non-English languages. Now the program is 24M after packaging. It would be great if it can be put in assets.

---

suggested solution:

- add
21 changes: 21 additions & 0 deletions issues/#354.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
fix: icons should be resolved from iconify local deps first over API request #354
Open
@harlan-zw
Description
harlan-zw
opened on Apr 5
πŸ› The bug
API is always used

πŸ› οΈ To reproduce
🌈 Expected behavior
Local dep should be used if available

ℹ️ Additional context
No response

---

tips:
- see @src/runtime/server/og-image/satori/transforms/emojis.ts, we need to change this to check if there's a local dependency for
- the iconify emoji set before hitting the API
22 changes: 11 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { SharpOptions } from 'sharp'
import type {
CompatibilityFlagEnvOverrides,
CompatibilityFlags,
EmojiStrategy,
FontConfig,
InputFontConfig,
OgImageComponent,
Expand Down Expand Up @@ -137,6 +138,17 @@ export interface ModuleOptions {
* This is similar behavior to using `nuxt/content` with `documentDriven: true`.
*/
strictNuxtContentPaths?: boolean

/**
* Strategy for resolving emoji icons.
*
* - 'auto': Automatically choose based on available dependencies (default)
* - 'local': Use local @iconify-json dependencies only
* - 'fetch': Use Iconify API to fetch emojis
*
* @default 'auto'
*/
emojiStrategy?: EmojiStrategy
}

export interface ModuleHooks {
Expand Down Expand Up @@ -176,6 +188,7 @@ export default defineNuxtModule<ModuleOptions>({
fonts: [],
runtimeCacheStorage: true,
debug: isDevelopment,
emojiStrategy: 'auto',
}
},
async setup(config, nuxt) {
Expand All @@ -201,6 +214,36 @@ export default defineNuxtModule<ModuleOptions>({
// legacy support
nuxt.options.alias['#nuxt-og-image-utils'] = resolve('./runtime/shared')

// Determine emoji strategy based on configuration and dependencies
const hasLocalIconify = await hasResolvableDependency(`@iconify-json/${config.defaults.emojis}`)
let finalEmojiStrategy = config.emojiStrategy

// Handle 'auto' strategy
if (finalEmojiStrategy === 'auto') {
finalEmojiStrategy = hasLocalIconify ? 'local' : 'fetch'
}

// Validate strategy against available dependencies
if (finalEmojiStrategy === 'local' && !hasLocalIconify) {
logger.warn(`emojiStrategy is set to 'local' but @iconify-json/${config.defaults.emojis} is not installed. Falling back to 'fetch'.`)
finalEmojiStrategy = 'fetch'
}

// Set emoji implementation based on final strategy
if (finalEmojiStrategy === 'local') {
logger.debug(`Using local dependency \`@iconify-json/${config.defaults.emojis}\` for emoji rendering.`)
nuxt.options.alias['#og-image/emoji-transform'] = resolve('./runtime/server/og-image/satori/transforms/emojis/local')
// add nitro virtual import for the iconify import
nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {}
nuxt.options.nitro.virtual['#og-image-virtual/iconify-json-icons.mjs'] = () => {
return `export { icons } from '@iconify-json/${config.defaults.emojis}/icons.json'`
}
}
else {
logger.info(`Using iconify API for emojis${hasLocalIconify ? ' (emojiStrategy: fetch)' : `, please install @iconify-json/${config.defaults.emojis} for local support`}.`)
nuxt.options.alias['#og-image/emoji-transform'] = resolve('./runtime/server/og-image/satori/transforms/emojis/fetch')
}

const preset = resolveNitroPreset(nuxt.options.nitro)
const targetCompatibility = getPresetNitroPresetCompatibility(preset)

Expand Down
107 changes: 100 additions & 7 deletions src/runtime/server/og-image/satori/plugins/emojis.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,113 @@
import type { VNode } from '../../../../types'
import type { OgImageRenderEventContext, VNode } from '../../../../types'
import { getEmojiSvg } from '#og-image/emoji-transform'
import { html as convertHtmlToSatori } from 'satori-html'
import { RE_MATCH_EMOJIS } from '../transforms/emojis/emoji-utils'
import { defineSatoriTransformer } from '../utils'

function isEmojiFilter(node: VNode) {
function isEmojiSvg(node: VNode) {
return node.type === 'svg'
&& typeof node.props?.['data-emoji'] !== 'undefined'
}

export default defineSatoriTransformer([
// need to make sure parent div has flex for the emoji to render inline
// Transform text nodes that contain emojis to replace them with SVG nodes
{
filter: (node: VNode) => {
if (typeof node.props?.children !== 'string') {
return false
}
node._emojiMatches = node.props.children.match(RE_MATCH_EMOJIS)
return node._emojiMatches
},
transform: async (node: VNode, ctx: OgImageRenderEventContext) => {
const text = node.props.children as string

// Find all emojis in the text
const matches = node._emojiMatches
if (!matches?.length)
return

const children: (VNode | string)[] = []
let lastIndex = 0

// Process each emoji match
for (const match of matches) {
const emojiIndex = text.indexOf(match, lastIndex)

// Add text before the emoji
if (emojiIndex > lastIndex) {
const beforeText = text.slice(lastIndex, emojiIndex)
if (beforeText)
children.push(beforeText)
}

// Try to get SVG for the emoji
const svg = await getEmojiSvg(ctx, match)
if (svg) {
// Parse the SVG and convert to Satori VNode instead of using img element
const node = convertHtmlToSatori(svg)
if (node?.props?.children?.[0]) {
const svgNode = node.props.children[0] as any as VNode
// Apply emoji styling
if (svgNode.props) {
svgNode.props['data-emoji'] = true
svgNode.props.style = {
...svgNode.props.style,
width: '1em',
height: '1em',
}
}
children.push(svgNode)
}
else {
// Fallback to original emoji if parsing fails
children.push(match)
}
}
else {
// If we can't get the SVG, keep the original emoji
children.push(match)
}

lastIndex = emojiIndex + match.length
}

// Add any remaining text after the last emoji
if (lastIndex < text.length) {
const afterText = text.slice(lastIndex)
if (afterText)
children.push(afterText)
}

// Update the node to have multiple children instead of a single text string
if (children.length > 1) {
// Convert all children to VNodes (wrap strings in divs)
const vnodeChildren: VNode[] = children.map((child) => {
if (typeof child === 'string') {
return {
type: 'div',
props: {
children: child,
},
} as VNode
}
return child
})

node.props.children = vnodeChildren
// Remove display styles as they're not supported by Satori
node.props.style = node.props.style || {}
}
},
},
// Keep the existing logic for styling containers with emoji SVGs
{
filter: (node: VNode) =>
['div', 'p'].includes(node.type) && Array.isArray(node.props?.children) && (node.props.children as VNode[]).some(isEmojiFilter),
['div', 'p'].includes(node.type) && Array.isArray(node.props?.children) && (node.props.children as VNode[]).some(child =>
(child.type === 'svg' && child.props?.['data-emoji']) || isEmojiSvg(child),
),
transform: (node: VNode) => {
node.props.style = node.props.style || {}
node.props.style.display = 'flex'
node.props.style.alignItems = 'center'
// check if any children nodes are just strings, wrap in a div
node.props.children = (node.props.children as VNode[]).map((child) => {
if (typeof child === 'string') {
Expand All @@ -25,7 +118,7 @@ export default defineSatoriTransformer([
},
}
}
if (child.props.class?.includes('emoji'))
if (child.props?.class?.includes('emoji'))
delete child.props.class
return child
})
Expand Down
Loading