adjusted name, updated lockfile, updated readme, adjusted styles, adjusted blog, testing glob

This commit is contained in:
Luke Hagar
2025-05-30 08:56:12 -05:00
parent 65125a7b92
commit 5bb69300f5
4 changed files with 409 additions and 14 deletions

411
README.md
View File

@@ -1,9 +1,50 @@
# 🚀 Sveltey - SvelteKit SaaS Template # 🚀 Sveltey - SvelteKit SaaS Template
![Sveltey SaaS Template](https://github.com/user-attachments/assets/5dc91129-ebc7-4458-98b6-1862c6de4428) ![Sveltey SaaS Template](https://github.com/user-attachments/assets/5dc91129-ebc7-4458-98b6-1862c6de4428)
A modern, production-ready SaaS template built with SvelteKit 2, Svelte 5, Supabase, and Skeleton UI. Get your SaaS project up and running in hours, not months. A modern, production-ready SaaS template built with SvelteKit 2, Svelte 5, Supabase, and Skeleton UI. Get your SaaS project up and running in hours, not months.
- [🚀 Sveltey - SvelteKit SaaS Template](#-sveltey---sveltekit-saas-template)
- [✨ Features](#-features)
- [🔐 Authentication \& User Management](#-authentication--user-management)
- [🎨 Modern UI/UX](#-modern-uiux)
- [📝 Content Management](#-content-management)
- [📊 Dashboard \& Analytics - Planned](#-dashboard--analytics---planned)
- [💳 Payments \& Subscriptions - Planned](#-payments--subscriptions---planned)
- [🚀 Quick Start](#-quick-start)
- [📦 Tech Stack](#-tech-stack)
- [📁 Project Structure](#-project-structure)
- [🔧 Configuration](#-configuration)
- [Environment Variables](#environment-variables)
- [Supabase Setup](#supabase-setup)
- [Stripe Setup](#stripe-setup)
- [🎯 Customization](#-customization)
- [Branding](#branding)
- [Components](#components)
- [🔍 SEO \& Meta Tags](#-seo--meta-tags)
- [Understanding the Meta Tag System](#understanding-the-meta-tag-system)
- [Base Meta Tags Structure](#base-meta-tags-structure)
- [Customizing Page Meta Tags](#customizing-page-meta-tags)
- [Dynamic Meta Tags](#dynamic-meta-tags)
- [Robot Control](#robot-control)
- [🖼️ OpenGraph Images](#-opengraph-images)
- [Setting Up OpenGraph Images](#setting-up-opengraph-images)
- [1. Default Site Image](#1-default-site-image)
- [2. Page-Specific Images](#2-page-specific-images)
- [3. Dynamic Images for Blog Posts](#3-dynamic-images-for-blog-posts)
- [OpenGraph Image Best Practices](#opengraph-image-best-practices)
- [Image Specifications](#image-specifications)
- [Design Guidelines](#design-guidelines)
- [Dynamic Image Generation](#dynamic-image-generation)
- [Testing OpenGraph Images](#testing-opengraph-images)
- [Common OpenGraph Properties](#common-opengraph-properties)
- [🤝 Contributing](#-contributing)
- [📄 License](#-license)
- [🆘 Support](#-support)
- [🙏 Acknowledgments](#-acknowledgments)
## ✨ Features ## ✨ Features
### 🔐 Authentication & User Management ### 🔐 Authentication & User Management
@@ -20,21 +61,21 @@ A modern, production-ready SaaS template built with SvelteKit 2, Svelte 5, Supab
- **Loading States**: Elegant loading indicators and transitions - **Loading States**: Elegant loading indicators and transitions
- **Toast Notifications**: User-friendly feedback system - **Toast Notifications**: User-friendly feedback system
### 📊 Dashboard & Analytics ### 📝 Content Management
- **Blog System**: Built-in blog with markdown support
- **SEO Optimized**: Meta tags, Open Graph, and structured data
### 📊 Dashboard & Analytics - Planned
- **Admin Dashboard**: Clean, intuitive admin interface - **Admin Dashboard**: Clean, intuitive admin interface
- **User Analytics**: Basic user metrics and insights - **User Analytics**: Basic user metrics and insights
- **Real-time Updates**: Live data updates using Supabase real-time - **Real-time Updates**: Live data updates using Supabase real-time
- **Data Visualization**: Charts and graphs for key metrics - **Data Visualization**: Charts and graphs for key metrics
### 📝 Content Management ### 💳 Payments & Subscriptions - Planned
- **Blog System**: Built-in blog with markdown support
- **SEO Optimized**: Meta tags, Open Graph, and structured data
<!-- ### 💳 Payments & Subscriptions
- **Stripe Integration**: Ready-to-use payment processing - **Stripe Integration**: Ready-to-use payment processing
- **Subscription Plans**: Flexible pricing tiers - **Subscription Plans**: Flexible pricing tiers
- **Billing Management**: Customer billing portal - **Billing Management**: Customer billing portal
- **Webhook Handling**: Secure webhook processing --> - **Webhook Handling**: Secure webhook processing
## 🚀 Quick Start ## 🚀 Quick Start
@@ -137,6 +178,362 @@ STRIPE_WEBHOOK_SECRET=your_webhook_secret
- Skeleton UI provides the base component library - Skeleton UI provides the base component library
- Easy to theme and customize with CSS variables - Easy to theme and customize with CSS variables
## 🔍 SEO & Meta Tags
Sveltey comes with a comprehensive SEO system built on top of `svelte-meta-tags` that provides automatic meta tag management, OpenGraph support, and Twitter Card integration.
### Understanding the Meta Tag System
The meta tag system in Sveltey uses a two-level approach:
1. **Base Meta Tags** (`src/routes/+layout.ts`) - Global defaults for your entire site
2. **Page Meta Tags** (`src/routes/*/+page.ts`) - Page-specific overrides and additions
#### Base Meta Tags Structure
The base meta tags are defined in `src/routes/+layout.ts` and include:
```typescript
const baseMetaTags = Object.freeze({
title: 'Sveltey - SvelteKit SaaS Template',
titleTemplate: '%s | Sveltey',
description: 'Your default site description...',
canonical: new URL(url.pathname, url.origin).href,
robots: 'index,follow',
keywords: ['SvelteKit', 'SaaS', 'template'],
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
title: 'Sveltey - SvelteKit SaaS Template',
description: 'Your OpenGraph description...',
siteName: 'Sveltey',
locale: 'en_US',
images: [
{
url: `${url.origin}/og-image.jpg`,
width: 1200,
height: 630,
alt: 'Sveltey - SvelteKit SaaS Template',
type: 'image/jpeg'
}
]
},
twitter: {
cardType: 'summary_large_image',
site: '@sveltey_dev',
creator: '@sveltey_dev',
title: 'Sveltey - SvelteKit SaaS Template',
description: 'Your Twitter description...',
image: `${url.origin}/og-image.jpg`,
imageAlt: 'Sveltey - SvelteKit SaaS Template'
}
}) satisfies MetaTagsProps;
```
### Customizing Page Meta Tags
Each page can override and extend the base meta tags by exporting a `load` function in its `+page.ts` file:
```typescript
// src/routes/your-page/+page.ts
import type { MetaTagsProps } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
title: 'Your Page Title',
description: 'Specific description for this page',
keywords: ['additional', 'keywords', 'for', 'this', 'page'],
openGraph: {
title: 'Your Page Title - Brand Name',
description: 'OpenGraph description for social sharing',
type: 'article', // or 'website', 'product', etc.
images: [
{
url: 'https://your-domain.com/specific-og-image.jpg',
width: 1200,
height: 630,
alt: 'Description of your image'
}
]
},
twitter: {
title: 'Twitter-specific title',
description: 'Twitter-specific description'
},
// Additional meta tags
additionalMetaTags: [
{
name: 'author',
content: 'Your Name'
},
{
property: 'article:published_time',
content: '2024-01-01T00:00:00Z'
}
]
}) satisfies MetaTagsProps;
return {
pageMetaTags
};
};
```
### Dynamic Meta Tags
For dynamic pages (like blog posts), you can generate meta tags based on content:
```typescript
// src/routes/blog/[slug]/+page.server.ts
export const load = async ({ params, url }) => {
const post = await getPostBySlug(params.slug);
const pageMetaTags = Object.freeze({
title: post.title,
description: post.excerpt,
canonical: new URL(`/blog/${params.slug}`, url.origin).href,
openGraph: {
type: 'article',
title: post.title,
description: post.excerpt,
url: new URL(`/blog/${params.slug}`, url.origin).href,
images: post.featuredImage ? [
{
url: post.featuredImage,
width: 1200,
height: 630,
alt: post.title
}
] : undefined,
article: {
publishedTime: post.publishedAt,
authors: [post.author],
section: 'Technology',
tags: post.tags
}
}
}) satisfies MetaTagsProps;
return { post, pageMetaTags };
};
```
### Robot Control
Control search engine indexing per page:
```typescript
const pageMetaTags = {
robots: 'noindex,nofollow', // Don't index this page
// or
robots: 'index,follow', // Index this page (default)
// or
robots: 'index,nofollow' // Index but don't follow links
};
```
## 🖼️ OpenGraph Images
OpenGraph images are crucial for social media sharing and SEO. Sveltey provides a flexible system for managing these images.
### Setting Up OpenGraph Images
#### 1. Default Site Image
Place your default OpenGraph image in the `static` folder:
```
static/
├── og-image.jpg # Default 1200x630 image
├── og-image-square.jpg # Optional square variant
└── favicon.png
```
The default image is automatically referenced in your base meta tags:
```typescript
// src/routes/+layout.ts
openGraph: {
images: [
{
url: `${url.origin}/og-image.jpg`,
width: 1200,
height: 630,
alt: 'Sveltey - SvelteKit SaaS Template',
type: 'image/jpeg'
}
]
}
```
#### 2. Page-Specific Images
Override the default image for specific pages:
```typescript
// src/routes/pricing/+page.ts
const pageMetaTags = {
openGraph: {
images: [
{
url: `${url.origin}/og-pricing.jpg`,
width: 1200,
height: 630,
alt: 'Sveltey Pricing Plans',
type: 'image/jpeg'
}
]
}
};
```
#### 3. Dynamic Images for Blog Posts
For blog posts or dynamic content, you can generate or specify images dynamically:
```typescript
// src/routes/blog/[slug]/+page.server.ts
const pageMetaTags = {
openGraph: {
images: post.featuredImage ? [
{
url: post.featuredImage,
width: 1200,
height: 630,
alt: post.title,
type: 'image/jpeg'
}
] : [
{
url: `${url.origin}/og-blog-default.jpg`,
width: 1200,
height: 630,
alt: 'Sveltey Blog',
type: 'image/jpeg'
}
]
}
};
```
### OpenGraph Image Best Practices
#### Image Specifications
- **Recommended Size**: 1200x630 pixels (1.91:1 aspect ratio)
- **Minimum Size**: 600x315 pixels
- **Maximum Size**: 8MB
- **Format**: JPG or PNG (JPG preferred for smaller file size)
#### Design Guidelines
```typescript
// Example with multiple image variants
openGraph: {
images: [
{
url: `${url.origin}/og-image-large.jpg`,
width: 1200,
height: 630,
alt: 'Large image for Facebook, LinkedIn',
type: 'image/jpeg'
},
{
url: `${url.origin}/og-image-square.jpg`,
width: 1080,
height: 1080,
alt: 'Square image for Instagram, Twitter',
type: 'image/jpeg'
}
]
}
```
#### Dynamic Image Generation
For advanced use cases, you can generate images dynamically:
```typescript
// src/routes/api/og/[slug]/+server.ts
export async function GET({ params, url }) {
const post = await getPostBySlug(params.slug);
// Generate image using libraries like @vercel/og or canvas
const image = await generateOGImage({
title: post.title,
author: post.author,
template: 'blog-post'
});
return new Response(image, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
}
```
Then reference it in your meta tags:
```typescript
openGraph: {
images: [
{
url: `${url.origin}/api/og/${params.slug}`,
width: 1200,
height: 630,
alt: post.title,
type: 'image/png'
}
]
}
```
### Testing OpenGraph Images
Use these tools to test your OpenGraph implementation:
- **Facebook Debugger**: https://developers.facebook.com/tools/debug/
- **Twitter Card Validator**: https://cards-dev.twitter.com/validator
- **LinkedIn Post Inspector**: https://www.linkedin.com/post-inspector/
### Common OpenGraph Properties
```typescript
openGraph: {
type: 'website', // website, article, product, etc.
title: 'Page Title', // Specific title for social sharing
description: 'Description', // Social media description
url: 'https://example.com', // Canonical URL
siteName: 'Site Name', // Your site/brand name
locale: 'en_US', // Language and region
// For articles
article: {
publishedTime: '2024-01-01T00:00:00Z',
modifiedTime: '2024-01-02T00:00:00Z',
authors: ['Author Name'],
section: 'Technology',
tags: ['svelte', 'sveltekit']
},
// For products
product: {
price: {
amount: '29.99',
currency: 'USD'
}
}
}
```
## 🤝 Contributing ## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. Contributions are welcome! Please feel free to submit a Pull Request.

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "saasy", "name": "sveltey",
"version": "0.0.1", "version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "saasy", "name": "sveltey",
"version": "0.0.1", "version": "0.0.1",
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.5", "@eslint/compat": "^1.2.5",

View File

@@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types';
import type { BlogPost } from '$lib/blog';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { Calendar, User, Tag, Share2, ArrowLeft, Star, Twitter, Linkedin } from '@lucide/svelte'; import { Calendar, User, Tag, Share2, ArrowLeft, Star, Twitter, Linkedin } from '@lucide/svelte';
@@ -21,7 +19,7 @@
onMount(async () => { onMount(async () => {
try { try {
// Dynamically import the markdown component // Dynamically import the markdown component
const module = await import(`../../../lib/posts/${data.slug}.md`); const module = import.meta.glob(`$lib/posts/${data.slug}.md`);
component = module.default; component = module.default;
} catch (err) { } catch (err) {
console.error('Failed to load blog post component:', err); console.error('Failed to load blog post component:', err);

View File

@@ -30,7 +30,7 @@
const baseClasses = 'btn flex items-center gap-2'; const baseClasses = 'btn flex items-center gap-2';
if (isActive) { if (isActive) {
return `${baseClasses} preset-filled-primary-500 cursor-default`; return `${baseClasses} preset-filled-primary-500 text-on-primary-500 cursor-default`;
} }
return `${baseClasses} preset-ghost-surface-200-800`; return `${baseClasses} preset-ghost-surface-200-800`;
} }
@@ -41,7 +41,7 @@
const baseClasses = 'btn flex items-center justify-start gap-2'; const baseClasses = 'btn flex items-center justify-start gap-2';
if (isActive) { if (isActive) {
return `${baseClasses} preset-filled-primary-500 cursor-default`; return `${baseClasses} preset-filled-primary-500 text-on-primary-500 cursor-default`;
} }
return `${baseClasses} preset-ghost-surface-200-800`; return `${baseClasses} preset-ghost-surface-200-800`;
} }