diff --git a/apps/docs/contribute/guides/blog.mdx b/apps/docs/contribute/guides/blog.mdx new file mode 100644 index 000000000..f6f8094f8 --- /dev/null +++ b/apps/docs/contribute/guides/blog.mdx @@ -0,0 +1,38 @@ +--- +title: 'Contribute to the blog' +sidebarTitle: 'Blog' +icon: 'newspaper' +--- + +The [official Typebot blog](https://typebot.io/blog) is a place where we share any ideas, original content related to the chatbot industry, and Typebot itself. + +You are free to contribute to the blog or fix any typos you may find. + +1. Head over to the [content folder](https://github.com/baptisteArno/typebot.io/tree/main/apps/landing-page/content) on the Github repo. +2. Click on the blog post file you want to edit. Or create a new file by clicking on the `Add file` button. +3. If you did not already have a fork of the repository, you will be prompted to create one. +4. Once you're happy with your changes, hit `Commit changes...`. +5. Click on `Create pull request`. +6. Add a title and a description to describe your changes. +7. Click on `Create pull request`. + +It will be reviewed and merged if approved! + +## New article guidelines + +- The article should be related to chatbots, or Typebot. +- The article should be written in English. +- The article should be original content. No plagiarism. +- The article should not be 100% AI-generated. + +The mdx file should always start with the following frontmatter: + +```md +--- +title: 'My awesome blog post' +publishedAt: '2023-11-19' +summary: 'A short summary of the blog post.' +--- +``` + +By default the og image is generated from the title of the blog post. If you want to use a custom og image, you can specify a `image` field in the frontmatter. diff --git a/apps/docs/contribute/overview.mdx b/apps/docs/contribute/overview.mdx index 3086fb97b..dab5b1344 100644 --- a/apps/docs/contribute/overview.mdx +++ b/apps/docs/contribute/overview.mdx @@ -40,8 +40,19 @@ Any contributions you make are **greatly appreciated**. There are many ways to c href="./guides/documentation" color="#97A0B1" > - Help us improve the documentation by fixing typos, adding missing information - or proposing new sections. + Improve the documentation by fixing typos, adding missing information or + proposing new sections. + + + + Write original content for Typebot's blog. Share your knowledge and ideas to a + wider audience. The author will be credited. ( + + Latest blog posts: + + {allBlogs + .filter((post) => post.metadata.publishedAt) + .sort((a, b) => { + if ( + new Date(a.metadata.publishedAt) > new Date(b.metadata.publishedAt) + ) { + return -1 + } + return 1 + }) + .map((post) => ( + + + + {post.metadata.title} + + + {new Date(post.metadata.publishedAt).toDateString()} + + + + ))} + + +) diff --git a/apps/landing-page/app/blog/[slug]/Post.tsx b/apps/landing-page/app/blog/[slug]/Post.tsx new file mode 100644 index 000000000..70d2761f3 --- /dev/null +++ b/apps/landing-page/app/blog/[slug]/Post.tsx @@ -0,0 +1,80 @@ +'use client' + +import { Link } from '@chakra-ui/next-js' +import { Heading, Stack, Text } from '@chakra-ui/react' +import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote' +import { highlight } from 'sugar-high' + +type Props = { + metadata: { + title: string + publishedAt: string + } + mdxSource: MDXRemoteSerializeResult +} + +export const Post = ({ metadata, mdxSource }: Props) => ( + + + {metadata.title} + {formatDate(metadata.publishedAt)} + + + , + h2: (props) => , + h3: (props) => , + h4: (props) => , + h5: (props) => , + h6: (props) => , + code: ({ children, ...props }) => { + const codeHTML = highlight(children?.toString() ?? '') + return ( + + ) + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + link: (props: any) => , + }} + /> + + +) + +function formatDate(date: string) { + const currentDate = new Date().getTime() + if (!date.includes('T')) { + date = `${date}T00:00:00` + } + const targetDate = new Date(date).getTime() + const timeDifference = Math.abs(currentDate - targetDate) + const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + + const fullDate = new Date(date).toLocaleString('en-us', { + month: 'long', + day: 'numeric', + year: 'numeric', + }) + + if (daysAgo < 1) { + return 'Today' + } else if (daysAgo < 7) { + return `${fullDate} (${daysAgo}d ago)` + } else if (daysAgo < 30) { + const weeksAgo = Math.floor(daysAgo / 7) + return `${fullDate} (${weeksAgo}w ago)` + } else if (daysAgo < 365) { + const monthsAgo = Math.floor(daysAgo / 30) + return `${fullDate} (${monthsAgo}mo ago)` + } else { + const yearsAgo = Math.floor(daysAgo / 365) + return `${fullDate} (${yearsAgo}y ago)` + } +} diff --git a/apps/landing-page/app/blog/[slug]/page.tsx b/apps/landing-page/app/blog/[slug]/page.tsx new file mode 100644 index 000000000..7572ed3f8 --- /dev/null +++ b/apps/landing-page/app/blog/[slug]/page.tsx @@ -0,0 +1,67 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { getBlogPosts } from '@/app/db/blog' +import { Post } from './Post' +import { serialize } from 'next-mdx-remote/serialize' +import '@/assets/prose.css' +import { env } from '@typebot.io/env' + +export async function generateMetadata({ + params, +}: { + params: { slug: string } +}): Promise { + const post = getBlogPosts().find( + (post) => post.slug === params.slug && post.metadata.publishedAt + ) + if (!post) { + return + } + + const { + title, + publishedAt: publishedTime, + summary: description, + image, + } = post.metadata + const ogImage = image + ? `${env.LANDING_PAGE_URL}${image}` + : `${env.LANDING_PAGE_URL}/og?title=${title}` + + return { + title, + description, + openGraph: { + title, + description, + type: 'article', + publishedTime, + url: `${env.LANDING_PAGE_URL}/blog/${post.slug}`, + images: [ + { + url: ogImage, + }, + ], + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: [ogImage], + }, + } +} + +export default async function Blog({ params }: { params: { slug: string } }) { + const post = getBlogPosts().find( + (post) => post.slug === params.slug && post.metadata.publishedAt + ) + + if (!post) { + notFound() + } + + const mdxSource = await serialize(post.content) + + return +} diff --git a/apps/landing-page/app/blog/page.tsx b/apps/landing-page/app/blog/page.tsx new file mode 100644 index 000000000..fb4c6c248 --- /dev/null +++ b/apps/landing-page/app/blog/page.tsx @@ -0,0 +1,13 @@ +import { getBlogPosts } from '@/app/db/blog' +import { Posts } from './Posts' + +export const metadata = { + title: 'Blog', + description: 'Read my thoughts on software development, design, and more.', +} + +export default function Home() { + const allBlogs = getBlogPosts() + + return +} diff --git a/apps/landing-page/app/db/blog.ts b/apps/landing-page/app/db/blog.ts new file mode 100644 index 000000000..f93ae3265 --- /dev/null +++ b/apps/landing-page/app/db/blog.ts @@ -0,0 +1,60 @@ +import fs from 'fs' +import path from 'path' + +type Metadata = { + title: string + publishedAt: string + summary: string + image?: string +} + +function parseFrontmatter(fileContent: string) { + const frontmatterRegex = /---\s*([\s\S]*?)\s*---/ + const match = frontmatterRegex.exec(fileContent) + const frontMatterBlock = match![1] + const content = fileContent.replace(frontmatterRegex, '').trim() + const frontMatterLines = frontMatterBlock.trim().split('\n') + const metadata: Partial = {} + + frontMatterLines.forEach((line) => { + const [key, ...valueArr] = line.split(': ') + let value = valueArr.join(': ').trim() + value = value.replace(/^['"](.*)['"]$/, '$1') // Remove quotes + metadata[key.trim() as keyof Metadata] = value + }) + + return { metadata: metadata as Metadata, content } +} + +function getMDXFiles(dir: fs.PathLike) { + return fs.readdirSync(dir).filter((file) => path.extname(file) === '.mdx') +} + +function readMDXFile(filePath: fs.PathOrFileDescriptor) { + const rawContent = fs.readFileSync(filePath, 'utf-8') + return parseFrontmatter(rawContent) +} + +function extractTweetIds(content: string) { + const tweetMatches = content.match(//g) + return tweetMatches?.map((tweet) => tweet.match(/[0-9]+/g)?.[0]) || [] +} + +function getMDXData(dir: string) { + const mdxFiles = getMDXFiles(dir) + return mdxFiles.map((file) => { + const { metadata, content } = readMDXFile(path.join(dir, file)) + const slug = path.basename(file, path.extname(file)) + const tweetIds = extractTweetIds(content) + return { + metadata, + slug, + tweetIds, + content, + } + }) +} + +export function getBlogPosts() { + return getMDXData(path.join(process.cwd(), 'content')) +} diff --git a/apps/landing-page/app/layout.tsx b/apps/landing-page/app/layout.tsx new file mode 100644 index 000000000..0114ce214 --- /dev/null +++ b/apps/landing-page/app/layout.tsx @@ -0,0 +1,42 @@ +/* eslint-disable @next/next/no-sync-scripts */ +/* eslint-disable @next/next/no-page-custom-font */ +import type { Metadata } from 'next' +import { Header } from 'components/common/Header/Header' +import { Footer } from 'components/common/Footer' +import { Providers } from './providers' +import { EndCta } from '@/components/Homepage/EndCta' +import 'assets/style.css' + +export const metadata: Metadata = { + title: 'Typebot - Open-source conversational apps builder', + description: + 'Powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + + +