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';