How do I connect my Framer blog?
Open Menu -> Connect my Blog -> Framer
Copy the provided API key and click the Connect button. If you need to retrieve your API key later, you can find it in Menu -> Blog Sync Status.
Navigate to your Framer dashboard and install the SEObot plugin. You can also find the SEObot plugin in the Framer marketplace.
Framer will prompt you to create a collection. Please give your collection a name.
You will then be asked to enter the SEObot API key. Input the key provided by SEObot in step 2 above. Click the Save & Sync button.
Create Index and Details pages using the SEObot collection.
Design the Details and Index pages.
On the Details page, select the Content field layer and make sure that all required styles are applied: Paragraph, Headings, Link, Blockquote, Code, Spacing, Code Block, YouTube, Image.
To support table rendering, you need to create a code override. Navigate to Assets and create a new code file in the Code section.
Copy and paste the code below, save the file. You can adjust the code, colors for dark and light themes, spacing, etc.
Navigate to the Details page and select the Content field layer again. Then, choose the created Code Override in the Code Overrides section.
Complete your pages design, setup page routes and publish changes.
If you already have a blog collection that operates under the /blog/ route, you should choose a different option for SEObot blogs.
This route limitation is imposed by Framer, which does not allow plugins to manage third-party CMS collections—only its own collections. Additionally, you cannot use two CMS collections on the same route. However, you can create a separate route, such as /insights/ or /resources/. This is standard practice and helps organize different types of content more effectively.
Common examples:
/blog/ (main blog)
/insights/ (thought leadership)
/news/ (company updates)
/resources/ (guides, tutorials)
/case-studies/
etc.
Copy the provided API key and click the Connect button. If you need to retrieve your API key later, you can find it in Menu -> Blog Sync Status.
Navigate to your Framer dashboard and install the SEObot plugin. You can also find the SEObot plugin in the Framer marketplace.
Framer will prompt you to create a collection. Please give your collection a name.
You will then be asked to enter the SEObot API key. Input the key provided by SEObot in step 2 above. Click the Save & Sync button.
Create Index and Details pages using the SEObot collection.
Design the Details and Index pages.
On the Details page, select the Content field layer and make sure that all required styles are applied: Paragraph, Headings, Link, Blockquote, Code, Spacing, Code Block, YouTube, Image.
To support table rendering, you need to create a code override. Navigate to Assets and create a new code file in the Code section.
Copy and paste the code below, save the file. You can adjust the code, colors for dark and light themes, spacing, etc.
"use client"
import * as React from "react"
import type { ComponentType } from "react"
import { createStore } from "https://framer.com/m/framer/store.js@^1.0.0"
export const tableBorderRadius = 8
export const cellPadding = 8
export const lightBgColor = "#fff"
export const lightTextColor = "#000"
export const lightBorderColor = "#ccc"
export const lightHeaderColor = "#f0f0f0"
export const lightEvenRowBg = "#fafafa"
export const darkBgColor = "#222"
export const darkTextColor = "#fff"
export const darkBorderColor = "#555"
export const darkHeaderColor = "#333"
export const darkEvenRowBg = "#2a2a2a"
function createTableOverride({ colorScheme }) {
const useStore = createStore({})
return (Component) =>
function TableOverride(props) {
const [store] = useStore()
const extractAndFormatTable = React.useCallback(() => {
if (!window || !document) return
const contentDivs = document.querySelectorAll(
'[data-framer-name="Content"][data-framer-component-type="RichTextContainer"]'
)
contentDivs.forEach((contentDiv) => {
const codeBlocks = contentDiv.querySelectorAll(
".framer-text.framer-text-module"
)
codeBlocks.forEach((codeBlock) => {
if (!(codeBlock instanceof HTMLElement)) return
// Skip code blocks that have already been processed
if (codeBlock.dataset.processed) return
// Mark the code block as processed
codeBlock.dataset.processed = "true"
const finalScheme =
colorScheme === "auto" ? "light" : colorScheme
const outerContainer = document.createElement("div")
outerContainer.style.cssText = `
border: 1px solid ${
finalScheme === "dark"
? darkBorderColor
: lightBorderColor
};
border-radius: ${tableBorderRadius}px;
overflow: hidden;
width: 100%;
height: 100%;
`
const innerContainer = document.createElement("div")
innerContainer.style.cssText = `
overflow: auto;
width: 100%;
height: 100%;
`
const codeContent = codeBlock.textContent || ""
const lines = codeContent
.split(/\r?\n/)
.filter((l) => l.trim() !== "")
if (!lines.length) return
const colCount = lines[0]
.split("|")
.filter((c) => c.trim()).length
const table = document.createElement("table")
table.style.cssText = `
border-collapse: collapse;
overflow: hidden;
width: 100%;
height: 100%;
`
const tableHtml = lines
.map((row, i) => {
if (i === 1) return ""
const cells = row
.split("|")
.map((c) => c.trim())
.filter(Boolean)
const isHeader = i === 0
const rowBg =
i > 1 && i % 2 === 0
? finalScheme === "dark"
? darkEvenRowBg
: lightEvenRowBg
: finalScheme === "dark"
? darkBgColor
: lightBgColor
const bgColor = isHeader
? finalScheme === "dark"
? darkHeaderColor
: lightHeaderColor
: rowBg
const tag = isHeader ? "th" : "td"
return `<tr style="border-color:${
finalScheme === "dark"
? darkBorderColor
: lightBorderColor
};">${cells
.map(
(cell) => `
<${tag} style="
width:${100 / colCount}%;
background:${bgColor};
color:${finalScheme === "dark" ? darkTextColor : lightTextColor};
padding:${cellPadding}px;
border-color:${
finalScheme === "dark"
? darkBorderColor
: lightBorderColor
};
">${cell}</${tag}>
`
)
.join("")}</tr>`
})
.filter(Boolean)
.join("")
table.innerHTML = tableHtml
innerContainer.appendChild(table)
outerContainer.appendChild(innerContainer)
codeBlock.innerHTML = ""
codeBlock.appendChild(outerContainer)
})
})
}, [store, colorScheme])
React.useEffect(() => {
if (!window || !document) return
extractAndFormatTable()
const observer = new MutationObserver(extractAndFormatTable)
const contentDivs = document.querySelectorAll(
'[data-framer-name="Content"][data-framer-component-type="RichTextContainer"]'
)
contentDivs.forEach((c) =>
observer.observe(c, { childList: true, subtree: true })
)
const style = document.createElement("style")
style.textContent = `
[data-framer-name="Content"][data-framer-component-type="RichTextContainer"] {
white-space: normal !important;
opacity: 1 !important;
}
`
document.head.appendChild(style)
return () => {
observer.disconnect()
document.head.removeChild(style)
}
}, [extractAndFormatTable])
return <Component {...props} />
}
}
export function ThemeLight(Component): ComponentType {
return createTableOverride({ colorScheme: "light" })(Component)
}
export function ThemeDark(Component): ComponentType {
return createTableOverride({ colorScheme: "dark" })(Component)
}
export function ThemeAuto(Component): ComponentType {
return createTableOverride({ colorScheme: "auto" })(Component)
}
Navigate to the Details page and select the Content field layer again. Then, choose the created Code Override in the Code Overrides section.
Complete your pages design, setup page routes and publish changes.
Can I use the /blog/ route?
If you already have a blog collection that operates under the /blog/ route, you should choose a different option for SEObot blogs.
This route limitation is imposed by Framer, which does not allow plugins to manage third-party CMS collections—only its own collections. Additionally, you cannot use two CMS collections on the same route. However, you can create a separate route, such as /insights/ or /resources/. This is standard practice and helps organize different types of content more effectively.
Common examples:
/blog/ (main blog)
/insights/ (thought leadership)
/news/ (company updates)
/resources/ (guides, tutorials)
/case-studies/
etc.
Updated on: 16/01/2025
Thank you!