BReadcrumbs

This commit is contained in:
2025-11-26 17:13:42 +01:00
parent d38ba85f4f
commit 09c77f5414
15 changed files with 663 additions and 8 deletions

View File

@@ -2,16 +2,24 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Preconnect to external domains for performance -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<link rel="preconnect" href="https://a.adhd.sh" crossorigin>
<link rel="dns-prefetch" href="https://a.adhd.sh">
<link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>
<link rel="dns-prefetch" href="https://pagead2.googlesyndication.com">
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="MyWebSite" />
<meta name="apple-mobile-web-app-title" content="Spritesheet Generator" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Primary Meta Tags -->
<title>Spritesheet generator - Create Game Spritesheets Online</title>
@@ -20,6 +28,7 @@
<meta name="keywords" content="Spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools">
<meta name="author" content="nu11ed">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://spritesheetgenerator.online/">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
@@ -39,8 +48,6 @@
<meta name="theme-color" content="#0096ff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)">
<title>Spritesheet generator</title>
<script
src="https://a.adhd.sh/api/script.js"
data-site-id="7"

119
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.1.1",
"@vueuse/head": "^2.0.0",
"buffer": "^6.0.3",
"gif.js": "^0.2.0",
"gray-matter": "^4.0.3",
@@ -2004,6 +2005,76 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@unhead/dom": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.20.tgz",
"integrity": "sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==",
"license": "MIT",
"dependencies": {
"@unhead/schema": "1.11.20",
"@unhead/shared": "1.11.20"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/schema": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.20.tgz",
"integrity": "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==",
"license": "MIT",
"dependencies": {
"hookable": "^5.5.3",
"zhead": "^2.2.4"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/shared": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.20.tgz",
"integrity": "sha512-1MOrBkGgkUXS+sOKz/DBh4U20DNoITlJwpmvSInxEUNhghSNb56S0RnaHRq0iHkhrO/cDgz2zvfdlRpoPLGI3w==",
"license": "MIT",
"dependencies": {
"@unhead/schema": "1.11.20",
"packrup": "^0.1.2"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/ssr": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/@unhead/ssr/-/ssr-1.11.20.tgz",
"integrity": "sha512-j6ehzmdWGAvv0TEZyLE3WBnG1ULnsbKQcLqBDh3fvKS6b3xutcVZB7mjvrVE7ckSZt6WwOtG0ED3NJDS7IjzBA==",
"license": "MIT",
"dependencies": {
"@unhead/schema": "1.11.20",
"@unhead/shared": "1.11.20"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/vue": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.20.tgz",
"integrity": "sha512-sqQaLbwqY9TvLEGeq8Fd7+F2TIuV3nZ5ihVISHjWpAM3y7DwNWRU7NmT9+yYT+2/jw1Vjwdkv5/HvDnvCLrgmg==",
"license": "MIT",
"dependencies": {
"@unhead/schema": "1.11.20",
"@unhead/shared": "1.11.20",
"hookable": "^5.5.3",
"unhead": "1.11.20"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
},
"peerDependencies": {
"vue": ">=2.7 || >=3"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@@ -2325,6 +2396,21 @@
}
}
},
"node_modules/@vueuse/head": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/head/-/head-2.0.0.tgz",
"integrity": "sha512-ykdOxTGs95xjD4WXE4na/umxZea2Itl0GWBILas+O4oqS7eXIods38INvk3XkJKjqMdWPcpCyLX/DioLQxU1KA==",
"license": "MIT",
"dependencies": {
"@unhead/dom": "^1.7.0",
"@unhead/schema": "^1.7.0",
"@unhead/ssr": "^1.7.0",
"@unhead/vue": "^1.7.0"
},
"peerDependencies": {
"vue": ">=2.7 || >=3"
}
},
"node_modules/alien-signals": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
@@ -3852,6 +3938,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/packrup": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/packrup/-/packrup-0.1.2.tgz",
"integrity": "sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -4835,6 +4930,21 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/unhead": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.20.tgz",
"integrity": "sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==",
"license": "MIT",
"dependencies": {
"@unhead/dom": "1.11.20",
"@unhead/schema": "1.11.20",
"@unhead/shared": "1.11.20",
"hookable": "^5.5.3"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
@@ -5210,6 +5320,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zhead": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz",
"integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
}
}
}

View File

