mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-03-29 11:12:19 +01:00
Remove blog for reviewing purposes
This commit is contained in:
parent
9ccb7c28ad
commit
e0dac1c93b
24
template/blog/.gitignore
vendored
24
template/blog/.gitignore
vendored
@ -1,24 +0,0 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# deployment
|
||||
.netlify/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
4
template/blog/.vscode/extensions.json
vendored
4
template/blog/.vscode/extensions.json
vendored
@ -1,4 +0,0 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
template/blog/.vscode/launch.json
vendored
11
template/blog/.vscode/launch.json
vendored
@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ ├── docs/
|
||||
│ │ └── config.ts
|
||||
│ └── env.d.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
@ -1,90 +0,0 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import starlightBlog from 'starlight-blog';
|
||||
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://your-site.com',
|
||||
trailingSlash: 'always',
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'Your SaaS',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
description: 'Documentation for your SaaS.',
|
||||
logo: {
|
||||
src: '/src/assets/logo.webp',
|
||||
alt: 'Your SaaS',
|
||||
},
|
||||
head: [
|
||||
// Add your script tags here. Below is an example for Google analytics, etc.
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
src: 'https://www.googletagmanager.com/gtag/js?id=<YOUR-GOOGLE-ANALYTICS-ID>',
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: 'script',
|
||||
content: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '<YOUR-GOOGLE-ANALYTICS-ID>');
|
||||
`,
|
||||
},
|
||||
],
|
||||
editLink: {
|
||||
baseUrl: 'https://github.com/<your-repo>',
|
||||
},
|
||||
components: {
|
||||
SiteTitle: './src/components/MyHeader.astro',
|
||||
ThemeSelect: './src/components/MyThemeSelect.astro',
|
||||
Head: './src/components/HeadWithOGImage.astro',
|
||||
PageTitle: './src/components/TitleWithBannerImage.astro',
|
||||
},
|
||||
social: {
|
||||
github: 'https://github.com/wasp-lang/open-saas',
|
||||
twitter: 'https://twitter.com/wasplang',
|
||||
discord: 'https://discord.gg/aCamt5wCpS',
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Start Here',
|
||||
items: [
|
||||
{
|
||||
label: 'Introduction',
|
||||
link: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Guides',
|
||||
items: [
|
||||
{
|
||||
label: 'Example Guide',
|
||||
link: '/guides/example/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
starlightBlog({
|
||||
title: 'Blog',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
authors: {
|
||||
Dev: {
|
||||
name: 'Dev',
|
||||
title: 'Dev @ Your SaaS',
|
||||
picture: '/CRAIG_ROCK.png', // Images in the `public` directory are supported.
|
||||
url: 'https://your-site.com',
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
tailwind({ applyBaseStyles: false }),
|
||||
],
|
||||
});
|
9170
template/blog/package-lock.json
generated
9170
template/blog/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "blog",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/starlight": "^0.29.2",
|
||||
"@astrojs/starlight-tailwind": "^2.0.3",
|
||||
"@astrojs/tailwind": "^5.1.2",
|
||||
"astro": "^4.16.15",
|
||||
"sharp": "^0.32.5",
|
||||
"starlight-blog": "^0.15.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
module.exports = {};
|
Binary file not shown.
Before Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
Before Width: | Height: | Size: 63 KiB |
@ -1,14 +0,0 @@
|
||||
# OG Images & Banner Images
|
||||
|
||||
When images are stored in this directory, they are automatically used as Open Graph (social media preview) Images and Cover/Banner Images for each blog post.
|
||||
|
||||
Images stored here must follow the naming convention `<post-slug>.webp` and must always be .webp files, e.g. `2023-11-21-coverlettergpt.webp`.
|
||||
|
||||
This is because OG Image URLs and Banner Images are automatically generated for each blog post based on the logic in the custom Title and Head components, e.g. `src/components/HeadWithOGImage.astro`:
|
||||
|
||||
```tsx
|
||||
const ogImageUrl = new URL(
|
||||
`/banner-images/${Astro.props.id.replace(/blog\//, '').replace(/\.\w+$/, '.webp')}`,
|
||||
Astro.site,
|
||||
)
|
||||
```
|
Binary file not shown.
Before Width: | Height: | Size: 145 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
@ -1,29 +0,0 @@
|
||||
---
|
||||
import type { Props } from '@astrojs/starlight/props'
|
||||
import Default from '@astrojs/starlight/components/Head.astro'
|
||||
import { BANNER_PATH, DEFAULT_BANNER_IMAGE, getBannerImageFilename, checkBannerImageExists } from './imagePaths'
|
||||
|
||||
const bannerImageFileName = getBannerImageFilename({ path: Astro.props.id })
|
||||
const imageExists = checkBannerImageExists({ bannerImageFileName })
|
||||
|
||||
// Get the URL of the social media preview image for the current post using its
|
||||
// slug ('Astro.props.id') and replace the path and file extension with `.webp`.
|
||||
let ogImageUrl = new URL(
|
||||
`${BANNER_PATH}/${DEFAULT_BANNER_IMAGE}`,
|
||||
Astro.site,
|
||||
);
|
||||
if (imageExists) {
|
||||
ogImageUrl = new URL(
|
||||
`${BANNER_PATH}/${bannerImageFileName}`,
|
||||
Astro.site,
|
||||
)
|
||||
}
|
||||
---
|
||||
|
||||
<Default {...Astro.props}><slot /></Default>
|
||||
|
||||
<!-- Open Graph images. -->
|
||||
<meta property="og:image" content={ogImageUrl} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta name="twitter:image" content={ogImageUrl} />
|
@ -1,90 +0,0 @@
|
||||
---
|
||||
import { logos } from 'virtual:starlight/user-images';
|
||||
import config from 'virtual:starlight/user-config';
|
||||
import blogConfig from 'virtual:starlight-blog-config'
|
||||
import type { Props } from '@astrojs/starlight/props';
|
||||
|
||||
const href = Astro.site
|
||||
const { siteTitle } = Astro.props;
|
||||
---
|
||||
<a {href} class="site-title sl-flex">
|
||||
{
|
||||
config.logo && logos.dark && (
|
||||
<>
|
||||
<img
|
||||
class:list={{ 'light:sl-hidden': !('src' in config.logo) }}
|
||||
alt={config.logo.alt}
|
||||
src={logos.dark.src}
|
||||
width={logos.dark.width}
|
||||
height={logos.dark.height}
|
||||
/>
|
||||
{/* Show light alternate if a user configure both light and dark logos. */}
|
||||
{!('src' in config.logo) && (
|
||||
<img
|
||||
class="dark:sl-hidden"
|
||||
alt={config.logo.alt}
|
||||
src={logos.light?.src}
|
||||
width={logos.light?.width}
|
||||
height={logos.light?.height}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
<span class:list={{ 'sr-only': config.logo?.replacesTitle }} class="dark:text-white hover:text-yellow-500">
|
||||
{siteTitle}
|
||||
</span>
|
||||
</a>
|
||||
<div>
|
||||
<a href="/" class="text-gray-900 hover:text-yellow-500 dark:text-white">Docs</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/blog/" class="text-gray-900 hover:text-yellow-500 dark:text-white">{blogConfig.title}</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.site-title {
|
||||
justify-self: flex-start;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
color: var(--sl-color-gray-9);
|
||||
gap: var(--sl-nav-gap);
|
||||
font-size: var(--sl-text-h4);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
padding-inline-end: 1rem;
|
||||
border-inline-end: 1px solid var(--sl-color-gray-5);
|
||||
}
|
||||
img {
|
||||
height: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
object-position: 0 50%;
|
||||
}
|
||||
div {
|
||||
border-inline-end: 1px solid var(--sl-color-gray-5);
|
||||
margin-left: 1rem;
|
||||
align-self: center;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
padding-inline-end: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 50rem) {
|
||||
div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: var(--sl-color-text-accent);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
opacity: 0.66;
|
||||
}
|
||||
</style>
|
@ -1,28 +0,0 @@
|
||||
---
|
||||
import StarlightThemeSelect from '@astrojs/starlight/components/ThemeSelect.astro'
|
||||
import type { Props } from '@astrojs/starlight/props'
|
||||
---
|
||||
|
||||
<StarlightThemeSelect {...Astro.props}>
|
||||
<slot />
|
||||
</StarlightThemeSelect>
|
||||
|
||||
<style>
|
||||
div {
|
||||
border-inline-end: 1px solid var(--sl-color-gray-5);
|
||||
display: none;
|
||||
padding-inline-end: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 50rem) {
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--sl-color-text-accent);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
@ -1,45 +0,0 @@
|
||||
---
|
||||
import type { Props } from '@astrojs/starlight/props'
|
||||
import { Image } from 'astro:assets';
|
||||
import { BANNER_PATH, getBannerImageFilename, checkBannerImageExists } from './imagePaths'
|
||||
|
||||
const { id, entry } = Astro.props;
|
||||
const { title, subtitle, hideBannerImage } = entry.data;
|
||||
const bannerImageFileName = getBannerImageFilename({ path: id })
|
||||
const imageExists = checkBannerImageExists({ bannerImageFileName })
|
||||
---
|
||||
|
||||
<h1 id='_top'>{title}</h1>
|
||||
{subtitle && <p class="subtitle">{subtitle}</p>}
|
||||
{imageExists && <div class="image-container">
|
||||
<Image src={`${BANNER_PATH}/${bannerImageFileName}`} loading="eager" alt={title} width="50" height="50" class={!hideBannerImage ? 'cover-image' : 'hidden'} />
|
||||
</div>}
|
||||
|
||||
<style>
|
||||
.image-container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--sl-text-h4);
|
||||
color: var(--sl-color-gray-2);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 1rem 0;
|
||||
font-size: var(--sl-text-h1);
|
||||
line-height: var(--sl-line-height-headings);
|
||||
font-weight: 600;
|
||||
color: var(--sl-color-white);
|
||||
}
|
||||
</style>
|
@ -1,16 +0,0 @@
|
||||
import path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export const BANNER_PATH = '/banner-images';
|
||||
|
||||
export const DEFAULT_BANNER_IMAGE = 'default-banner.webp';
|
||||
|
||||
export const getBannerImageFilename = ({ path }: { path: string }) =>
|
||||
path.replace(/.*\//, '').replace(/\.\w+$/, '.webp');
|
||||
|
||||
export const checkBannerImageExists = ({ bannerImageFileName }: { bannerImageFileName: string }) => {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const imagePath = path.join(__dirname, `../../public/${BANNER_PATH}`, bannerImageFileName);
|
||||
return existsSync(imagePath);
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { i18nSchema, docsSchema } from '@astrojs/starlight/schema';
|
||||
import { blogSchema } from 'starlight-blog/schema';
|
||||
import { z } from 'astro:content';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({
|
||||
schema: docsSchema({
|
||||
extend: (context) => {
|
||||
const blogSchemaResult = blogSchema(context);
|
||||
return z.object({
|
||||
...blogSchemaResult.shape,
|
||||
subtitle: z.string().optional(),
|
||||
hideBannerImage: z.boolean().optional(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
|
||||
};
|
@ -1,157 +0,0 @@
|
||||
---
|
||||
title: How I Built & Grew CoverLetterGPT to 5,000 Users and $200 MRR
|
||||
date: 2023-11-21
|
||||
tags: ["indiehacker", "saas", "sideproject"]
|
||||
subtitle: A guide to building a profitable, open-source side-project
|
||||
hideBannerImage: false # Banner images stored in public/banner-images/ are automatically used as cover images and social media preview images (og:image) for each blog post.
|
||||
authors:
|
||||
- name: vince
|
||||
title: Dev Rel @ Wasp
|
||||
url: https://wasp.sh
|
||||
---
|
||||
## Hey, I’m Vince…
|
||||
|
||||

|
||||
|
||||
I’m a self-taught developer that changed careers during the Covid Pandemic. I did it because I wanted a better career, enjoyed programming, and at the same time, had a keen interest in IndieHacking.
|
||||
<!--truncate-->
|
||||
If you’re not aware, IndieHacking is the movement of developers who build potentially profitable side-projects in their spare time. And there are some very successful examples of IndieHackers and “solopreneurs” out there inspiring others, such as [levels.io](http://levels.io) and [Marc Lou](https://twitter.com/marc_louvion).
|
||||
|
||||
This thought of being able to build my own side-project that could generate profit while I slept was always attractive to me.
|
||||
|
||||

|
||||
|
||||
So I’m happy to report that I’ve finally done it with my first software-as-a-service (SaaS) app, [CoverLetterGPT.xyz](http://CoverLetterGPT.xyz), which I launched in March 2023!
|
||||
|
||||
I’ll be the first to admit that the results aren’t spectacular, but they’re still something I’m very proud of:
|
||||
|
||||
- over 5,000 registered users
|
||||
- $203 monthly recurring revenue (MRR)
|
||||
|
||||
Below, I’m going to share with you how I built it (yes, it’s [open-source](https://github.com/vincanger/coverlettergpt)!), how I marketed and monetized it, along with a bunch of helpful resources to help you build your own profitable side-project.
|
||||
|
||||
## What the heck is CoverLetterGPT?
|
||||
|
||||
[CoverLetterGPT.xyz](http://CoverLetterGPT.xyz) was an idea I got after the OpenAI API was released. It’s an app that allows you to upload a PDF of your CV/resumé, along with the job description you’re applying to, and it will generate and edit unique cover letters for you based on this information.
|
||||
|
||||
{% embed https://youtu.be/ZhcFRD9cVrI %}
|
||||
|
||||
It also lets you save and manage your cover letters per each job, making it easy to make and apply to multiple jobs without having to keep copy and pasting all your important info into ChatGPT!
|
||||
|
||||
## What’s the Tech Stack?
|
||||
|
||||

|
||||
|
||||
CoverLetterGPT is entirely open-source, so you can [check out the code](https://github.com/vincanger/coverlettergpt), fork it, learn from it, make your own, submit a PR (I’d love you forever if you did 🙂)… whatever!
|
||||
|
||||
I built it using the [Wasp full-stack framework](https://wasp.sh) which allowed me to ship it about 10x faster.
|
||||
|
||||
Why?
|
||||
|
||||
Because [Wasp](https://wasp.sh) as a framework allows you to describe your app’s core features in a `main.wasp` config file. Then it continually compiles and “glues” these features into a React-ExpressJS-Prisma full-stack app for you.
|
||||
|
||||
All you have to focus on is writing the client and server-side logic, and Wasp will do the boring stuff for you, like authentication & authorization, server config, email sending, and cron jobs.
|
||||
|
||||
BTW, [Wasp](https://wasp.sh) is open-source and free and you can help the project out a ton by starring the repo on GitHub: [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) 🙏
|
||||
|
||||

|
||||
|
||||
{% cta [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) %} ⭐️ Thanks For Your Support 🙏 {% endcta %}
|
||||
|
||||
For the UI, I used [Chakra UI](https://chakra-ui.com/), as I always do. I like that it’s a component-based UI library. This helps me build UI’s a lot faster than I would with Tailwind or vanilla CSS.
|
||||
|
||||
For payments, I used [Stripe](https://www.notion.so/How-I-Built-and-Open-Sourced-CoverLetterGPT-5-000-users-200-MRR-0d32f13fa00a440fb8e08c8dbf2b8a27?pvs=21), (I’ll go into the details of monetization below).
|
||||
|
||||
The Server and Postgres Database are hosted on [https://railway.app](https://railway.app/), with the client on [Netlify.com](http://Netlify.com)’s free tier.
|
||||
|
||||

|
||||
|
||||
By the way, If you’re interested in building your own SaaS with almost the same stack as above, I also built a [free SaaS template](https://github.com/wasp-lang/SaaS-Template-GPT) you can use that will save you days of work!
|
||||
|
||||
## How I Marketed It
|
||||
|
||||
My biggest take-away from this whole project was that open-sourcing it was the best way to market it!
|
||||
|
||||
This seems counter-intuitive, right? Why would making the code available for anyone to see and copy be good for a business? You’re basically rolling out a red carpet for competitors, aren’t you?
|
||||
|
||||
Well, not quite.
|
||||
|
||||
First of all, the number of people who will realistically spend the time and energy launching a direct competitor is low. Also, most people interested in your open-source code want to learn some aspect of it and apply it to their own ideas, not just copy yours directly.
|
||||
|
||||
Secondly, and most importantly, the fact that it’s open-source makes people a lot more receptive to you talking about it.
|
||||
|
||||

|
||||
|
||||
When you present something you’ve built and give people the opportunity to learn from it, they’re much more welcoming! As a result, they’re more likely to upvote it, share it, use it, and recommend it to others.
|
||||
|
||||
This is exactly what happened with CoverLetterGPT! As a result of me sharing the open-source code, it get featured on the [IndieHackers.com](https://www.indiehackers.com/post/whats-new-don-t-build-things-no-one-wants-833ee752ba?utm_source=indie-hackers-emails&utm_campaign=ih-newsletter&utm_medium=email) newsletter (>100k subscribers), shared on blogs, and talked about on social media platforms.
|
||||
|
||||

|
||||
|
||||
And even though it’s a small, simple product, I tried launching it on [Product Hunt](http://producthunt.com), where it also performed considerably well.
|
||||
|
||||
So, all together, these initial efforts combined gave my product a good initial marketing presence. To this day, I haven’t really done much else to market it, except some twitter posts (and this post, if you want to consider it marketing 🤑).
|
||||
|
||||
## How I Monetized It
|
||||
|
||||
When I first launched in March 2023, I didn’t really expect anyone to pay for the product, but I wanted to learn how to use Stripe as a payments processor, thinking that the skills might be useful in the future.
|
||||
|
||||
So I started simple, and just put a one-time payment link for tips. No paywall, no subscriptions. It was entirely free to use with any tip amount welcome.
|
||||
|
||||
To my surprise, tips started coming in, with some as high as $10 dollars!
|
||||
|
||||
This encouraged me to force users to login to use the product, and add a paywall after users used up 3 credits.
|
||||
|
||||
My initial payment options were:
|
||||
|
||||
- $4.95 for a 3 months access
|
||||
- $2.95 for 10 cover letter generations
|
||||
|
||||

|
||||
|
||||
That went reasonably well until I implemented the ability for users to use GPT to make finer edits to their generated cover letters. That’s when I changed my pricing and that’s when better profits started to come in:
|
||||
|
||||
- $5.95 / month subscription with GPT-4
|
||||
- $2.95 / month subscription with GPT-3.5-turbo
|
||||
|
||||
Currently, over 90% of my customers choose the more powerful, more [expensive plan with GPT-4](https://openai.com/pricing), even though the 3 trial credits use the GPT-3.5-turbo model.
|
||||
|
||||
(I also integrated Bitcoin Lightning payments — check out the [repo](https://github.com/vincanger/coverlettergpt) if you want to learn how — but haven’t received any yet.)
|
||||
|
||||
Now, with an MRR of ~$203, my monthly profit of course depends on my costs, which are:
|
||||
|
||||
- Domain Name: $10/year
|
||||
- OpenAI bill: ~ $15/month
|
||||
- Hosting bill: ~ $3/month
|
||||
|
||||
Which leaves me at about ~ $183/month in profits 😀
|
||||
|
||||
## Future Plans
|
||||
|
||||
One of the most surprising aspects about [CoverLetterGPT.xyz](http://CoverLetterGPT.xyz)’s success is that, on the surface, the product is very simple. Also, I’ve done very little in the way of SEO marketing, and haven’t continued to market it much at all. The current growth is mostly organic at this point thanks to my initial marketing efforts.
|
||||
|
||||
But I still have some plans to make it better:
|
||||
|
||||
- buy a better top-level domain (TLD), like [CoverLetterGPT.ai](http://CoverLetterGPT.ai)
|
||||
- add more features, like the ability to generate interview questions based on the cover letters
|
||||
- improve the UX and make it look more “professional”
|
||||
|
||||
If you have any other ideas how I could improve it, drop me a comment, message me on [twitter/x](https://twitter.com/hot_town), or submit a [PR to the repo](https://github.com/vincanger/coverlettergpt).
|
||||
|
||||
## Final Words + More Resources
|
||||
|
||||
My intention with this article was to help others who might be considering launching their own SaaS product. So I hope that’s been the case here. If you still have any questions, don’t hesitate to ask.
|
||||
|
||||
Here are also the most important links from this article along with some further resources that will help in building and marketing your own profitable side-project:
|
||||
|
||||
- 👨💻 [CoverLetterGPT GitHub Repo](https://github.com/vincanger/coverlettergpt)
|
||||
- 💸 [Free Full-Stack SaaS Template w/ Google Auth, Stripe, GPT, & instructions in the README!](https://github.com/wasp-lang/SaaS-Template-GPT)
|
||||
- ✍️ [Initial CoverLetterGPT Reddit Post](https://www.reddit.com/r/webdev/comments/11uh4qo/comment/jco5ggp/?utm_source=share&utm_medium=web2x&context=3)
|
||||
- 🪓 [IndieHackers Feature](https://www.indiehackers.com/post/whats-new-don-t-build-things-no-one-wants-833ee752ba?utm_source=indie-hackers-emails&utm_campaign=ih-newsletter&utm_medium=email)
|
||||
- 💸 [Great Video on how to use Stripe CLI & Webhooks](https://www.youtube.com/watch?v=Psq5N5C-FGo&t=1041s)
|
||||
|
||||
Oh, and if you found these resources useful, don't forget to support Wasp by [starring the repo on GitHub](https://github.com/wasp-lang/wasp)!
|
||||
|
||||

|
||||
|
||||
{% cta [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) %} ⭐️ Thanks For Your Support 🙏 {% endcta %}
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
title: My first blog post
|
||||
date: 2023-11-20
|
||||
tags: ["blog", "post", "saas", "rocknroll"]
|
||||
authors: ["Dev"]
|
||||
---
|
||||
|
||||
## Hello
|
||||
|
||||
Hello world!
|
||||
|
||||

|
@ -1,9 +0,0 @@
|
||||
---
|
||||
title: Example Guide
|
||||
---
|
||||
|
||||
This is an example guide. It's a good idea to have a guide for each major feature of your project. Guides should be short and to the point. They should also be written in a way that is easy to understand for beginners.
|
||||
|
||||
## Example Heading
|
||||
|
||||
...
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
Hello World! 👋
|
2
template/blog/src/env.d.ts
vendored
2
template/blog/src/env.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
22
template/blog/src/virtual.d.ts
vendored
22
template/blog/src/virtual.d.ts
vendored
@ -1,22 +0,0 @@
|
||||
declare module 'virtual:starlight-blog-config' {
|
||||
const StarlightBlogConfig: import('./libs/config').StarlightBlogConfig;
|
||||
|
||||
export default StarlightBlogConfig;
|
||||
}
|
||||
declare module 'virtual:starlight/user-config' {
|
||||
const Config: import('@astrojs/starlight/types').StarlightConfig;
|
||||
|
||||
export default Config;
|
||||
}
|
||||
declare module 'virtual:starlight/user-images' {
|
||||
type ImageMetadata = import('astro').ImageMetadata;
|
||||
export const logos: {
|
||||
dark?: ImageMetadata;
|
||||
light?: ImageMetadata;
|
||||
};
|
||||
}
|
||||
declare module 'virtual:astro-config' {
|
||||
const Config: import('@astrojs/types').Config;
|
||||
|
||||
export default Config;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import starlightPlugin from "@astrojs/starlight-tailwind";
|
||||
import colors from "tailwindcss/colors";
|
||||
|
||||
const yellow = colors.yellow
|
||||
const gray = colors.gray
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
accent: yellow, gray
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [starlightPlugin()],
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user