update astro starlight & blog plugin (#334)
* update astro starlight & blog plugin * Delete logo.png * fix twitter preview image & add readme * improve banner image assignments & docs perf * change default banner * change image name * keep tabs on the tabs
@ -6,7 +6,7 @@ import tailwind from '@astrojs/tailwind';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://opensaas.sh',
|
||||
site: 'https://docs.opensaas.sh',
|
||||
trailingSlash: 'always',
|
||||
integrations: [
|
||||
starlight({
|
||||
@ -51,6 +51,7 @@ export default defineConfig({
|
||||
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',
|
||||
|
12419
opensaas-sh/blog/package-lock.json
generated
@ -10,13 +10,13 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.6.0",
|
||||
"@astrojs/starlight": "^0.22.2",
|
||||
"@astrojs/starlight-tailwind": "^2.0.2",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"astro": "^4.3.5",
|
||||
"@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.7.1",
|
||||
"starlight-blog": "^0.15.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
14
opensaas-sh/blog/public/banner-images/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# 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,
|
||||
)
|
||||
```
|
BIN
opensaas-sh/blog/public/banner-images/opensaas.webp
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
opensaas-sh/blog/src/assets/admin/admin-dashboard.png
Normal file
After Width: | Height: | Size: 342 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 248 KiB |
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 324 KiB |
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 170 KiB |
Before Width: | Height: | Size: 1003 KiB After Width: | Height: | Size: 1003 KiB |
Before Width: | Height: | Size: 6.4 MiB After Width: | Height: | Size: 6.4 MiB |
Before Width: | Height: | Size: 728 KiB After Width: | Height: | Size: 728 KiB |
Before Width: | Height: | Size: 772 KiB After Width: | Height: | Size: 772 KiB |
Before Width: | Height: | Size: 974 KiB After Width: | Height: | Size: 974 KiB |
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 550 KiB After Width: | Height: | Size: 550 KiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 730 KiB |
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 828 KiB After Width: | Height: | Size: 828 KiB |
Before Width: | Height: | Size: 530 KiB After Width: | Height: | Size: 530 KiB |
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 315 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 213 KiB |
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 251 KiB |
@ -1,18 +1,29 @@
|
||||
---
|
||||
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'
|
||||
|
||||
// Get the URL of the generated image for the current page using its
|
||||
// ID and replace the file extension with `.png`.
|
||||
const ogImageUrl = new URL(
|
||||
`/og-images/${Astro.props.id.replace(/blog\//, '').replace(/\.\w+$/, '.webp')}`,
|
||||
'https://docs.opensaas.sh',
|
||||
)
|
||||
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,
|
||||
)
|
||||
}
|
||||
---
|
||||
|
||||
<!-- Render the default <Head/> component. -->
|
||||
<Default {...Astro.props}><slot /></Default>
|
||||
|
||||
<!-- Render the <meta/> tags for the Open Graph images. -->
|
||||
<!-- 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} />
|
@ -4,10 +4,9 @@ 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">
|
||||
<a href='https://opensaas.sh' class="site-title sl-flex">
|
||||
{
|
||||
config.logo && logos.dark && (
|
||||
<>
|
||||
@ -17,6 +16,7 @@ const { siteTitle } = Astro.props;
|
||||
src={logos.dark.src}
|
||||
width={logos.dark.width}
|
||||
height={logos.dark.height}
|
||||
loading="eager"
|
||||
/>
|
||||
{/* Show light alternate if a user configure both light and dark logos. */}
|
||||
{!('src' in config.logo) && (
|
||||
@ -26,6 +26,7 @@ const { siteTitle } = Astro.props;
|
||||
src={logos.light?.src}
|
||||
width={logos.light?.width}
|
||||
height={logos.light?.height}
|
||||
loading="eager"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
45
opensaas-sh/blog/src/components/TitleWithBannerImage.astro
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
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>
|
16
opensaas-sh/blog/src/components/imagePaths.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export const BANNER_PATH = '/banner-images';
|
||||
|
||||
export const DEFAULT_BANNER_IMAGE = 'opensaas.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,8 +1,20 @@
|
||||
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: blogSchema() }) }),
|
||||
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,19 +1,20 @@
|
||||
---
|
||||
title: How I Built & Grew CoverLetterGPT to 5,000 Users and $200 MRR
|
||||
date: 2023-11-21
|
||||
tags: ["indiehacker", "saas", "sideproject"]
|
||||
tags: ["indiehacker", "saas", "sideproject"]
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
|
||||
## Hey, I’m Vince…
|
||||
|
||||

|
||||
<Image src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/az8xf61b2qxx1msfo4t5.png" alt="Vince Headshot" loading="lazy" width={700} height={700} />
|
||||
|
||||
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.
|
||||
|
||||

|
||||
<Image src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e1r07ajn3gysdscjdkns.png" alt="CoverLetterGPT" loading="lazy" width={700} height={700} />
|
||||
|
||||
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!
|
||||
|
||||
@ -22,19 +23,28 @@ I’ll be the first to admit that the results aren’t spectacular, but they’r
|
||||
- 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.
|
||||
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 %}
|
||||
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
|
||||
<iframe
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
|
||||
src="https://www.youtube.com/embed/ZhcFRD9cVrI"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
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?
|
||||
|
||||

|
||||
<Image src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xpb97bgrx98bwxemrg0o.png" alt="Tech Stack" loading="lazy" width={700} height={700} />
|
||||
|
||||
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!
|
||||
|
||||
@ -48,9 +58,9 @@ All you have to focus on is writing the client and server-side logic, and Wasp w
|
||||
|
||||
BTW, [Wasp](https://wasp-lang.dev) 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) 🙏
|
||||
|
||||

|
||||
<Image src='https://media1.giphy.com/media/ZfK4cXKJTTay1Ava29/giphy.gif?cid=7941fdc6pmqo30ll0e4rzdiisbtagx97sx5t0znx4lk0auju&ep=v1_gifs_search&rid=giphy.gif&ct=g' loading="lazy" alt="star wasp" width={500} height={500} />
|
||||
|
||||
{% cta [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) %} ⭐️ Thanks For Your Support 🙏 {% endcta %}
|
||||
[⭐️ Star Wasp on GitHub 🙏](https://www.github.com/wasp-lang/wasp)
|
||||
|
||||
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.
|
||||
|
||||
@ -58,9 +68,7 @@ For payments, I used [Stripe](https://www.notion.so/How-I-Built-and-Open-Sourced
|
||||
|
||||
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!
|
||||
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/open-saas) you can use that will save you days of work!
|
||||
|
||||
## How I Marketed It
|
||||
|
||||
@ -74,13 +82,13 @@ First of all, the number of people who will realistically spend the time and ene
|
||||
|
||||
Secondly, and most importantly, the fact that it’s open-source makes people a lot more receptive to you talking about it.
|
||||
|
||||

|
||||
<Image src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q79djej6doj2yq10l2og.png" alt="reddit" loading="lazy" width={700} height={700} />
|
||||
|
||||
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.
|
||||
|
||||

|
||||
<Image src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/44rlv65u97qhufbhqt0k.png" alt="product hunt" loading="lazy" width={700} height={700} />
|
||||
|
||||
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.
|
||||
|
||||
@ -101,7 +109,7 @@ My initial payment options were:
|
||||
- $4.95 for a 3 months access
|
||||
- $2.95 for 10 cover letter generations
|
||||
|
||||

|
||||
<Image src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/golo3tnh3o0sy5sujrer.png" alt="pricing page" loading="lazy" width={700} height={700} />
|
||||
|
||||
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:
|
||||
|
||||
@ -146,6 +154,6 @@ Here are also the most important links from this article along with some further
|
||||
|
||||
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)!
|
||||
|
||||

|
||||
<Image src="https://res.cloudinary.com/practicaldev/image/fetch/s--OCpry2p9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bky8z46ii7ayejprrqw3.gif" alt="star wasp" loading="lazy" width={500} height={500} />
|
||||
|
||||
{% cta [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) %} ⭐️ Thanks For Your Support 🙏 {% endcta %}
|
||||
[⭐️ Thanks For Your Support 🙏 ](https://www.github.com/wasp-lang/wasp)
|
@ -1,11 +1,19 @@
|
||||
---
|
||||
title: 🍪 THE MOST ANNOYING COOKIE BANNER EVER HACKATHON 🤬
|
||||
date: 2024-10-10
|
||||
tags: ["cookie consent", "saas", "sideproject", "hackathon"]
|
||||
cover:
|
||||
alt: Annoying Cookie Banner Contest
|
||||
image: "/cookie-consent/annoying-cookie-banners.jpg"
|
||||
tags:
|
||||
- cookie consent
|
||||
- saas
|
||||
- sideproject
|
||||
- hackathon
|
||||
hideBannerImage: true
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import wheel from '@assets/cookie-consent/wheel.gif';
|
||||
import enter from '@assets/cookie-consent/enter.gif';
|
||||
import keyboard from '@assets/cookie-consent/keyboard.jpg';
|
||||
import share from '@assets/cookie-consent/image.png';
|
||||
|
||||
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
|
||||
<iframe
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
|
||||
@ -26,11 +34,11 @@ Cookie consent banners annoy us all. So we thought, why not have some fun with t
|
||||
|
||||
1. *The Cookie Consent Wheel of Fortune:*
|
||||
|
||||

|
||||
<Image src={wheel} alt='Consent wheel' loading='lazy' />
|
||||
|
||||
2. *The “Hit Enter When the Red Ball is Over the Accept Button to Consent” Banner:*
|
||||
|
||||

|
||||
<Image src={enter} alt='Enter to win' loading='lazy' />
|
||||
|
||||
|
||||
Now it’s time for you to get creative. Btw, if you’re looking for some inspiration, check out these [Ridiculous Volume Slider UI’s](https://uxdesign.cc/the-worst-volume-control-ui-in-the-world-60713dc86950).
|
||||
@ -46,7 +54,7 @@ The 2 winners will be selected by:
|
||||
|
||||
The community will get a chance to vote in a battle royale style elimination tournament, where two banners will go head-to-head and the winner will advance to the next round.
|
||||
|
||||

|
||||
<Image src={keyboard} alt='Keyboard' loading='lazy' />
|
||||
|
||||
(The brand/style will depend on the winner's location, but we'll do our best to find one with a Wasp look and feel 😃)
|
||||
|
||||
@ -56,7 +64,7 @@ The community will get a chance to vote in a battle royale style elimination tou
|
||||
- If you prefer to work in your own editor, just click on the `Create a repository` button after you fork the template
|
||||
- When finished with your banner, click on `Share` in the top left, and in the `Embed` tab, click `Copy URL` with the following settings:
|
||||
|
||||

|
||||
<Image src={share} alt='Share' loading='lazy' />
|
||||
|
||||
- Next, [edit the `MOST-ANNOYING-COOKIE-BANNER.md` file](https://github.com/wasp-lang/open-saas/edit/main/MOST-ANNOYING-COOKIE-BANNER.md) on the Open SaaS repo.
|
||||
- Enter your GitHub username followed by the embed link you copied from Stackblitz
|
@ -1,12 +1,21 @@
|
||||
---
|
||||
title: We Made the Most Annoying Cookie Banners Ever
|
||||
date: 2024-11-26
|
||||
tags: ["cookie consent", "saas", "sideproject", "hackathon"]
|
||||
cover:
|
||||
alt: the Most Annoying Cookie Banners
|
||||
image: "/cookie-consent/annoying-cookie-banners.jpg"
|
||||
tags:
|
||||
- cookie consent
|
||||
- saas
|
||||
- sideproject
|
||||
- hackathon
|
||||
subtitle: and it was totally worth it
|
||||
---
|
||||
import VideoPlayer from '../../../components/VideoPlayer.astro';
|
||||
import { Image } from 'astro:assets';
|
||||
import camblackwood from '@assets/cookie-banner-hackathon/295-camblackwood.mp4';
|
||||
import gangnam from '@assets/cookie-banner-hackathon/300-lezzz-sound.mp4';
|
||||
import wheredaway from '@assets/cookie-banner-hackathon/302-fecony-whereda.mp4';
|
||||
import henryboyd from '@assets/cookie-banner-hackathon/296-henryboyd.mp4';
|
||||
import wardbox from '@assets/cookie-banner-hackathon/286-wardbox.mp4';
|
||||
import gangnamwinner from '@assets/cookie-banner-hackathon/285-3umaGH-gangnam.mp4';
|
||||
|
||||
## The Most Annoying Cookie Consent Banner Ever Hackathon
|
||||
|
||||
@ -24,7 +33,7 @@ This submission by [Cam Blackwood](https://www.tiktok.com/@cameronblackwoodcode/
|
||||
|
||||
Thanks for the reality check, Cam.
|
||||
|
||||
<VideoPlayer src="/cookie-banner-hackathon/295-camblackwood.mp4" />
|
||||
<VideoPlayer src={camblackwood} />
|
||||
|
||||
## Windows of Time
|
||||
|
||||
@ -32,7 +41,7 @@ Do you ever feel like cookie consent banners are UX design pattern from the past
|
||||
|
||||
Disturbing, yet oddly comforting.
|
||||
|
||||
<VideoPlayer src="/cookie-banner-hackathon/300-lezzz-sound.mp4" />
|
||||
<VideoPlayer src={gangnam} />
|
||||
|
||||
## Find all the Cookies
|
||||
|
||||
@ -42,7 +51,7 @@ This submission by [Fecony](https://github.com/fecony), Wasp community meme lord
|
||||
|
||||
Well played, Fecony.
|
||||
|
||||
<VideoPlayer src="/cookie-banner-hackathon/302-fecony-whereda.mp4" />
|
||||
<VideoPlayer src={wheredaway} />
|
||||
|
||||
## Fresh Batch of Cookies
|
||||
|
||||
@ -50,7 +59,7 @@ Most of us probably just smash the "accept" or "reject" button without even read
|
||||
|
||||
And that's a whole lot of cookies.
|
||||
|
||||
<VideoPlayer src="/cookie-banner-hackathon/296-henryboyd.mp4" />
|
||||
<VideoPlayer src={henryboyd} />
|
||||
|
||||
## Cookie Management Application Process
|
||||
|
||||
@ -58,7 +67,7 @@ What's more annoying than cookie consent banners? Probably job applications. Wel
|
||||
|
||||
Now all we have to do is wait for the rejection email.
|
||||
|
||||
<VideoPlayer src="/cookie-banner-hackathon/286-wardbox.mp4" />
|
||||
<VideoPlayer src={wardbox} />
|
||||
|
||||
## Grand Prize Winner: Gangnam Style Beat
|
||||
|
||||
@ -68,7 +77,7 @@ Make sure you turn on the sound for this one!
|
||||
|
||||
🎤 _Eeeeh, sexy cookie. Op! op-op-op!_ 🎵
|
||||
|
||||
<VideoPlayer src="/cookie-banner-hackathon/285-3umaGH-gangnam.mp4" />
|
||||
<VideoPlayer src={gangnamwinner} />
|
||||
## And there you have it!
|
||||
|
||||
Thanks to everyone who participated! We had a lot of fun looking at all the submissions and we're glad to see that the community is as creative (and annoying) as ever.
|
||||
@ -79,7 +88,7 @@ At [Wasp](https://wasp.sh/) we're working hard to build a modern, open-source fu
|
||||
|
||||
The easiest way to show your support is just to star the Wasp repo! 🐝 It helps us spread the word and motivates us to keep building.
|
||||
|
||||

|
||||
<Image src='https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axqiv01tl1pha9ougp21.gif' alt='friendly handshake' width="500" height="500" loading='lazy' />
|
||||
|
||||
<div className="cta">
|
||||
<a href="https://github.com/wasp-lang/wasp" target="_blank" rel="noopener noreferrer">
|
||||
|
@ -2,9 +2,15 @@
|
||||
title: Admin Dashboard
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
This is a reference on how the Admin dashboard is set up and works.
|
||||
import { Image } from 'astro:assets';
|
||||
import dbStudio from '@assets/stripe/db-studio.png';
|
||||
import adminDashboard from '@assets/admin/admin-dashboard.png';
|
||||
|
||||
This is a reference on how the Admin dashboard, available at `/admin`, is set up.
|
||||
|
||||
<Image src={adminDashboard} alt="admin dashboard" loading="eager" />
|
||||
|
||||
## Permissions
|
||||
|
||||
@ -34,7 +40,7 @@ Or if you've already logged in with an email address that you want to give admin
|
||||
wasp db studio
|
||||
```
|
||||
|
||||

|
||||
<Image src={dbStudio} alt="db studio" loading="lazy" />
|
||||
|
||||
---
|
||||
|
||||
@ -49,8 +55,6 @@ If you're finding this template and its guides useful, consider giving us [a sta
|
||||
### Analytics Dashboard
|
||||
The Admin analytics dashboard is a single place for you to view your most important metrics and perform some admin tasks. At the moment, it pulls data from:
|
||||
|
||||
<!-- TODO: add photo -->
|
||||
|
||||
- [Payments Processor](/guides/payments-integration/):
|
||||
- total revenue
|
||||
- revenue for each day of the past week
|
@ -2,7 +2,7 @@
|
||||
title: User Overview
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
|
||||
This reference will help you understand how the User entity works in this template.
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: Analytics
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
This guide will show you how to integrate analytics for your app. You can choose between [Google Analytics](#google-analytics) and [Plausible](#plausible).
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: Authentication
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
|
||||
Setting up your app's authentication is easy with Wasp. In fact, it's already set up for you in the `main.wasp` file:
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: Authorization
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
|
||||
This guide will help you get started with authorization in your SaaS app.
|
||||
|
@ -2,10 +2,13 @@
|
||||
title: Cookie Consent Modal
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import cookieBanner from '@assets/cookie-consent/cookiebanner.png';
|
||||
import preferences from '@assets/cookie-consent/preferences.png';
|
||||
|
||||
<img src="/cookie-consent/cookiebanner.png" alt="cookie banner" width="400px" />
|
||||
<Image src={cookieBanner} alt="cookie banner" width="400px" />
|
||||
|
||||
Cookie consent banners are annoying, we know. But they are legally required in many countries, so we have to deal with them.
|
||||
|
||||
@ -76,7 +79,7 @@ You should also add a link to your terms and privacy policy within `consentModal
|
||||
|
||||
If you've added more than just Google Analytics cookies to your app, you can allow users to control which cookies they want to accept or reject. For example, if you've added marketing cookies, you can add a button to the modal that allows users to reject them, while accepting analytics cookies.
|
||||
|
||||

|
||||
<Image src={preferences} alt="fine-grained cookie control" loading="lazy" />
|
||||
|
||||
To do that, you can change the `preferencesModal.sections` property in `config.language`. Any section that you add to `preferencesModal.sections` must match a `linkedCategory` in the `config.categories` property. Make sure you also add a `showPreferencesBtn` property to `consentModal` (highlighted below).
|
||||
|
@ -2,8 +2,12 @@
|
||||
title: Deploying
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import npmVersion from '@assets/stripe/npm-version.png';
|
||||
import stripeListenEvents from '@assets/stripe/listen-to-stripe-events.png';
|
||||
import stripeSigningSecret from '@assets/stripe/stripe-webhook-signing-secret.png';
|
||||
|
||||
Because this SaaS app is a React/NodeJS/Postgres app built on top of [Wasp](https://wasp-lang.dev), Open SaaS can take advantage of Wasp's easy, one-command deploy to Fly.io or manual deploy to any provider of your choice.
|
||||
|
||||
@ -149,7 +153,7 @@ export const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
- If your default version on the Stripe dashboard is not the latest version, and you don't want to [upgrade to the latest version](https://docs.stripe.com/upgrades#how-can-i-upgrade-my-api), because e.g. you have other projects that depend on the current version, you can find and install the Stripe NPM package version that matches your default API version by following these steps:
|
||||
- Find and note the date of your default API version in the [developer dashboard](https://dashboard.stripe.com/developers).
|
||||
- Go to the [Stripe NPM package](https://www.npmjs.com/package/stripe) page and hover over `Published` date column until you find the package release that matches your version. For example, here we find the NPM version that matches the default API version of `2023-08-16` in our dashboard, which is `13.x.x`.
|
||||

|
||||
<Image src={npmVersion} alt="npm version" loading="lazy" />
|
||||
- Install the correct version of the Stripe NPM package by running, :
|
||||
```sh
|
||||
npm install stripe@x.x.x # e.g. npm install stripe@13.11.0
|
||||
@ -161,14 +165,14 @@ export const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
1. go to [https://dashboard.stripe.com/webhooks](https://dashboard.stripe.com/webhooks)
|
||||
2. click on `+ add endpoint`
|
||||
3. enter your endpoint url, which will be the url of your deployed server + `/payments-webhook`, e.g. `https://open-saas-wasp-sh-server.fly.dev/payments-webhook`
|
||||

|
||||
<Image src={stripeListenEvents} alt="listen events" loading="lazy" />
|
||||
4. select the events you want to listen to. These should be the same events you're consuming in your webhook. For example, if you haven't added any additional events to the webhook and are using the defaults that came with this template, then you'll need to add:
|
||||
<br/>- `account.updated`
|
||||
<br/>- `checkout.session.completed`
|
||||
<br/>- `customer.subscription.deleted`
|
||||
<br/>- `customer.subscription.updated`
|
||||
<br/>- `invoice.paid`
|
||||

|
||||
<Image src={stripeSigningSecret} alt="signing secret" loading="lazy" />
|
||||
5. after that, go to the webhook you just created and `reveal` the new signing secret.
|
||||
6. add this secret to your deployed server's `STRIPE_WEBHOOK_SECRET=` environment variable. <br/>If you've deployed to Fly.io, you can do that easily with the following command:
|
||||
```sh
|
@ -2,7 +2,7 @@
|
||||
title: Email Sending
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
|
||||
|
@ -2,8 +2,17 @@
|
||||
title: File Uploading
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import findS3 from '@assets/file-uploads/find-s3.png';
|
||||
import createBucket from '@assets/file-uploads/create-bucket.png';
|
||||
import defaultSettings from '@assets/file-uploads/default-settings.png';
|
||||
import newBucket from '@assets/file-uploads/new-bucket.png';
|
||||
import permissions from '@assets/file-uploads/permissions.png';
|
||||
import cors from '@assets/file-uploads/cors.png';
|
||||
import username from '@assets/file-uploads/username.png';
|
||||
import keys from '@assets/file-uploads/keys.png';
|
||||
|
||||
This guide will show you how to set up file uploading in your SaaS app.
|
||||
|
||||
@ -52,23 +61,23 @@ To do so, follow the steps in this external guide: [Creating IAM users and S3 bu
|
||||
Once you are logged in with your IAM user, you'll need to create an S3 bucket to store your files.
|
||||
|
||||
1. Navigate to the S3 service in the AWS console
|
||||

|
||||
<Image src={findS3} alt="find s3" loading="lazy" />
|
||||
2. Click on the `Create bucket` button
|
||||

|
||||
<Image src={createBucket} alt="create bucket" loading="lazy" />
|
||||
3. Fill in the bucket name and region
|
||||
4. **Leave all the settings as default** and click `Create bucket`
|
||||

|
||||
<Image src={defaultSettings} alt="bucket settings" loading="lazy" />
|
||||
|
||||
### Change the CORS settings
|
||||
|
||||
Now we need to change some permissions on the bucket to allow for file uploads from your app.
|
||||
|
||||
1. Click on the bucket you just created
|
||||

|
||||
<Image src={newBucket} alt="new bucket" loading="lazy" />
|
||||
2. Click on the `Permissions` tab
|
||||

|
||||
<Image src={permissions} alt="permissions" loading="lazy" />
|
||||
3. Scroll down to the `Cross-origin resource sharing (CORS)` section and click `Edit`
|
||||

|
||||
<Image src={cors} alt="cors" loading="lazy" />
|
||||
5. Paste the following CORS configuration and click `Save changes`:
|
||||
```json
|
||||
[
|
||||
@ -93,11 +102,11 @@ Now we need to change some permissions on the bucket to allow for file uploads f
|
||||
Now that you have your S3 bucket set up, you'll need to get your S3 credentials to use in your app.
|
||||
|
||||
1. Click on your username in the top right corner of the AWS console and select `Security Credentials`
|
||||

|
||||
<Image src={username} alt="username" loading="lazy" />
|
||||
2. Scroll down to the `Access keys` section
|
||||
3. Click on `Create Access Key`
|
||||
4. Select the `Application running on an AWS service` option and create the access key
|
||||

|
||||
<Image src={keys} alt="keys" loading="lazy" />
|
||||
5. Copy the `Access key ID` and `Secret access key` and paste them in your `src/app/.env.server` file:
|
||||
```sh
|
||||
AWS_S3_IAM_ACCESS_KEY=ACK...
|
@ -2,8 +2,19 @@
|
||||
title: Payments Integration
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import testApiKeys from '@assets/stripe/api-keys.png';
|
||||
import testProduct from '@assets/stripe/test-product.png';
|
||||
import priceIds from '@assets/stripe/price-ids.png';
|
||||
import switchPlans from '@assets/stripe/switch-plans.png';
|
||||
import dbStudio from '@assets/stripe/db-studio.png';
|
||||
import addProduct from '@assets/lemon-squeezy/add-product.png';
|
||||
import addVariant from '@assets/lemon-squeezy/add-variant.png';
|
||||
import variantId from '@assets/lemon-squeezy/variant-id.png';
|
||||
import subscriptionVariantIds from '@assets/lemon-squeezy/subscription-variant-ids.png';
|
||||
import ngrok from '@assets/lemon-squeezy/ngrok.png';
|
||||
|
||||
This guide will show you how to set up Payments for testing and local development with the following payment processors:
|
||||
- Stripe
|
||||
@ -55,7 +66,7 @@ If you're finding this template and its guides useful, consider giving us [a sta
|
||||
|
||||
Once you've created your account, you'll need to get your test API keys. You can do that by navigating to [https://dashboard.stripe.com/test/apikeys](https://dashboard.stripe.com/test/apikeys) or by going to the [Stripe Dashboard](https://dashboard.stripe.com/test/dashboard) and clicking on the `Developers`.
|
||||
|
||||

|
||||
<Image src={testApiKeys} alt="test api keys" loading="lazy" />
|
||||
|
||||
- Click on the `Reveal test key token` button and copy the `Secret key`.
|
||||
- Paste it in your `.env.server` file under `STRIPE_API_KEY=`
|
||||
@ -64,7 +75,7 @@ Once you've created your account, you'll need to get your test API keys. You can
|
||||
|
||||
To create a test product, go to the test products url [https://dashboard.stripe.com/test/products](https://dashboard.stripe.com/test/products), or after navigating to your dashboard, click the `test mode` toggle.
|
||||
|
||||

|
||||
<Image src={testProduct} alt="test product" loading="lazy" />
|
||||
|
||||
- Click on the `Add a product` button and fill in the relevant information for your product.
|
||||
- Make sure you select `Software as a service (SaaS)` as the product type.
|
||||
@ -73,7 +84,7 @@ To create a test product, go to the test products url [https://dashboard.stripe.
|
||||
- If you intend to let your users switch between two subscription plans, e.g. upgrade from hobby to pro, you'll need to create two separate products and with their own price IDs. The ability for users to swich plans can then be configured later in the [Customer Portal](#set-up-the-customer-portal).
|
||||
- If you want to add different price tiers for the same product (e.g. monthly and yearly), click the `Add another price` button at the buttom.
|
||||
|
||||

|
||||
<Image src={priceIds} alt="price ids" loading="lazy" />
|
||||
|
||||
- After you save the product, you'll be directed to the product page.
|
||||
- Copy the price IDs and paste them in the `.env.server` file
|
||||
@ -100,7 +111,7 @@ STRIPE_CUSTOMER_PORTAL_URL=<your-test-customer-portal-link>
|
||||
|
||||
If you'd like to give users the ability to switch between different plans, e.g. upgrade from a hobby to a pro subscription, go down to the `Subscriptions` dropdown and select `customers can switch plans`.
|
||||
|
||||

|
||||
<Image src={switchPlans} alt="switch plans" loading="lazy" />
|
||||
|
||||
Then select the products you'd like them to be able to switch between.
|
||||
|
||||
@ -205,7 +216,7 @@ You can then test the payment flow via the client by doing the following:
|
||||
wasp db studio
|
||||
```
|
||||
|
||||

|
||||
<Image src={dbStudio} alt="db studio" loading="lazy" />
|
||||
|
||||
- Navigate to `localhost:5555` and click on the `users` table. You should see the `subscriptionStatus` is `active` for the user that just made the purchase.
|
||||
|
||||
@ -240,14 +251,14 @@ To create a test product, go to the test products url [https://app.lemonsqueezy.
|
||||
- Click on the `+ New Product` button and fill in the relevant information for your product.
|
||||
- Fill in the general information.
|
||||
- For pricing, select the type of product you'd like to create, e.g. `Subscription` for a recurring monthly payment product or `Single Payment` for credits-based product.
|
||||

|
||||
<Image src={addProduct} alt="add product" loading="lazy" />
|
||||
- Make sure you select `Software as a service (SaaS)` as the Tax category type.
|
||||
- If you want to add different price tiers for `Subscription` products, click on `add variant` under the `variants` tab. Here you can input the name of the variant (e.g. "Hobby", "Pro"), and that variant's price.
|
||||

|
||||
<Image src={addVariant} alt="add variant" loading="lazy" />
|
||||
- For a product with no variants, on the product page, click the `...` menu button and select `Copy variant ID`
|
||||

|
||||
<Image src={variantId} alt="variant id" loading="lazy" />
|
||||
- For a product with variants, on the product page, click on the product, go to the variants tab and select `Copy ID` for each variant.
|
||||

|
||||
<Image src={subscriptionVariantIds} alt="subscription variant ids" loading="lazy" />
|
||||
- Paste these IDs in the `.env.server` file:
|
||||
- We've set you up with two example subscription product environment variables, `PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID=` and `PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID=`.
|
||||
- As well as a one-time payment product/credits-based environment variable, `PAYMENTS_CREDITS__10_PLAN_ID=`.
|
||||
@ -264,7 +275,7 @@ Once installed, and with your wasp app running, run:
|
||||
ngrok http 3001
|
||||
```
|
||||
|
||||

|
||||
<Image src={ngrok} alt="ngrok" loading="lazy" />
|
||||
|
||||
Ngrok will output a forwarding address for you. Copy and paste this address and add `/payments-webhook` to the end (this URL path has been configured for you already in `main.wasp` under the `api paymentsWebhook` definition). It should look something like this:
|
||||
|
@ -2,8 +2,10 @@
|
||||
title: SEO
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import openSaaSGoogle from '@assets/seo/open-saas-google.png';
|
||||
|
||||
This guides explains how to improve SEO for of your app
|
||||
|
||||
@ -64,4 +66,4 @@ Open SaaS and Wasp do not currently have a SSR option (although it is coming soo
|
||||
That's because the meta tags for the landing page (described above), plus the Astro docs/blog provided with Open SaaS are more than enough! Not to mention, Google is also able to crawl websites with JavaScript activated, making SSR unnecessary.
|
||||
|
||||
For example, try searching "Open SaaS" on Google and you'll see this App, which was built with this template, as the first result!
|
||||

|
||||
<Image src={openSaaSGoogle} alt="open-saas-google" loading="lazy" />
|
@ -2,7 +2,7 @@
|
||||
title: Tests
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
|
||||
This guide will show you how to use the included end-to-end (e2e) tests for your Open SaaS application.
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: Introduction
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
import HiddenLLMHelper from '../../components/HiddenLLMHelper.astro';
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: Getting Started
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
|
||||
This guide will help you get your new SaaS app up and running.
|
||||
@ -17,8 +17,8 @@ Your version of Node.js must be >= 18.
|
||||
To switch easily between Node.js versions, we recommend using [nvm](https://github.com/nvm-sh/nvm).
|
||||
|
||||
:::note[Installing and using nvm]
|
||||
<details>
|
||||
<summary>
|
||||
<details aria-label="Installing and using nvm">
|
||||
<summary aria-label="Need help with nvm?">
|
||||
Need help with nvm?
|
||||
</summary>
|
||||
<div>
|
||||
@ -47,7 +47,7 @@ node -v
|
||||
|
||||
to check the version of Node.js currently being used in this shell session.
|
||||
|
||||
Check NVM repo for more details: https://github.com/nvm-sh/nvm.
|
||||
Check NVM repo for more details: [https://github.com/nvm-sh/nvm](https://github.com/nvm-sh/nvm).
|
||||
|
||||
</div>
|
||||
</details>
|
||||
@ -63,11 +63,11 @@ curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||
```
|
||||
|
||||
:::caution[Bad CPU type in executable]
|
||||
<details>
|
||||
<summary>
|
||||
<details aria-label="Bad CPU type in executable">
|
||||
<summary aria-label="Are you getting this error on a Mac (Apple Silicon)?">
|
||||
Are you getting this error on a Mac (Apple Silicon)?
|
||||
</summary>
|
||||
Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install <a href='https://support.apple.com/en-us/HT211861'>Rosetta on your Mac</a> if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal
|
||||
Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install <a href='https://support.apple.com/en-us/HT211861' alt='Rosetta on your Mac'>Rosetta on your Mac</a> if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal
|
||||
|
||||
```bash
|
||||
softwareupdate --install-rosetta
|
||||
@ -89,8 +89,8 @@ curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||
```
|
||||
|
||||
:::caution[WSL2 and file system issues]
|
||||
<details>
|
||||
<summary>
|
||||
<details aria-label="Are you getting file system issues using WSL2?">
|
||||
<summary aria-label="Are you getting file system issues using WSL2?">
|
||||
Are you getting file system issues using WSL2?
|
||||
</summary>
|
||||
If you are using WSL2, make sure that your Wasp project is not on the Windows file system, <b>but instead on the Linux file system</b>. Otherwise, Wasp won't be able to detect file changes, due to this <a href='https://github.com/microsoft/WSL/issues/4739'>issue in WSL2</a>.
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: Guided Tour
|
||||
banner:
|
||||
content: |
|
||||
🆕 Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
Open SaaS is now running on <b><a href='https://wasp-lang.dev'>Wasp v0.15</a></b>! <br/>⚙️<br/>If you're running an older version and would like to upgrade, please follow the <a href="https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15">migration instructions.</a>
|
||||
---
|
||||
|
||||
Awesome, you now have your very own SaaS app up and running! But, first, here are some important things you need to know about your app in its current state:
|
||||
|
@ -1,3 +1,10 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@assets/*": ["src/assets/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import tailwind from '@astrojs/tailwind';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://opensaas.sh',
|
||||
site: 'https://your-site.com',
|
||||
trailingSlash: 'always',
|
||||
integrations: [
|
||||
starlight({
|
||||
@ -42,8 +42,9 @@ export default defineConfig({
|
||||
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/wasp_lang',
|
||||
@ -74,11 +75,11 @@ export default defineConfig({
|
||||
title: 'Blog',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
authors: {
|
||||
vince: {
|
||||
name: 'Vince',
|
||||
title: 'Dev Rel @ Wasp',
|
||||
Dev: {
|
||||
name: 'Dev',
|
||||
title: 'Dev @ Your SaaS',
|
||||
picture: '/CRAIG_ROCK.png', // Images in the `public` directory are supported.
|
||||
url: 'https://wasp-lang.dev',
|
||||
url: 'https://your-site.com',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
3924
template/blog/package-lock.json
generated
@ -10,13 +10,13 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.6.0",
|
||||
"@astrojs/starlight": "^0.22.2",
|
||||
"@astrojs/starlight-tailwind": "^2.0.2",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"astro": "^4.3.5",
|
||||
"@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.7.1",
|
||||
"starlight-blog": "^0.15.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 63 KiB |
14
template/blog/public/banner-images/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# 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,
|
||||
)
|
||||
```
|
BIN
template/blog/public/banner-images/default-banner.webp
Normal file
After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 24 KiB |
BIN
template/blog/src/assets/logo.webp
Normal file
After Width: | Height: | Size: 13 KiB |
29
template/blog/src/components/HeadWithOGImage.astro
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
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} />
|
45
template/blog/src/components/TitleWithBannerImage.astro
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
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>
|
16
template/blog/src/components/imagePaths.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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,8 +1,20 @@
|
||||
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: blogSchema() }) }),
|
||||
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() }),
|
||||
};
|
||||
|
@ -2,6 +2,12 @@
|
||||
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-lang.dev
|
||||
---
|
||||
## Hey, I’m Vince…
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
---
|
||||
title: My first blog post
|
||||
date: 2023-11-20
|
||||
authors:
|
||||
- name: Craig Man
|
||||
title: Rock n Roller
|
||||
picture: /CRAIG_ROCK.png
|
||||
tags: ["blog", "post", "saas", "rocknroll"]
|
||||
authors: ["Dev"]
|
||||
---
|
||||
|
||||
## Hello
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
}
|
||||
|