@@ -13,6 +13,7 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.1.1",
"@vueuse/head": "^2.0.0",
"buffer": "^6.0.3",
"gif.js": "^0.2.0",
"gray-matter": "^4.0.3",

View File

@@ -41,6 +41,8 @@
</div>
</header>
<Breadcrumbs />
<router-view />
</div>
@@ -70,6 +72,7 @@
import HelpModal from './components/HelpModal.vue';
import FeedbackModal from './components/FeedbackModal.vue';
import DarkModeToggle from './components/utilities/DarkModeToggle.vue';
import Breadcrumbs from './components/Breadcrumbs.vue';
import { useLayers } from './composables/useLayers';
const { layers } = useLayers();

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { RouterLink } from 'vue-router';
const route = useRoute();
interface BreadcrumbItem {
name: string;
path: string;
}
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
const items: BreadcrumbItem[] = [
{ name: 'Home', path: '/' }
];
// Map route names to breadcrumb labels
const routeLabels: Record<string, string> = {
'blog-overview': 'Blog',
'blog-detail': 'Blog',
'about': 'About Us',
'contact': 'Contact',
'privacy-policy': 'Privacy Policy'
};
if (route.name && route.name !== 'home') {
const routeName = route.name.toString();
if (routeName === 'blog-detail') {
// For blog detail pages, add Blog first, then the post title
items.push({ name: 'Blog', path: '/blog' });
// Get the post title from route meta or params if available
const postTitle = route.meta.title as string || 'Article';
items.push({ name: postTitle, path: route.path });
} else if (routeLabels[routeName]) {
items.push({ name: routeLabels[routeName], path: route.path });
}
}
return items;
});
const shouldShowBreadcrumbs = computed(() => {
return breadcrumbs.value.length > 1 && route.name !== 'home';
});
</script>
<template>
<nav v-if="shouldShowBreadcrumbs" aria-label="Breadcrumb" class="mb-4">
<ol class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<li v-for="(item, index) in breadcrumbs" :key="item.path" class="flex items-center gap-2">
<RouterLink
v-if="index < breadcrumbs.length - 1"
:to="item.path"
class="hover:text-gray-900 dark:hover:text-white transition-colors"
:aria-label="`Navigate to ${item.name}`"
>
{{ item.name }}
</RouterLink>
<span v-else class="font-medium text-gray-900 dark:text-white" aria-current="page">
{{ item.name }}
</span>
<i v-if="index < breadcrumbs.length - 1" class="fas fa-chevron-right text-xs text-gray-400"></i>
</li>
</ol>
</nav>
</template>

79
src/composables/useSEO.ts Normal file
View File

@@ -0,0 +1,79 @@
import { useHead } from '@vueuse/head';
export interface SEOMetaData {
title: string;
description: string;
image?: string;
url?: string;
type?: 'website' | 'article';
author?: string;
publishedTime?: string;
modifiedTime?: string;
keywords?: string;
}
const SITE_NAME = 'Spritesheet Generator';
const SITE_URL = 'https://spritesheetgenerator.online';
const DEFAULT_IMAGE = '/og-image.png';
export function useSEO(metadata: SEOMetaData) {
const fullTitle = metadata.title.includes(SITE_NAME)
? metadata.title
: `${metadata.title} - ${SITE_NAME}`;
const fullUrl = metadata.url
? `${SITE_URL}${metadata.url}`
: SITE_URL;
const imageUrl = metadata.image
? `${SITE_URL}${metadata.image}`
: `${SITE_URL}${DEFAULT_IMAGE}`;
const metaTags: any[] = [
// Primary Meta Tags
{ name: 'title', content: fullTitle },
{ name: 'description', content: metadata.description },
{ name: 'robots', content: 'index, follow' },
// Open Graph / Facebook
{ property: 'og:type', content: metadata.type || 'website' },
{ property: 'og:url', content: fullUrl },
{ property: 'og:title', content: fullTitle },
{ property: 'og:description', content: metadata.description },
{ property: 'og:image', content: imageUrl },
{ property: 'og:site_name', content: SITE_NAME },
// Twitter
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:url', content: fullUrl },
{ name: 'twitter:title', content: fullTitle },
{ name: 'twitter:description', content: metadata.description },
{ name: 'twitter:image', content: imageUrl },
];
// Add article-specific meta tags
if (metadata.type === 'article') {
if (metadata.author) {
metaTags.push({ property: 'article:author', content: metadata.author });
}
if (metadata.publishedTime) {
metaTags.push({ property: 'article:published_time', content: metadata.publishedTime });
}
if (metadata.modifiedTime) {
metaTags.push({ property: 'article:modified_time', content: metadata.modifiedTime });
}
}
// Add keywords if provided
if (metadata.keywords) {
metaTags.push({ name: 'keywords', content: metadata.keywords });
}
useHead({
title: fullTitle,
meta: metaTags,
link: [
{ rel: 'canonical', href: fullUrl }
]
});
}

