mirror of
https://github.com/LukeHagar/Sveltey.git
synced 2025-12-06 04:21:38 +00:00
adjusted name, updated lockfile, updated readme, adjusted styles, adjusted blog, testing glob
This commit is contained in:
411
README.md
411
README.md
@@ -1,9 +1,50 @@
|
||||
|
||||
# 🚀 Sveltey - SvelteKit SaaS Template
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
### 🔐 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
|
||||
- **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
|
||||
- **User Analytics**: Basic user metrics and insights
|
||||
- **Real-time Updates**: Live data updates using Supabase real-time
|
||||
- **Data Visualization**: Charts and graphs for key metrics
|
||||
|
||||
### 📝 Content Management
|
||||
- **Blog System**: Built-in blog with markdown support
|
||||
- **SEO Optimized**: Meta tags, Open Graph, and structured data
|
||||
|
||||
<!-- ### 💳 Payments & Subscriptions
|
||||
### 💳 Payments & Subscriptions - Planned
|
||||
- **Stripe Integration**: Ready-to-use payment processing
|
||||
- **Subscription Plans**: Flexible pricing tiers
|
||||
- **Billing Management**: Customer billing portal
|
||||
- **Webhook Handling**: Secure webhook processing -->
|
||||
- **Webhook Handling**: Secure webhook processing
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
@@ -137,6 +178,362 @@ STRIPE_WEBHOOK_SECRET=your_webhook_secret
|
||||
- Skeleton UI provides the base component library
|
||||
- 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
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "saasy",
|
||||
"name": "sveltey",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "saasy",
|
||||
"name": "sveltey",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import type { BlogPost } from '$lib/blog';
|
||||
import { onMount } from 'svelte';
|
||||
import { Calendar, User, Tag, Share2, ArrowLeft, Star, Twitter, Linkedin } from '@lucide/svelte';
|
||||
|
||||
@@ -21,7 +19,7 @@
|
||||
onMount(async () => {
|
||||
try {
|
||||
// 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;
|
||||
} catch (err) {
|
||||
console.error('Failed to load blog post component:', err);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
const baseClasses = 'btn flex items-center gap-2';
|
||||
|
||||
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`;
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
const baseClasses = 'btn flex items-center justify-start gap-2';
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user