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
This commit is contained in:
vincanger 2024-12-04 10:34:07 +01:00 committed by GitHub
parent fd509b2010
commit 54b4141820
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 7267 additions and 9615 deletions

View File

@ -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',

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View 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,
)
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 248 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 324 KiB

View File

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

Before

Width:  |  Height:  |  Size: 1003 KiB

After

Width:  |  Height:  |  Size: 1003 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 MiB

After

Width:  |  Height:  |  Size: 6.4 MiB

View File

Before

Width:  |  Height:  |  Size: 728 KiB

After

Width:  |  Height:  |  Size: 728 KiB

View File

Before

Width:  |  Height:  |  Size: 772 KiB

After

Width:  |  Height:  |  Size: 772 KiB

View File

Before

Width:  |  Height:  |  Size: 974 KiB

After

Width:  |  Height:  |  Size: 974 KiB

View File

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

View File

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 550 KiB

After

Width:  |  Height:  |  Size: 550 KiB

View File

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

View File

Before

Width:  |  Height:  |  Size: 730 KiB

After

Width:  |  Height:  |  Size: 730 KiB

View File

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 828 KiB

After

Width:  |  Height:  |  Size: 828 KiB

View File

Before

Width:  |  Height:  |  Size: 530 KiB

After

Width:  |  Height:  |  Size: 530 KiB

View File

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

View File

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

View File

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

View File

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

View File

@ -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} />

View File

@ -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"
/>
)}
</>

View 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>

View 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);
};

View File

@ -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() }),
};
};

View File

@ -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, Im Vince…
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/az8xf61b2qxx1msfo4t5.png)
<Image src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/az8xf61b2qxx1msfo4t5.png" alt="Vince Headshot" loading="lazy" width={700} height={700} />
Im 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 youre 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 description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e1r07ajn3gysdscjdkns.png)
<Image src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e1r07ajn3gysdscjdkns.png" alt="CoverLetterGPT" loading="lazy" width={700} height={700} />
So Im happy to report that Ive 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 @@ Ill be the first to admit that the results arent spectacular, but theyr
- over 5,000 registered users
- $203 monthly recurring revenue (MRR)
Below, Im going to share with you how I built it (yes, its [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, Im going to share with you how I built it (yes, its [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. Its an app that allows you to upload a PDF of your CV/resumé, along with the job description youre 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!
## Whats the Tech Stack?
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xpb97bgrx98bwxemrg0o.png)
<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 (Id 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) 🙏
![https://media1.giphy.com/media/ZfK4cXKJTTay1Ava29/giphy.gif?cid=7941fdc6pmqo30ll0e4rzdiisbtagx97sx5t0znx4lk0auju&ep=v1_gifs_search&rid=giphy.gif&ct=g](https://media1.giphy.com/media/ZfK4cXKJTTay1Ava29/giphy.gif?cid=7941fdc6pmqo30ll0e4rzdiisbtagx97sx5t0znx4lk0auju&ep=v1_gifs_search&rid=giphy.gif&ct=g)
<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 its a component-based UI library. This helps me build UIs 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.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/09ca1yaqodkb7b2vnwr9.png)
By the way, If youre 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 youre 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 its open-source makes people a lot more receptive to you talking about it.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q79djej6doj2yq10l2og.png)
<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 youve built and give people the opportunity to learn from it, theyre much more welcoming! As a result, theyre 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 description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/44rlv65u97qhufbhqt0k.png)
<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 its 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 description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/golo3tnh3o0sy5sujrer.png)
<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. Thats when I changed my pricing and thats 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)!
![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](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)
<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)

View File

@ -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:*
![Consent wheel](/cookie-consent/wheel.gif)
<Image src={wheel} alt='Consent wheel' loading='lazy' />
2. *The “Hit Enter When the Red Ball is Over the Accept Button to Consent” Banner:*
![Enter to win](/cookie-consent/enter.gif)
<Image src={enter} alt='Enter to win' loading='lazy' />
Now its time for you to get creative. Btw, if youre looking for some inspiration, check out these [Ridiculous Volume Slider UIs](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.
![Keyboard](/cookie-consent/keyboard.jpg)
<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.png](/cookie-consent/image.png)
<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

View File

@ -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.
![https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axqiv01tl1pha9ougp21.gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axqiv01tl1pha9ougp21.gif)
<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">

View File

@ -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
```
![db studio](/stripe/db-studio.png)
<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

View File

@ -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.

View File

@ -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).

View File

@ -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:

View 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.

View File

@ -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.
![fine-grained cookie control](/cookie-consent/preferences.png)
<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).

View File

@ -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`.
![stripe-npm-versions](/stripe/npm-version.png)
<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`
![listen-events](/stripe/listen-to-stripe-events.png)
<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`
![signing-secret](/stripe/stripe-webhook-signing-secret.png)
<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

View File

@ -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';

View File

@ -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
![find s3](/file-uploads/find-s3.png)
<Image src={findS3} alt="find s3" loading="lazy" />
2. Click on the `Create bucket` button
![create bucket](/file-uploads/create-bucket.png)
<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`
![bucket settings](/file-uploads/default-settings.png)
<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
![new bucket](/file-uploads/new-bucket.png)
<Image src={newBucket} alt="new bucket" loading="lazy" />
2. Click on the `Permissions` tab
![permissions](/file-uploads/permissions.png)
<Image src={permissions} alt="permissions" loading="lazy" />
3. Scroll down to the `Cross-origin resource sharing (CORS)` section and click `Edit`
![cors](/file-uploads/cors.png)
<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`
![security credentials](/file-uploads/username.png)
<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
![access key](/file-uploads/keys.png)
<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...

View File

@ -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`.
![test api keys](/stripe/api-keys.png)
<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.
![test product](/stripe/test-product.png)
<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.
![price ids](/stripe/price-ids.png)
<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`.
![switch plans](/stripe/switch-plans.png)
<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
```
![db studio](/stripe/db-studio.png)
<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.
![test product](/lemon-squeezy/add-product.png)
<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.
![add variant](/lemon-squeezy/add-variant.png)
<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`
![variant id](/lemon-squeezy/variant-id.png)
<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.
![variant ids](/lemon-squeezy/subscription-variant-ids.png)
<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
```
![ngrok](/lemon-squeezy/ngrok.png)
<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:

View File

@ -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!
![open-saas-google-search](/seo/open-saas-google.png)
<Image src={openSaaSGoogle} alt="open-saas-google" loading="lazy" />

View File

@ -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.

View File

@ -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';

View File

@ -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>.

View File

@ -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:

View File

@ -1,3 +1,10 @@
{
"extends": "astro/tsconfigs/strict"
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@assets/*": ["src/assets/*"]
}
}
}

View File

@ -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',
},
},
}),

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View 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,
)
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View 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} />

View 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>

View 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);
};

View File

@ -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() }),
};

View File

@ -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, Im Vince…

View File

@ -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

View File

@ -1,3 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}
"extends": "astro/tsconfigs/strict",
}