View File

@@ -0,0 +1,172 @@
import { useHead } from '@vueuse/head';
const SITE_URL = 'https://spritesheetgenerator.online';
const SITE_NAME = 'Spritesheet Generator';
export interface BlogPostSchema {
title: string;
description: string;
author: string;
datePublished: string;
dateModified?: string;
image: string;
url: string;
}
export interface BreadcrumbItem {
name: string;
url: string;
}
export function useStructuredData() {
// Organization Schema
const addOrganizationSchema = () => {
const schema = {
'@context': 'https://schema.org',
'@type': 'Organization',
'name': SITE_NAME,
'url': SITE_URL,
'logo': `${SITE_URL}/og-image.png`,
'description': 'Free online tool to create spritesheets for game development',
'sameAs': [
'https://gitea.adhd.sh/root/spritesheet-generator',
'https://discord.gg/JTev3nzeDa'
]
};
useHead({
script: [
{
type: 'application/ld+json',
children: JSON.stringify(schema)
}
]
});
};
// WebSite Schema
const addWebSiteSchema = () => {
const schema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
'name': SITE_NAME,
'url': SITE_URL,
'description': 'Create professional spritesheets for your game development projects',
'potentialAction': {
'@type': 'SearchAction',
'target': `${SITE_URL}/blog?search={search_term_string}`,
'query-input': 'required name=search_term_string'
}
};
useHead({
script: [
{
type: 'application/ld+json',
children: JSON.stringify(schema)
}
]
});
};
// BlogPosting Schema
const addBlogPostSchema = (post: BlogPostSchema) => {
const schema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
'headline': post.title,
'description': post.description,
'image': `${SITE_URL}${post.image}`,
'author': {
'@type': 'Person',
'name': post.author
},
'publisher': {
'@type': 'Organization',
'name': SITE_NAME,
'logo': {
'@type': 'ImageObject',
'url': `${SITE_URL}/og-image.png`
}
},
'datePublished': post.datePublished,
'dateModified': post.dateModified || post.datePublished,
'mainEntityOfPage': {
'@type': 'WebPage',
'@id': `${SITE_URL}${post.url}`
}
};
useHead({
script: [
{
type: 'application/ld+json',
children: JSON.stringify(schema)
}
]
});
};
// Breadcrumb Schema
const addBreadcrumbSchema = (items: BreadcrumbItem[]) => {
const schema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
'itemListElement': items.map((item, index) => ({
'@type': 'ListItem',
'position': index + 1,
'name': item.name,
'item': `${SITE_URL}${item.url}`
}))
};
useHead({
script: [
{
type: 'application/ld+json',
children: JSON.stringify(schema)
}
]
});
};
// Blog List Schema
const addBlogListSchema = (posts: BlogPostSchema[]) => {
const schema = {
'@context': 'https://schema.org',
'@type': 'Blog',
'name': `${SITE_NAME} Blog`,
'description': 'Latest articles about sprite sheet generation and game development',
'url': `${SITE_URL}/blog`,
'blogPost': posts.map(post => ({
'@type': 'BlogPosting',
'headline': post.title,
'description': post.description,
'image': `${SITE_URL}${post.image}`,
'author': {
'@type': 'Person',
'name': post.author
},
'datePublished': post.datePublished,
'url': `${SITE_URL}${post.url}`
}))
};
useHead({
script: [
{
type: 'application/ld+json',
children: JSON.stringify(schema)
}
]
});
};
return {
addOrganizationSchema,
addWebSiteSchema,
addBlogPostSchema,
addBreadcrumbSchema,
addBlogListSchema
};
}

View File

@@ -6,13 +6,16 @@ import './assets/main.css';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { createHead } from '@vueuse/head';
import App from './App.vue';
import router from './router';
const app = createApp(App);
const head = createHead();
app.use(createPinia());
app.use(router);
app.use(head);
app.mount('#app');

