15
loading...
This website collects cookies to deliver better user experience
Title: short text
Content: single reference (in our case, referencing blog posts)
Type: short text
Image: media
URL: short text
npx @contentful/create-contentful-app init open-graph
cd open-graph
npm start
http://localhost:3000
. In order to see our app in Contentful, we will need to create an AppDefinition and assign our app to show up in the correct locations.Name: Open Graph
App URL: http://localhost:3000 (remember this is running on our machine)
First location: Field - short text
Second location: Page
import React, {useState, useEffect} from 'react';
import tokens from '@contentful/forma-36-tokens'
import { TextInput, Note, Button } from '@contentful/forma-36-react-components';
import { FieldExtensionSDK } from '@contentful/app-sdk';
const Field = (props: FieldProps) => {
// Get the title of the field and create a setter
const [title, setTitle] = useState(props.sdk.field.getValue() || null);
content
reference field. When the content
field has been populated with data, grab the entry, find the title and set that as the title in state. Also save that as the title field's value in the Open Graph content type.useEffect(() => {
// Resize the field so it isn't cut off in the UI
props.sdk.window.startAutoResizer();
// when the value of the `content` field changes, set the title
props.sdk.entry.fields.content.onValueChanged((entry) => {
if (!entry) {
setTitle(null);
return;
}
// The content field is a reference to a blog post.
// We want to grab that full entry to get the `title` of it
props.sdk.space.getEntry<BlogPost>(entry.sys.id).then((data) => {
if (data.fields.title['en-US'] !== title) {
const title = data.fields.title['en-US'];
setTitle(title);
props.sdk.field.setValue(title);
}
});
});
});
if (title === null) {
return <Note noteType="warning">Link a blog post below to assign a title.</Note>
}
return (
<>
<TextInput disabled value={title} style={{marginBottom: tokens.spacingM}}/>
<Button onClick={() => props.sdk.navigator.openCurrentAppPage({path: `/${props.sdk.entry.getSys().id}`})} buttonType="naked">Preview Open Graph</Button>
</>
);
};
import React, {useState, useEffect} from 'react';
import tokens from '@contentful/forma-36-tokens'
import { TextInput, Note, Button } from '@contentful/forma-36-react-components';
import { FieldExtensionSDK } from '@contentful/app-sdk';
interface FieldProps {
sdk: FieldExtensionSDK;
}
// Custom type to denote a blog post content type
interface BlogPost {
sys: {
id: string;
};
fields: {
body: object;
title: {
'en-US': string;
};
};
};
const Field = (props: FieldProps) => {
// Get the title of the field and create a setter
const [title, setTitle] = useState(props.sdk.field.getValue() || null);
useEffect(() => {
// Resize the field so it isn't cut off in the UI
props.sdk.window.startAutoResizer();
// when the value of the `content` field changes, set the title
props.sdk.entry.fields.content.onValueChanged((entry) => {
if (!entry) {
setTitle(null);
return;
}
// The content field is a reference to a blog post.
// We want to grab that full entry to get the `title` of it
props.sdk.space.getEntry<BlogPost>(entry.sys.id).then((data) => {
if (data.fields.title['en-US'] !== title) {
const title = data.fields.title['en-US'];
setTitle(title);
props.sdk.field.setValue(title);
}
});
});
});
if (title === null) {
return <Note noteType="warning">Link a blog post below to assign a title.</Note>
}
return (
<>
<TextInput disabled value={title} style={{marginBottom: tokens.spacingM}}/>
<Button onClick={() => props.sdk.navigator.openCurrentAppPage({path: `/${props.sdk.entry.getSys().id}`})} buttonType="naked">Preview Open Graph</Button>
</>
);
};
export default Field;
import React, { useState, useEffect } from 'react';
import {
Card,
Note,
Heading,
Paragraph,
Typography,
TextLink,
Button,
SkeletonBodyText,
SkeletonContainer,
SkeletonDisplayText,
} from '@contentful/forma-36-react-components';
import tokens from '@contentful/forma-36-tokens';
import { PageExtensionSDK } from '@contentful/app-sdk';
In the Page component, check the URL parameters that we passed in when the editor clicked the button:
const Page = (props: PageProps) => {
// false indicates an error occurred
// otherwise the entry is loading or loaded
const [entry, setEntry] = useState<OpenGraphPreview | false>();
useEffect(() => {
// Get the entry ID which is passed in via the URL
const entryId = props.sdk.parameters?.invocation?.path.replace('/', '');
// If no entry ID exists, show an error message by setting content to false
if (!entryId) {
setEntry(false);
return;
}
sdk.parameters.invocation.path
to access the custom path in the URL of our app, which was passed in by sdk.navigator.openCurrentAppPage
from our field component as the on click function of the button.useEffect
function with some network calls to get all the data we need to display the preview:// Get the entry data by getting the linked asset and content body
props.sdk.space.getEntry<OpenGraphEntry>(entryId).then((data) => {
Promise.all([
// Grabs the Image asset of the Open Graph content type
props.sdk.space.getAsset<Asset>(
data.fields.image['en-US'].sys.id
),
// Grabs the long text body of the blog post
props.sdk.space.getEntry<Content>(
data.fields.content['en-US'].sys.id
),
]).then(([asset, content]) => {
// combine the data from the two `space` calls
setEntry({
title: data.fields.title['en-US'],
imageUrl: asset.fields.file['en-US'].url,
previewBody: content.fields.body['en-US'],
url: data.fields.url['en-US'],
id: entryId,
});
});
});
}, []);
Promise.all
to get data about the entry and the image asset which we will use in our preview.if (entry === false) {
return <Note noteType="negative">Error retrieving entry!</Note>;
}
return (
<div
style={{
display: 'flex',
width: '100%',
height: '100vh',
alignItems: 'center',
marginTop: tokens.spacingXl,
flexDirection: 'column',
}}
>
<div style={{ justifyContent: 'center' }}>
<SkeletonContainer>
<SkeletonDisplayText numberOfLines={1} />
<SkeletonBodyText numberOfLines={3} offsetTop={35} />
</SkeletonContainer>
</div>
<Typography>
<Heading>
Open Graph Preview
</Heading>
</Typography>
<Card style={{ width: '260px' }}>
{!entry ?
<SkeletonContainer>
<SkeletonDisplayText numberOfLines={1} />
<SkeletonBodyText numberOfLines={3} offsetTop={35} />
</SkeletonContainer> :
<>
<Typography>
<TextLink href={entry.url}>{entry.title}</TextLink>
<Paragraph>{entry.previewBody}</Paragraph>
</Typography>
<img src={entry.imageUrl} alt="" style={{ width: '200px' }} />
</>}
</Card>
<Button
buttonType="muted"
disabled={!entry}
onClick={() => props.sdk.navigator.openEntry(entry!.id)}
style={{ margin: `${tokens.spacingXl} 0` }}
>
Back to entry
</Button>
<div style={{ justifyContent: 'center' }}>
<SkeletonContainer>
<SkeletonDisplayText numberOfLines={1} />
<SkeletonBodyText numberOfLines={10} offsetTop={35} />
</SkeletonContainer>
</div>
</div>
);
};
import React, { useState, useEffect } from 'react';
import {
Card,
Note,
Heading,
Paragraph,
Typography,
TextLink,
Button,
SkeletonBodyText,
SkeletonContainer,
SkeletonDisplayText,
} from '@contentful/forma-36-react-components';
import tokens from '@contentful/forma-36-tokens';
import { PageExtensionSDK } from '@contentful/app-sdk';
interface PageProps {
sdk: PageExtensionSDK;
}
// An open graph content type
interface OpenGraphEntry {
fields: {
title: {
'en-US': string;
};
type: {
'en-US': string;
};
url: {
'en-US': string;
};
image: {
'en-US': {
sys: {
id: string;
};
};
};
content: {
'en-US': {
sys: {
id: string;
};
};
};
};
sys: {
id: string;
};
}
// A custom shape for the entry we are going to render in our UI
interface OpenGraphPreview {
id: string;
title: string;
imageUrl: string;
previewBody: string;
url: string;
}
interface Content {
fields: {
body: {
'en-US': string;
};
};
}
interface Asset {
fields: {
file: {
'en-US': {
url: string;
};
};
};
}
const Page = (props: PageProps) => {
// false indicates an error occurred
// otherwise the entry is loading or loaded
const [entry, setEntry] = useState<OpenGraphPreview | false>();
useEffect(() => {
// Get the entry ID which is passed in via the URL
// @ts-ignore
const entryId = props.sdk.parameters?.invocation?.path.replace('/', '');
// If no entry ID exists, show an error message by setting content to false
if (!entryId) {
setEntry(false);
return;
}
// Get the entry data by getting the linked asset and content body
props.sdk.space.getEntry<OpenGraphEntry>(entryId).then((data) => {
Promise.all([
// Grabs the Image asset of the Open Graph content type
props.sdk.space.getAsset<Asset>(
data.fields.image['en-US'].sys.id
),
// Grabs the long text body of the blog post
props.sdk.space.getEntry<Content>(
data.fields.content['en-US'].sys.id
),
]).then(([asset, content]) => {
// combine the data from the two `space` calls
setEntry({
title: data.fields.title['en-US'],
imageUrl: asset.fields.file['en-US'].url,
previewBody: content.fields.body['en-US'],
url: data.fields.url['en-US'],
id: entryId,
});
});
});
}, []);
if (entry === false) {
return <Note noteType="negative">Error retrieving entry!</Note>;
}
return (
<div
style={{
display: 'flex',
width: '100%',
height: '100vh',
alignItems: 'center',
marginTop: tokens.spacingXl,
flexDirection: 'column',
}}
>
<div style={{ justifyContent: 'center' }}>
<SkeletonContainer>
<SkeletonDisplayText numberOfLines={1} />
<SkeletonBodyText numberOfLines={3} offsetTop={35} />
</SkeletonContainer>
</div>
<Typography>
<Heading>
Open Graph Preview
</Heading>
</Typography>
<Card style={{ width: '260px' }}>
{!entry ?
<SkeletonContainer>
<SkeletonDisplayText numberOfLines={1} />
<SkeletonBodyText numberOfLines={3} offsetTop={35} />
</SkeletonContainer> :
<>
<Typography>
<TextLink href={entry.url}>{entry.title}</TextLink>
<Paragraph>{entry.previewBody}</Paragraph>
</Typography>
<img src={entry.imageUrl} alt="" style={{ width: '200px' }} />
</>}
</Card>
<Button
buttonType="muted"
disabled={!entry}
onClick={() => props.sdk.navigator.openEntry(entry!.id)}
style={{ margin: `${tokens.spacingXl} 0` }}
>
Back to entry
</Button>
<div style={{ justifyContent: 'center' }}>
<SkeletonContainer>
<SkeletonDisplayText numberOfLines={1} />
<SkeletonBodyText numberOfLines={10} offsetTop={35} />
</SkeletonContainer>
</div>
</div>
);
};
export default Page;
15