How to publish SEO blog posts to Contentful automatically
Contentful is a good home for a blog and a slow place to write one. This guide covers what it takes to publish SEO posts into Contentful without pasting them in by hand: the content model, how each field gets filled, what the rich text looks like, images, internal links, scheduling, and what your front end has to handle. Frogpost does all of it, from 3 free posts a month, and Pro is $10 a month.
Updated
Why publishing to Contentful by hand is slow
Contentful stores content as structured entries, not as pages. A blog post is an entry of a content type, and every part of the post lives in its own field: the title, the slug, the summary, the SEO fields, the image, and a rich text body. That structure is what makes a headless CMS worth using. It is also why copying a finished article from a document into Contentful takes so long.
Pasting into the rich text editor loses headings and turns lists into paragraphs. Every image has to be uploaded as an asset, processed, published, and then embedded. The slug and meta description are typed separately. The category has to match the exact values your content type allows. Multiply that by a post a week and the writing is no longer the bottleneck. Getting the post into the CMS is.
Automating it means doing the same work through the Contentful Management API: creating the entry, filling each field in the right format, uploading the assets, and publishing. The rest of this guide describes what that involves, in the order a post goes through it.
What you need before you start
- A Contentful space and an environment. Most blogs publish to
master. - A content type for blog posts, with at least a title, a slug and a rich text body.
- Access to that space, either by signing in to Contentful through Frogpost, or with a personal access token.
- A list of topics you want to rank for. Check that people actually search them before you queue them.
- Your website address, so the posts can describe your product accurately and link to it.
- Optional but worth it: the URL pattern your blog uses, like
https://example.com/blog/{slug}, so posts can link to each other.
One Frogpost project is one blog: one Contentful connection, one content type, one queue, and one schedule. If you run two blogs, they are two projects, and neither can publish into the other.
The content model, and how each field gets filled
You do not have to rebuild your content type. Frogpost reads the fields on the type you picked and matches each one to a part of the post by its type and its field ID. This table shows the rules, so you can check your model before the first run.
| Part of the post | Contentful field type | Matched when | What gets written |
|---|---|---|---|
| Title | Short text | ID is title or ends in title | The post headline |
| Slug | Short text | ID contains slug or urlPath | A lowercase, hyphenated URL slug |
| Excerpt | Short or long text | ID contains excerpt, summary, or description (but not meta or seo) | A 100 to 150 word summary |
| Body | Rich text | The first rich text field on the type | The full article as Contentful rich text |
| SEO title | Short text | ID contains metaTitle or seoTitle | A title under 60 characters |
| Meta description | Short or long text | ID contains metaDesc or seoDesc | A 150 to 160 character description |
| Publish date | Date and time | The first date field on the type | The moment the entry is created |
| Featured image | Media (one file) | The first single media field on the type | An uploaded, published image asset |
| Category or tags | Short text, or a list of short text | Fields you choose to control in settings | Values picked from your allowed list |
When a field is not recognised
A field that does not match any rule is left empty. That is fine for optional fields. It is a problem for a required one, because Contentful will refuse to publish an entry with a required field missing. The quickest fix is usually the field ID, not the content model. A meta description field with the ID googleSnippet will not be found. Renaming the ID to seoDescription fixes it, and the name editors see can stay whatever you like.
A content type that works well
If you are creating a blog post type from scratch, this set of field IDs is matched without any changes: title, slug, excerpt, body (rich text), seoTitle, seoDescription, publishDate (date), featuredImage (media), and category. Make the slug field unique in its validations, so two posts can never share a URL.
What the rich text body looks like
The body is not pasted HTML or a markdown string. It is a Contentful rich text document, the same structure the editor produces, so it renders through your existing rich text renderer and stays editable in the Contentful web app.
Each post is written with a clear outline: four to six main sections, each with two or three subsections, and at least 1,500 words. That outline becomes these node types:
| In the article | Contentful rich text node |
|---|---|
| Section and subsection headings | heading-2 and heading-3 |
| Paragraphs | paragraph |
| Bold and italic text | bold and italic marks |
| Links to your site and to earlier posts | hyperlink |
| Bulleted and numbered lists | unordered-list and ordered-list |
| Images inside the article | embedded-asset-block |
Tables, quotes and code blocks are not produced. If your renderer styles those, nothing breaks, they just will not appear in generated posts. A short excerpt of a real body looks like this:
{
"nodeType": "document",
"data": {},
"content": [
{
"nodeType": "heading-2",
"data": {},
"content": [{ "nodeType": "text", "value": "Choosing a content model", "marks": [], "data": {} }]
},
{
"nodeType": "paragraph",
"data": {},
"content": [
{ "nodeType": "text", "value": "Start with the fields your ", "marks": [], "data": {} },
{
"nodeType": "hyperlink",
"data": { "uri": "https://example.com/blog/headless-cms-basics" },
"content": [{ "nodeType": "text", "value": "headless CMS", "marks": [], "data": {} }]
},
{ "nodeType": "text", "value": " already has.", "marks": [], "data": {} }
]
},
{
"nodeType": "embedded-asset-block",
"data": { "target": { "sys": { "type": "Link", "linkType": "Asset", "id": "4Xa9..." } } },
"content": []
}
]
}Images and assets
Every post gets a featured image if your content type has a media field for it. Frogpost searches for a relevant photo, uploads it to your space as an asset, waits for Contentful to process it, publishes the asset, and links it to the entry. The asset title is the post title, so your media library stays searchable.
Inside the article, two to four images are added only where a picture helps the reader, such as when a named product or screen is being described. Each goes through the same upload and publish steps, and sits in the body as an embedded asset. If an image cannot be found or processed in time, the post is published without it. A missing picture never blocks a post.
Your front end has to render embedded assets. With the official React renderer, that is one entry in the options object:
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { BLOCKS } from '@contentful/rich-text-types';
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: (node) => {
const { file, title } = node.data.target.fields;
const { width, height } = file.details.image;
return <img src={`https:${file.url}?w=1200&fm=webp`} alt={title} width={width} height={height} loading="lazy" />;
},
},
};
export const PostBody = ({ body }) => documentToReactComponents(body, options);The asset has to be resolved for node.data.target.fields to exist. The Contentful JavaScript client does that for you when the entry is fetched with its linked assets included, which is the default. If you use GraphQL, request body { json links { assets { block { sys { id } url title width height } } } } and look each asset up by ID.
Categories and tags that match your model
Free-text categories are how a blog ends up with Guides, Guide and How-to guides as three separate sections. To avoid that, you choose which fields Frogpost controls, such as a category, a section, or a list of tags, and give each one its allowed values. If the Contentful field already has a list of accepted values in its validations, those are picked up.
Each post must choose from that list, word for word. A single field gets one value. A list field gets one or more. If a required list comes back empty, the first allowed value is used so Contentful does not reject the entry. Fields you delete or rename in Contentful later are skipped instead of causing an error.
Internal links between posts
Internal links are the part of blog SEO that teams most often skip, because it means remembering what you published six months ago. Frogpost keeps a record of every post it has written for a project. When it writes a new one, it looks for the earlier posts closest in meaning to the new topic and gives the writer up to six of them.
A few of those, only the ones that genuinely fit, become inline links on a natural phrase in the text. There is no "read our article on" line, and no invented URLs. Every post also links to your own website two or three times with descriptive anchor text.
For this to work, set the project's URL pattern, for example https://example.com/blog/{slug}. Contentful does not know where your front end serves each post, so without the pattern Frogpost cannot build a working link and leaves internal links out.
Draft, publish, or schedule
| Mode | What happens in Contentful | Good for |
|---|---|---|
| Draft | The entry is created and left unpublished. | Your first posts, while you check the tone and the fields. |
| Publish now | The entry is created and published at once. | A post you have already read and edited. |
| Schedule | At each hour you set, in UTC, the oldest topics in the queue are written and published live. | A steady cadence, once you trust the output. |
You can edit a post in Frogpost after it is created, and the changes are written back to the same entry. You can also edit it in Contentful like any other entry. The publish date field is set when the entry is created, so it matches the order posts went out.
Scheduling is per project. Pick how many posts each run writes and which hours it runs at. If a topic fails, for example because a required field could not be filled, it is marked failed with the reason and the rest of the queue carries on.
What your front end has to handle
- A route for each post, built from the slug. This is the same pattern you give Frogpost for internal links.
- Rendering for
heading-2,heading-3, lists, hyperlinks and embedded assets in the rich text body. - The SEO title and meta description in the page head. In Next.js that is
generateMetadatareading the two fields. - A blog index and a sitemap that pick up new entries. A static site needs a webhook on Entry publish that revalidates or rebuilds the blog.
Contentful sends webhooks for publishes made through the API exactly as it does for publishes in the web app, so a scheduled post triggers your site the same way an editor's post does.
Frogpost and Contentful AI Actions
Contentful has its own AI features, called AI Actions. They are useful, and they solve a different problem.
| Contentful AI Actions | Frogpost | |
|---|---|---|
| Starts from | An entry that already exists | A topic in a queue |
| Typical jobs | Meta descriptions, keyword suggestions, alt text, translation, rewriting a field | Writing the whole post and creating the entry |
| Images | Alt text for images you added | Finds, uploads and embeds the images |
| Scheduling | Run by an editor inside an entry | Publishes a queue at hours you set |
| Plan | Contentful Premium and Enterprise | Free for 3 posts a month, Pro is $10 a month, on any Contentful plan |
They work together. A team can let Frogpost write and file the posts, and use AI Actions in Contentful to translate them.
A checklist for the first run
- Check the field IDs on your blog content type against the table above, and rename any required field that will not be matched.
- Connect the space in Frogpost and choose that content type.
- Add your website so Frogpost reads your product pages before it writes.
- Set the URL pattern for your posts so internal links work.
- Choose the category and tag fields you want controlled, and their allowed values.
- Queue three topics you know people search for, and save the first post as a draft.
- Open the draft in Contentful and on your site. Check the headings, the images, the SEO fields and the category.
- When the drafts look right, turn on a schedule.
Questions
- How do I automatically publish SEO posts to Contentful?
- Connect a Contentful space, pick the content type your blog posts use, and add topics to a queue. Frogpost writes each post, fills the title, slug, excerpt, rich text body, SEO title, meta description, publish date and featured image fields it finds on that content type, and creates the entry. A single post can be saved as a draft. A scheduled queue publishes entries live at the hours you choose, in UTC.
- How is this different from Contentful AI Actions?
- AI Actions works on an entry that already exists: it can draft a meta description, suggest keywords, write alt text, or translate a field. It is part of the Premium and Enterprise plans. Frogpost starts from a topic, writes the whole post, uploads its images as assets, and creates the entry in your content type.
- Do I have to change my content model?
- No. Frogpost reads the content type you already have and writes into the fields it can identify. Fields it cannot identify are left empty, so a required field it cannot fill will stop the entry from publishing. Renaming a field ID to something recognisable, like seoTitle, is usually enough.
- Which Contentful fields does Frogpost fill?
- Title, slug, excerpt, the rich text body, an SEO title, a meta description, a publish date, a featured image, and any category or tag fields you choose to control. The body is written as real Contentful rich text with headings, lists, links and embedded image assets.
- Will the posts link to each other?
- Yes, if you give the project a URL pattern such as https://example.com/blog/{slug}. Frogpost looks up earlier posts on the same blog that are closest in meaning to the new topic and links a few of them inline. Without a pattern it skips internal links, because a headless CMS does not know your site routes and the links would 404.
- Does publishing trigger my site to rebuild?
- Publishing through Frogpost is a normal Contentful publish, so any webhook you already have on Entry publish fires as usual. If your blog is statically generated, point that webhook at your revalidation or rebuild hook and new posts appear without a manual deploy.
- What does it cost?
- The free plan includes 3 posts a month, with no card required. Pro is $10 a month for unlimited posts and scheduled publishing, shared across every project on the account.