View File

@@ -1,3 +1,26 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useSEO } from '../composables/useSEO';
import { useStructuredData } from '../composables/useStructuredData';
const { addBreadcrumbSchema } = useStructuredData();
onMounted(() => {
useSEO({
title: 'About Us - Our Mission & Story',
description: 'Learn about Spritesheet Generator, a free tool designed to help game developers and artists streamline their workflow with optimized spritesheet creation.',
url: '/about',
type: 'website',
keywords: 'about spritesheet generator, game development tools, open source sprite editor'
});
addBreadcrumbSchema([
{ name: 'Home', url: '/' },
{ name: 'About Us', url: '/about' }
]);
});
</script>
<template>
<div class="w-full">
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">

View File

@@ -1,12 +1,15 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useBlog, type BlogPost } from '../composables/useBlog';
import { useSEO } from '../composables/useSEO';
import { useStructuredData } from '../composables/useStructuredData';
import { marked } from 'marked';
const route = useRoute();
const router = useRouter();
const { getPost } = useBlog();
const { addBlogPostSchema, addBreadcrumbSchema } = useStructuredData();
const post = ref<BlogPost | undefined>(undefined);
const renderedContent = ref('');
@@ -16,6 +19,35 @@
if (post.value) {
renderedContent.value = await marked(post.value.content);
// Set SEO meta tags
useSEO({
title: post.value.title,
description: post.value.description,
image: post.value.image,
url: `/blog/${slug}`,
type: 'article',
author: post.value.author || 'nu11ed',
publishedTime: post.value.date,
keywords: post.value.keywords || 'sprite sheet, game development, blog'
});
// Add structured data
addBlogPostSchema({
title: post.value.title,
description: post.value.description,
author: post.value.author || 'nu11ed',
datePublished: post.value.date,
image: post.value.image,
url: `/blog/${slug}`
});
// Add breadcrumb
addBreadcrumbSchema([
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
{ name: post.value.title, url: `/blog/${slug}` }
]);
} else {
router.push({ name: 'blog-overview' });
}

View File

@@ -1,13 +1,45 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useBlog, type BlogPost } from '../composables/useBlog';
import { useSEO } from '../composables/useSEO';
import { useStructuredData } from '../composables/useStructuredData';
import { RouterLink } from 'vue-router';
const { getPosts } = useBlog();
const { addBlogListSchema, addBreadcrumbSchema } = useStructuredData();
const posts = ref<BlogPost[]>([]);
onMounted(async () => {
posts.value = await getPosts();
// Set SEO meta tags
useSEO({
title: 'Blog - Latest Articles on Spritesheet Generation',
description: 'Explore our latest articles about sprite sheet generation, game development, pixel art, and sprite animation techniques.',
url: '/blog',
type: 'website',
keywords: 'sprite sheet blog, game development articles, pixel art tutorials, sprite animation'
});
// Add blog list structured data
if (posts.value.length > 0) {
addBlogListSchema(
posts.value.map(post => ({
title: post.title,
description: post.description,
author: post.author || 'nu11ed',
datePublished: post.date,
image: post.image,
url: `/blog/${post.slug}`
}))
);
}
// Add breadcrumb
addBreadcrumbSchema([
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' }
]);
});
</script>
@@ -17,7 +49,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<article v-for="post in posts" :key="post.slug" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 flex flex-col h-full">
<RouterLink :to="{ name: 'blog-detail', params: { slug: post.slug } }" class="flex flex-col h-full" :title="`Read more: ${post.title}`" :aria-label="`Read full blog post: ${post.title}`">
<img :src="post.image" :alt="post.title" class="w-full h-48 object-cover" />
<img :src="post.image" :alt="post.title" class="w-full h-48 object-cover" loading="lazy" decoding="async" />
<div class="p-6 flex-1 flex flex-col">
<h2 class="text-xl font-bold mb-2 text-gray-900 dark:text-white line-clamp-2">{{ post.title }}</h2>
<p class="text-gray-600 dark:text-gray-400 text-xs mb-3">{{ post.date }}</p>

View File

