diff --git a/.changeset/curly-bottles-knock.md b/.changeset/curly-bottles-knock.md new file mode 100644 index 0000000000..880746f8f9 --- /dev/null +++ b/.changeset/curly-bottles-knock.md @@ -0,0 +1,5 @@ +--- +'@coinbase/onchainkit': patch +--- + +- **feat**: automated the `og:image` and `og:title` properties for `getFrameHtmlResponse` and `FrameMetadata`. By @zizzamia #109 diff --git a/src/components/FrameMetadata.test.tsx b/src/components/FrameMetadata.test.tsx index 3a7b49a9c0..b408e9a2b7 100644 --- a/src/components/FrameMetadata.test.tsx +++ b/src/components/FrameMetadata.test.tsx @@ -15,7 +15,7 @@ describe('FrameMetadata', () => { expect( meta.container.querySelector('meta[property="fc:frame:image"]')?.getAttribute('content'), ).toBe('https://example.com/image.png'); - expect(meta.container.querySelectorAll('meta').length).toBe(2); + expect(meta.container.querySelectorAll('meta').length).toBe(3); }); it('renders with image src', () => { @@ -23,7 +23,7 @@ describe('FrameMetadata', () => { expect( meta.container.querySelector('meta[property="fc:frame:image"]')?.getAttribute('content'), ).toBe('https://example.com/image.png'); - expect(meta.container.querySelectorAll('meta').length).toBe(2); + expect(meta.container.querySelectorAll('meta').length).toBe(3); }); it('renders with image aspect ratio', () => { @@ -38,7 +38,7 @@ describe('FrameMetadata', () => { .querySelector('meta[property="fc:frame:image:aspect_ratio"]') ?.getAttribute('content'), ).toBe('1:1'); - expect(meta.container.querySelectorAll('meta').length).toBe(3); + expect(meta.container.querySelectorAll('meta').length).toBe(4); }); it('renders with input', () => { @@ -49,7 +49,7 @@ describe('FrameMetadata', () => { expect( meta.container.querySelector('meta[property="fc:frame:input:text"]')?.getAttribute('content'), ).toBe('test'); - expect(meta.container.querySelectorAll('meta').length).toBe(3); + expect(meta.container.querySelectorAll('meta').length).toBe(4); }); it('renders with two basic buttons', () => { @@ -79,7 +79,7 @@ describe('FrameMetadata', () => { ?.getAttribute('content'), ).toBe('post_redirect'); // Length - expect(meta.container.querySelectorAll('meta').length).toBe(5); + expect(meta.container.querySelectorAll('meta').length).toBe(6); }); it('renders with all buttons', () => { @@ -156,7 +156,7 @@ describe('FrameMetadata', () => { ?.getAttribute('content'), ).toBe('https://zizzamia.xyz/api/frame/link'); // Length - expect(meta.container.querySelectorAll('meta').length).toBe(11); + expect(meta.container.querySelectorAll('meta').length).toBe(12); }); it('renders with post_url', () => { @@ -167,7 +167,7 @@ describe('FrameMetadata', () => { expect( meta.container.querySelector('meta[property="fc:frame:post_url"]')?.getAttribute('content'), ).toBe('https://example.com'); - expect(meta.container.querySelectorAll('meta').length).toBe(3); + expect(meta.container.querySelectorAll('meta').length).toBe(4); }); it('renders with refresh_period', () => { @@ -178,7 +178,7 @@ describe('FrameMetadata', () => { .querySelector('meta[property="fc:frame:refresh_period"]') ?.getAttribute('content'), ).toBe('10'); - expect(meta.container.querySelectorAll('meta').length).toBe(3); + expect(meta.container.querySelectorAll('meta').length).toBe(4); }); it('renders with wrapper', () => { @@ -191,7 +191,7 @@ describe('FrameMetadata', () => { expect(meta.container.querySelector('#wrapper')).not.toBeNull(); expect(meta.container.querySelector('meta[property="fc:frame:image"]')).not.toBeNull(); - expect(meta.container.querySelectorAll('meta').length).toBe(2); + expect(meta.container.querySelectorAll('meta').length).toBe(3); }); it('renders with action mint', () => { @@ -223,7 +223,7 @@ describe('FrameMetadata', () => { .querySelector('meta[property="fc:frame:button:1:target"]') ?.getAttribute('content'), ).toBe('https://zizzamia.xyz/api/frame/mint'); - expect(meta.container.querySelectorAll('meta').length).toBe(5); + expect(meta.container.querySelectorAll('meta').length).toBe(6); }); it('renders with action link', () => { @@ -255,7 +255,7 @@ describe('FrameMetadata', () => { .querySelector('meta[property="fc:frame:button:1:target"]') ?.getAttribute('content'), ).toBe('https://zizzamia.xyz/api/frame/link'); - expect(meta.container.querySelectorAll('meta').length).toBe(5); + expect(meta.container.querySelectorAll('meta').length).toBe(6); }); it('should not render action target if action is not link or mint', () => { @@ -267,6 +267,36 @@ describe('FrameMetadata', () => { />, ); expect(meta.container.querySelector('meta[property="fc:frame:button:1:target"')).toBeNull(); - expect(meta.container.querySelectorAll('meta').length).toBe(5); + expect(meta.container.querySelectorAll('meta').length).toBe(6); + }); + + it('should set og:description', () => { + const meta = render( + , + ); + expect( + meta.container.querySelector('meta[property="og:description"]')?.getAttribute('content'), + ).toBe('This is the description'); + expect(meta.container.querySelectorAll('meta').length).toBe(4); + }); + + it('should set og:title', () => { + const meta = render( + , + ); + expect(meta.container.querySelector('meta[property="og:title"]')?.getAttribute('content')).toBe( + 'This is the title', + ); + expect(meta.container.querySelectorAll('meta').length).toBe(4); + }); + + it('should not render og:description and og:title if not provided', () => { + const meta = render(); + expect(meta.container.querySelector('meta[property="og:description"]')).toBeNull(); + expect(meta.container.querySelector('meta[property="og:title"]')).toBeNull(); + expect(meta.container.querySelectorAll('meta').length).toBe(3); }); }); diff --git a/src/components/FrameMetadata.tsx b/src/components/FrameMetadata.tsx index 5e1d3fb2bb..0579540392 100644 --- a/src/components/FrameMetadata.tsx +++ b/src/components/FrameMetadata.tsx @@ -2,6 +2,8 @@ import { Fragment } from 'react'; import type { FrameMetadataType, FrameImageMetadata } from '../core/types'; type FrameMetadataReact = FrameMetadataType & { + ogDescription?: string; + ogTitle?: string; wrapper?: React.ComponentType; }; @@ -40,6 +42,8 @@ type FrameMetadataReact = FrameMetadataType & { * @param {Array<{ label: string, action?: string }>} props.buttons - The buttons. * @param {string | { src: string, aspectRatio?: string }} props.image - The image URL. * @param {string} props.input - The input text. + * @param {string} props.ogDescription - The Open Graph description. + * @param {string} props.ogTitle - The Open Graph title. * @param {string} props.postUrl - The post URL. * @param {number} props.refreshPeriod - The refresh period. * @param {React.ComponentType | undefined} props.wrapper - The wrapper component meta tags are rendered in. @@ -49,6 +53,8 @@ export function FrameMetadata({ buttons, image, input, + ogDescription, + ogTitle, postUrl, post_url, refreshPeriod, @@ -68,10 +74,12 @@ export function FrameMetadata({ // with Helmet as a wrapper component, it is crucial to flatten the Buttons loop. return ( + {!!ogDescription && } + {!!ogTitle && } + {!!imageSrc && } {!!aspectRatio && } - {!!input && } {!!button1 && } diff --git a/src/core/getFrameHtmlResponse.test.ts b/src/core/getFrameHtmlResponse.test.ts index 852b53b228..edc77fdbf4 100644 --- a/src/core/getFrameHtmlResponse.test.ts +++ b/src/core/getFrameHtmlResponse.test.ts @@ -23,6 +23,8 @@ describe('getFrameHtmlResponse', () => { expect(html).toBe(` + + @@ -32,6 +34,7 @@ describe('getFrameHtmlResponse', () => { + @@ -53,10 +56,13 @@ describe('getFrameHtmlResponse', () => { expect(html).toBe(` + + + @@ -75,10 +81,13 @@ describe('getFrameHtmlResponse', () => { expect(html).toBe(` + + + @@ -95,10 +104,13 @@ describe('getFrameHtmlResponse', () => { expect(html).toBe(` + + + @@ -114,10 +126,13 @@ describe('getFrameHtmlResponse', () => { expect(html).toBe(` + + + @@ -136,6 +151,7 @@ describe('getFrameHtmlResponse', () => { expect(html).toContain( '', ); + expect(html).toContain(''); expect(html).toContain( '', ); @@ -152,6 +168,7 @@ describe('getFrameHtmlResponse', () => { expect(html).toContain( '', ); + expect(html).toContain(''); expect(html).toContain( '', ); @@ -168,6 +185,7 @@ describe('getFrameHtmlResponse', () => { expect(html).toContain( '', ); + expect(html).toContain(''); expect(html).toContain(''); expect(html).not.toContain('fc:frame:post_url'); }); @@ -204,6 +222,30 @@ describe('getFrameHtmlResponse', () => { expect(html).toContain(''); expect(html).not.toContain('fc:frame:button:1:target'); }); + + it('should set og:description and og:title to default values if not provided', () => { + const html = getFrameHtmlResponse({ + buttons: [{ label: 'button1' }], + image: 'image', + postUrl: 'post_url', + }); + + expect(html).toContain(''); + expect(html).toContain(''); + }); + + it('should set og:description and og:title to provided values', () => { + const html = getFrameHtmlResponse({ + buttons: [{ label: 'button1' }], + image: 'image', + postUrl: 'post_url', + ogDescription: 'description', + ogTitle: 'title', + }); + + expect(html).toContain(''); + expect(html).toContain(''); + }); }); export { getFrameHtmlResponse }; diff --git a/src/core/getFrameHtmlResponse.ts b/src/core/getFrameHtmlResponse.ts index 92fb0bce1c..603bc797b6 100644 --- a/src/core/getFrameHtmlResponse.ts +++ b/src/core/getFrameHtmlResponse.ts @@ -1,11 +1,18 @@ import { FrameMetadataType, FrameImageMetadata } from './types'; +type FrameMetadataHTMLResponse = FrameMetadataType & { + ogDescription?: string; + ogTitle?: string; +}; + /** * Returns an HTML string containing metadata for a new valid frame. * * @param buttons: The buttons to use for the frame. * @param image: The image to use for the frame. * @param input: The text input to use for the frame. + * @param ogDescription: The Open Graph description for the frame. + * @param ogTitle: The Open Graph title for the frame. * @param postUrl: The URL to post the frame to. * @param refreshPeriod: The refresh period for the image used. * @returns An HTML string containing metadata for the frame. @@ -14,21 +21,20 @@ function getFrameHtmlResponse({ buttons, image, input, + ogDescription, + ogTitle, postUrl, post_url, refreshPeriod, refresh_period, -}: FrameMetadataType): string { - // Set the image metadata if it exists. - let imageHtml = ''; - if (typeof image === 'string') { - imageHtml = ` \n`; - } else { - imageHtml = ` \n`; - if (image.aspectRatio) { - imageHtml += ` \n`; - } +}: FrameMetadataHTMLResponse): string { + const imgSrc = typeof image === 'string' ? image : image.src; + const ogImageHtml = ` \n`; + let imageHtml = ` \n`; + if (typeof image !== 'string' && image.aspectRatio) { + imageHtml += ` \n`; } + // Set the input metadata if it exists. const inputHtml = input ? ` \n` @@ -67,8 +73,10 @@ function getFrameHtmlResponse({ let html = ` + + -${buttonsHtml}${imageHtml}${inputHtml}${postUrlHtml}${refreshPeriodHtml} +${buttonsHtml}${ogImageHtml}${imageHtml}${inputHtml}${postUrlHtml}${refreshPeriodHtml} `; diff --git a/src/version.ts b/src/version.ts index 42a5ab6c24..358203cd28 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '0.6.0'; +export const version = '0.6.1';