Create an AI Content Generation SaaS Like Copy.ai
Copy.ai is an AI-powered tool that helps users generate marketing copy adjusted to specific brand voice and tone. Under the hood, Copy.ai uses AI to generate the content based on the analysis of brand voice, tone and user input.
In this guide, you will learn how to recreate the content generation functionality of Copy.ai using Marblism - a platform that takes an input and turns it into a fully functioning Next.js application.
Prerequisites
To get started, you will need:
Getting Started
Navigate to your Marblism account. If you don't have any projects, you will be prompted to create one. Otherwise, press New Project on the left sidebar.
Creating a New Project
You will have to populate the following fields:
- Project Name
- Project Type
- Project Description
- Modules
- Theme
Enter the project name as "Copy.ai", and select the project type as SaaS. The project description should concisely and directly describe project's functionality, relations and such.
An example of a good project description we're using for this Copy.ai clone:
Copy.ai is a SaaS product where users can:
- Create organizations
- Create products in organizations with a title and description
- For each product users can generate a Twitter post, a LinkedIn post, and a blog article using OpenAI's ChatGPT
An example of a project description that wouldn't get us the desired result would look something like this:
❌ Copy.ai is a SaaS product where users can generate posts based on the products they have in their organizations.
As shown in the example above, the description (the actual prompt) should be broken down, so that Marblism can understand and interconnect the puzzle pieces together, creating the desired outcome.
Leave the selected Landing Page module, then choose a Dark or Light theme.
Now, press Next and move to the Description step.
Page Description
Marblism will auto-generate pages, their paths and user stories.
Each project will generate a different set of pages and user stories.
User stories, as in general software development process, are generalized explanations of a feature, written from the perspective of an end user.
Marblism will convert those user stories into direct features in the built application.
As you'll see below, you can specify scenarios, that specify how actions should behave when an user initiates them.
Examples of Page Descriptions
You can modify the auto-generated output with the examples we're using.
- Home
- Organization
- Create Organization
- Product
- Authentication & Profile
Home Page
/home
- As a user, I can view a list of my organizations so that I can select one to manage.
- Scenario: Viewing the list of organizations. Given I am logged in, when I navigate to the organizations page, then I can see a list of my organizations.
Organization Page
/organizations/:organizationId
- As a user, I can view the details of an organization so that I can see its products and related information.
- As a user, I can create a product within an organization so that I can manage its details and generate content.
- As a user, I can view a list of products within an organization so that I can manage them.
- Scenario: Viewing the list of products in an organization. Given I am viewing an organization, when I navigate to the products section, then I can see a list of products within that organization.
- Scenario: Creating a product. Given I am viewing an organization, when I click on the "Create Product" button, then I can fill out the product details (title and description) and save it.
Create Organization
/organizations/create
- As a user, I can create an organization so that I can manage my products within it.
- Scenario: Creating an organization. Given I am on the organizations page, when I click on the "Create Organization" button, then I can fill out the organization details and save it.
Product
/organizations/:organizationId/products/:productId
- As a user, I can view the details of a product so that I can see its title and description.
- As a user, I can generate a Twitter post for a product using OpenAI's ChatGPT so that I can promote it on Twitter.
- As a user, I can generate a LinkedIn post for a product using OpenAI's ChatGPT so that I can promote it on LinkedIn.
- As a user, I can generate a blog article for a product using OpenAI's ChatGPT so that I can promote it through a blog.
- Scenario: Generating a Twitter post. Given I am viewing a product, when I click on the "Generate Twitter Post" button, then OpenAI's ChatGPT generates a Twitter post for the product.
- Scenario: Generating a LinkedIn post. Given I am viewing a product, when I click on the "Generate LinkedIn Post" button, then OpenAI's ChatGPT generates a LinkedIn post for the product.
- Scenario: Generating a blog article. Given I am viewing a product, when I click on the "Generate Blog Article" button, then OpenAI's ChatGPT generates a blog article for the product.
These pages are included by default and cannot be modified.
After you're satisfied with the descriptions and user stories, press Next. Marblism will now generate a Data model.
Data Model
In this Marblism will generate a Data model, that corresponds to a Prisma database schema.
It will contain all the necessary fields in the database, as well as the relations betwen the tables.
Each generation will likely generate a different data model, so changes might be necessary.
An example we're using for the tables:
- User Table
- Organization Table
- Product Table
- PostData Table
You can press Change something and instruct Marblism with a prompt the changes you want. For example, you might want to have different tables, or different fields in the existing tables.
Press Next and Marblism will start generating the codebase.
Customizing the Project
After generating the project, you will be presented with the Overview section.
Optionally, you can change the Zone on the right-hand side to Europe, Asia or America
From here, press Launch Workspace and then Go to Workspace. You will be redirected to the Workspace platform. Wait for the repository to start.
Changing or Adding Features
First, let's login to the app by pressing Get Started in the right-hand corner of the Preview screen.
Your app's user interface (UI) might look slightly different, but the essence should be the same.
Marblism will auto-populate test environment credentials.
See What's Missing
By testing the app, in this video, we can see a few things we're missing:
- A nice loader that tells the user: Hey, the app didn't crash, it just takes a few seconds to generate the post!
- A nicely formatted markdown instead of plain text.
Adding a Loader
Let's fix this by first creating a loader. On the left-hande side of the Workspace is the Michelangelo chat.
With it you can code, update theme, install libraries, ask questions, and even fix code issues.
Now, while we're on the product page (in this case /src/app/(authenticated)/organizations/[organizationId]/products/[productId]/page.tsx
, but this will probably differ for you), select the Code option from the Michelangelo chat.
In Describe the changes field, enter the following prompt:
While the post is generating, show a nice loader on top of everything, with blurred background.
Press Next and Michelangelo will first tell you what it's planning to do, before commiting to generating any new code.
After accepting proposed changes, you can now see that it added a loader with the blurred background.
This is the code snippet of what Michelangelo created behind the scenes:
import React from 'react'
import { Spin } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import styled from 'styled-components'
const LoaderContainer = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
`
const BlurBackground = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(5px);
`
export const LoaderWithBlur: React.FC = () => {
return (
<LoaderContainer>
<BlurBackground />
<Spin indicator={<LoadingOutlined spin />} size="large" />
</LoaderContainer>
)
}
Processing Markdown
Now, let's format that generated markdown into something nice looking.
Choose Install a library from the Michelangelo dropdown. Enter the following library name:
react-markdown
Michelangelo will prompt you this:
pnpm add react-markdown
pnpm install
Press Yes to continue with the installation and Save the changes.
Now, with the Code option from Michelangelo, send it the following prompt:
Using the react-markdown library, process the generated text from the post and format it using the previously mentioned library, and show that in a modal.
Check the proposed solution and press Continue.
Let's take a look on the actual code changes.
-
Michelangelo created a new file:
/src/designSystem/ui/MarkdownModal/index.tsximport React from 'react'
import { Modal } from 'antd'
import ReactMarkdown from 'react-markdown'
type MarkdownModalProps = {
visible: boolean
content: string
onClose: () => void
}
export const MarkdownModal: React.FC<MarkdownModalProps> = ({
visible,
content,
onClose,
}) => {
return (
<Modal visible={visible} onCancel={onClose} footer={null}>
<ReactMarkdown>{content}</ReactMarkdown>
</Modal>
)
} -
Michelangelo edited a rendering component:
/src/app/(authenticated)/organizations/[organizationId]/products/[productId]/page.tsx'use client'
import { Prisma } from '@prisma/client'
import { Button, Col, Row, Spin, Typography } from 'antd'
import {
TwitterOutlined,
LinkedinOutlined,
FileTextOutlined,
} from '@ant-design/icons'
import { useEffect, useState } from 'react'
const { Title, Paragraph } = Typography
import { useUserContext } from '@/core/context'
import { useRouter, useParams } from 'next/navigation'
import { useUploadPublic } from '@/core/hooks/upload'
import { useSnackbar } from 'notistack'
import dayjs from 'dayjs'
import { Api } from '@/core/trpc'
import { PageLayout } from '@/designSystem/layouts/Page.layout'
import { MarkdownModal } from '@/designSystem/ui/MarkdownModal'
export default function ProductPage() {
const router = useRouter()
const params = useParams<any>()
const { user } = useUserContext()
const { enqueueSnackbar } = useSnackbar()
const productId = params.productId
const {
data: product,
isLoading,
refetch,
} = Api.product.findUnique.useQuery({
where: { id: productId },
include: { organization: true },
})
const generateText = Api.ai.generateText.useMutation()
const [twitterPost, setTwitterPost] = useState<string | null>(null)
const [linkedinPost, setLinkedinPost] = useState<string | null>(null)
const [blogArticle, setBlogArticle] = useState<string | null>(null)
const [isModalVisible, setIsModalVisible] = useState(false)
const [modalContent, setModalContent] = useState<string>('')
const handleGeneratePost = async (type: 'Twitter' | 'LinkedIn' | 'Blog') => {
if (!product) return
let prompt = ''
switch (type) {
case 'Twitter':
prompt = `Generate a Twitter post for the product titled "${product.title}" with the description "${product.description}"`
break
case 'LinkedIn':
prompt = `Generate a LinkedIn post for the product titled "${product.title}" with the description "${product.description}"`
break
case 'Blog':
prompt = `Generate a blog article for the product titled "${product.title}" with the description "${product.description}"`
break
}
try {
const response = await generateText.mutateAsync({ prompt })
setModalContent(response.answer)
setIsModalVisible(true)
switch (type) {
case 'Twitter':
setTwitterPost(response.answer)
enqueueSnackbar('Twitter post generated successfully!', {
variant: 'success',
})
break
case 'LinkedIn':
setLinkedinPost(response.answer)
enqueueSnackbar('LinkedIn post generated successfully!', {
variant: 'success',
})
break
case 'Blog':
setBlogArticle(response.answer)
enqueueSnackbar('Blog article generated successfully!', {
variant: 'success',
})
break
}
} catch (error) {
enqueueSnackbar('Failed to generate post. Please try again.', {
variant: 'error',
})
}
}
if (isLoading) {
return (
<PageLayout layout="full-width">
<Spin size="large" />
</PageLayout>
)
}
if (!product) {
return (
<PageLayout layout="full-width">
<Title level={2}>Product not found</Title>
</PageLayout>
)
}
return (
<PageLayout layout="full-width">
<Row
justify="center"
align="middle"
style={{ textAlign: 'center', padding: '20px' }}
>
<Col span={24}>
<Title level={2}>{product.title}</Title>
<Paragraph>{product.description}</Paragraph>
</Col>
<Col span={24} style={{ marginTop: '20px' }}>
<Button
type="primary"
icon={<TwitterOutlined />}
onClick={() => handleGeneratePost('Twitter')}
style={{ margin: '0 10px' }}
>
Generate Twitter Post
</Button>
<Button
type="primary"
icon={<LinkedinOutlined />}
onClick={() => handleGeneratePost('LinkedIn')}
style={{ margin: '0 10px' }}
>
Generate LinkedIn Post
</Button>
<Button
type="primary"
icon={<FileTextOutlined />}
onClick={() => handleGeneratePost('Blog')}
style={{ margin: '0 10px' }}
>
Generate Blog Article
</Button>
</Col>
<MarkdownModal
visible={isModalVisible}
content={modalContent}
onClose={() => setIsModalVisible(false)}
/>
</Row>
</PageLayout>
)
}
This showcases the ability of changing the code, without disrupting previous functionality.
Technical Considerations
Marblism generates Next.js and NestJS code out-of-the-box, meaning you get a modern, modular, production-ready codebase.
Technical intricacies you might be interested in:
Next Steps & Resources
Congratulations! In this guide you've learned how to recreate Copy.ai on Marblism, in almost no time! This guide should serve as a base for further playtime on Marblism.
See what other's have built with Marblism.
Feeling like you're out of ideas? Check out MuckBrass.com to find and validate your startup ideas.