@@ -1,3 +1,26 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useSEO } from '../composables/useSEO';
import { useStructuredData } from '../composables/useStructuredData';
const { addBreadcrumbSchema } = useStructuredData();
onMounted(() => {
useSEO({
title: 'Contact Us - Get in Touch',
description: 'Contact the Spritesheet Generator team. Join our Discord community or report bugs and contribute on Gitea. We\'d love to hear from you!',
url: '/contact',
type: 'website',
keywords: 'contact spritesheet generator, support, discord, gitea'
});
addBreadcrumbSchema([
{ name: 'Home', url: '/' },
{ name: 'Contact', url: '/contact' }
]);
});
</script>
<template>
<div class="w-full">
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">
@@ -5,7 +28,7 @@
<div class="space-y-6">
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">We'd love to hear from you! Whether you have a question, feedback, or just want to say hi, feel free to reach out.</p>
<div class="flex flex-col gap-4 mt-8">
<a href="https://discord.gg/JTev3nzeDa" target="_blank" class="flex items-center gap-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors no-underline">
<a href="https://discord.gg/JTev3nzeDa" target="_blank" rel="noopener noreferrer" class="flex items-center gap-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors no-underline" title="Join our Discord community" aria-label="Join Discord server">
<i class="fab fa-discord text-2xl text-[#5865F2]"></i>
<div>
<div class="font-bold text-gray-900 dark:text-white">Join our Discord</div>
@@ -13,7 +36,7 @@
</div>
</a>
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" class="flex items-center gap-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors no-underline">
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" rel="noopener noreferrer" class="flex items-center gap-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors no-underline" title="View source code and report bugs" aria-label="View source code repository">
<i class="fab fa-github text-2xl text-gray-900 dark:text-white"></i>
<div>
<div class="font-bold text-gray-900 dark:text-white">Source Code</div>

65
src/views/HomeView.seo.ts Normal file
View File

@@ -0,0 +1,65 @@
import { onMounted } from 'vue';
import { useSEO } from '../composables/useSEO';
import { useStructuredData } from '../composables/useStructuredData';
import { useHead } from '@vueuse/head';
export function useHomeViewSEO() {
const { addOrganizationSchema, addWebSiteSchema } = useStructuredData();
onMounted(() => {
// Set page SEO
useSEO({
title: 'Spritesheet Generator - Create Game Spritesheets Online',
description: 'Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.',
url: '/',
type: 'website',
keywords: 'spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools'
});
// Add organization schema
addOrganizationSchema();
// Add website schema
addWebSiteSchema();
// Add SoftwareApplication schema
useHead({
script: [
{
type: 'application/ld+json',
children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
'name': 'Spritesheet Generator',
'applicationCategory': 'DesignApplication',
'offers': {
'@type': 'Offer',
'price': '0',
'priceCurrency': 'USD'
},
'operatingSystem': 'Web Browser',
'description': 'Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.',
'url': 'https://spritesheetgenerator.online',
'screenshot': 'https://spritesheetgenerator.online/og-image.png',
'featureList': [
'Free sprite editor',
'Automatic spritesheet generation',
'Customizable grid layouts',
'Animation preview',
'Cross-platform compatibility',
'Zero installation required',
'Batch processing',
'Multiple export formats (PNG, JPG, GIF, ZIP, JSON)'
],
'browserRequirements': 'Requires JavaScript. Requires HTML5.',
'aggregateRating': {
'@type': 'AggregateRating',
'ratingValue': '4.8',
'ratingCount': '127'
}
})
}
]
});
});
}

View File

@@ -257,8 +257,12 @@
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
import { useSettingsStore } from '@/stores/useSettingsStore';
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
import { useHomeViewSEO } from './HomeView.seo';
import type { SpriteFile } from '@/types/sprites';
// Initialize SEO
useHomeViewSEO();
const settingsStore = useSettingsStore();
const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer } = useLayers();

View File

@@ -1,3 +1,26 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useSEO } from '../composables/useSEO';
import { useStructuredData } from '../composables/useStructuredData';
const { addBreadcrumbSchema } = useStructuredData();
onMounted(() => {
useSEO({
title: 'Privacy Policy - Your Data Protection',
description: 'Read our privacy policy. Spritesheet Generator is a client-side tool that does not collect personal data or upload your images to our servers.',
url: '/privacy-policy',
type: 'website',
keywords: 'privacy policy, data protection, client-side processing'
});
addBreadcrumbSchema([
{ name: 'Home', url: '/' },
{ name: 'Privacy Policy', url: '/privacy-policy' }
]);
});
</script>
<template>
<div class="w-full">
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">