Skip to content

Commit

Permalink
📝 Add new blog structure
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Apr 19, 2024
1 parent af01439 commit 6fe4e28
Show file tree
Hide file tree
Showing 25 changed files with 862 additions and 184 deletions.
38 changes: 38 additions & 0 deletions apps/docs/contribute/guides/blog.mdx
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 13 additions & 2 deletions apps/docs/contribute/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Card>

<Card
title="Write official blog posts"
icon="newspaper"
iconType="duotone"
href="./guides/blog"
color="#97A0B1"
>
Write original content for Typebot's blog. Share your knowledge and ideas to a
wider audience. The author will be credited.
</Card>

<Card
Expand Down
1 change: 1 addition & 0 deletions apps/docs/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@
"contribute/guides/local-installation",
"contribute/guides/create-block",
"contribute/guides/documentation",
"contribute/guides/blog",
"contribute/guides/translation"
]
},
Expand Down
56 changes: 56 additions & 0 deletions apps/landing-page/app/blog/Posts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client'
import { Heading, Stack, Text } from '@chakra-ui/react'
import { Link } from '@chakra-ui/next-js'

type Props = {
allBlogs: {
metadata: {
title: string
publishedAt: string
}
slug: string
}[]
}

export const Posts = ({ allBlogs }: Props) => (
<Stack
spacing={10}
mx="auto"
maxW="3xl"
my="20"
fontSize="17px"
textAlign="justify"
>
<Heading>Latest blog posts:</Heading>
<Stack>
{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) => (
<Link key={post.slug} href={`/blog/${post.slug}`}>
<Stack
w="full"
rounded="md"
borderColor="gray.600"
borderWidth={1}
p="4"
>
<Heading as="h2" fontSize="2xl">
{post.metadata.title}
</Heading>
<Text color="gray.500">
{new Date(post.metadata.publishedAt).toDateString()}
</Text>
</Stack>
</Link>
))}
</Stack>
</Stack>
)
80 changes: 80 additions & 0 deletions apps/landing-page/app/blog/[slug]/Post.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<Stack spacing={10} my="20">
<Stack mx="auto" w="full" maxW="65ch">
<Heading>{metadata.title}</Heading>
<Text>{formatDate(metadata.publishedAt)}</Text>
</Stack>
<Stack
mx="auto"
spacing={0}
as="article"
className="prose prose-quoteless prose-neutral prose-invert"
>
<MDXRemote
{...mdxSource}
components={{
h1: (props) => <Heading as="h1" {...props} />,
h2: (props) => <Heading as="h2" {...props} />,
h3: (props) => <Heading as="h3" {...props} />,
h4: (props) => <Heading as="h4" {...props} />,
h5: (props) => <Heading as="h5" {...props} />,
h6: (props) => <Heading as="h6" {...props} />,
code: ({ children, ...props }) => {
const codeHTML = highlight(children?.toString() ?? '')
return (
<code dangerouslySetInnerHTML={{ __html: codeHTML }} {...props} />
)
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
link: (props: any) => <Link {...props} />,
}}
/>
</Stack>
</Stack>
)

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)`
}
}
67 changes: 67 additions & 0 deletions apps/landing-page/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata | undefined> {
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 <Post metadata={post.metadata} mdxSource={mdxSource} />
}
13 changes: 13 additions & 0 deletions apps/landing-page/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <Posts allBlogs={allBlogs} />
}
60 changes: 60 additions & 0 deletions apps/landing-page/app/db/blog.ts
Original file line number Diff line number Diff line change
@@ -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<Metadata> = {}

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(/<StaticTweet\sid="[0-9]+"\s\/>/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'))
}
42 changes: 42 additions & 0 deletions apps/landing-page/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en">
<head>
<link rel="icon" type="image/png" href="/favicon.png" />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&family=Indie+Flower:wght@400&display=swap"
rel="stylesheet"
/>
<script src="/__ENV.js" />
</head>

<body style={{ backgroundColor: '#171923' }}>
<Providers>
<Header />
{children}
<EndCta />
<Footer />
</Providers>
</body>
</html>
)
}
Loading

0 comments on commit 6fe4e28

Please sign in to comment.