[FEAT] SEO best practices
This commit is contained in:
@@ -11,17 +11,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
|
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
|
||||||
const items: BreadcrumbItem[] = [
|
const items: BreadcrumbItem[] = [{ name: 'Home', path: '/' }];
|
||||||
{ name: 'Home', path: '/' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Map route names to breadcrumb labels
|
// Map route names to breadcrumb labels
|
||||||
const routeLabels: Record<string, string> = {
|
const routeLabels: Record<string, string> = {
|
||||||
'blog-overview': 'Blog',
|
'blog-overview': 'Blog',
|
||||||
'blog-detail': 'Blog',
|
'blog-detail': 'Blog',
|
||||||
'about': 'About Us',
|
about: 'About Us',
|
||||||
'contact': 'Contact',
|
contact: 'Contact',
|
||||||
'privacy-policy': 'Privacy Policy'
|
'privacy-policy': 'Privacy Policy',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (route.name && route.name !== 'home') {
|
if (route.name && route.name !== 'home') {
|
||||||
@@ -32,7 +30,7 @@
|
|||||||
items.push({ name: 'Blog', path: '/blog' });
|
items.push({ name: 'Blog', path: '/blog' });
|
||||||
|
|
||||||
// Get the post title from route meta or params if available
|
// Get the post title from route meta or params if available
|
||||||
const postTitle = route.meta.title as string || 'Article';
|
const postTitle = (route.meta.title as string) || 'Article';
|
||||||
items.push({ name: postTitle, path: route.path });
|
items.push({ name: postTitle, path: route.path });
|
||||||
} else if (routeLabels[routeName]) {
|
} else if (routeLabels[routeName]) {
|
||||||
items.push({ name: routeLabels[routeName], path: route.path });
|
items.push({ name: routeLabels[routeName], path: route.path });
|
||||||
@@ -51,12 +49,7 @@
|
|||||||
<nav v-if="shouldShowBreadcrumbs" aria-label="Breadcrumb" class="mb-4">
|
<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">
|
<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">
|
<li v-for="(item, index) in breadcrumbs" :key="item.path" class="flex items-center gap-2">
|
||||||
<RouterLink
|
<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}`">
|
||||||
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 }}
|
{{ item.name }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<span v-else class="font-medium text-gray-900 dark:text-white" aria-current="page">
|
<span v-else class="font-medium text-gray-900 dark:text-white" aria-current="page">
|
||||||
|
|||||||
@@ -17,17 +17,11 @@ const SITE_URL = 'https://spritesheetgenerator.online';
|
|||||||
const DEFAULT_IMAGE = '/og-image.png';
|
const DEFAULT_IMAGE = '/og-image.png';
|
||||||
|
|
||||||
export function useSEO(metadata: SEOMetaData) {
|
export function useSEO(metadata: SEOMetaData) {
|
||||||
const fullTitle = metadata.title.includes(SITE_NAME)
|
const fullTitle = metadata.title.includes(SITE_NAME) ? metadata.title : `${metadata.title} - ${SITE_NAME}`;
|
||||||
? metadata.title
|
|
||||||
: `${metadata.title} - ${SITE_NAME}`;
|
|
||||||
|
|
||||||
const fullUrl = metadata.url
|
const fullUrl = metadata.url ? `${SITE_URL}${metadata.url}` : SITE_URL;
|
||||||
? `${SITE_URL}${metadata.url}`
|
|
||||||
: SITE_URL;
|
|
||||||
|
|
||||||
const imageUrl = metadata.image
|
const imageUrl = metadata.image ? `${SITE_URL}${metadata.image}` : `${SITE_URL}${DEFAULT_IMAGE}`;
|
||||||
? `${SITE_URL}${metadata.image}`
|
|
||||||
: `${SITE_URL}${DEFAULT_IMAGE}`;
|
|
||||||
|
|
||||||
const metaTags: any[] = [
|
const metaTags: any[] = [
|
||||||
// Primary Meta Tags
|
// Primary Meta Tags
|
||||||
@@ -72,8 +66,6 @@ export function useSEO(metadata: SEOMetaData) {
|
|||||||
useHead({
|
useHead({
|
||||||
title: fullTitle,
|
title: fullTitle,
|
||||||
meta: metaTags,
|
meta: metaTags,
|
||||||
link: [
|
link: [{ rel: 'canonical', href: fullUrl }],
|
||||||
{ rel: 'canonical', href: fullUrl }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,23 +24,20 @@ export function useStructuredData() {
|
|||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'Organization',
|
'@type': 'Organization',
|
||||||
'name': SITE_NAME,
|
name: SITE_NAME,
|
||||||
'url': SITE_URL,
|
url: SITE_URL,
|
||||||
'logo': `${SITE_URL}/og-image.png`,
|
logo: `${SITE_URL}/og-image.png`,
|
||||||
'description': 'Free online tool to create spritesheets for game development',
|
description: 'Free online tool to create spritesheets for game development',
|
||||||
'sameAs': [
|
sameAs: ['https://gitea.adhd.sh/root/spritesheet-generator', 'https://discord.gg/JTev3nzeDa'],
|
||||||
'https://gitea.adhd.sh/root/spritesheet-generator',
|
|
||||||
'https://discord.gg/JTev3nzeDa'
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
children: JSON.stringify(schema)
|
children: JSON.stringify(schema),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,23 +46,23 @@ export function useStructuredData() {
|
|||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'WebSite',
|
'@type': 'WebSite',
|
||||||
'name': SITE_NAME,
|
name: SITE_NAME,
|
||||||
'url': SITE_URL,
|
url: SITE_URL,
|
||||||
'description': 'Create professional spritesheets for your game development projects',
|
description: 'Create professional spritesheets for your game development projects',
|
||||||
'potentialAction': {
|
potentialAction: {
|
||||||
'@type': 'SearchAction',
|
'@type': 'SearchAction',
|
||||||
'target': `${SITE_URL}/blog?search={search_term_string}`,
|
target: `${SITE_URL}/blog?search={search_term_string}`,
|
||||||
'query-input': 'required name=search_term_string'
|
'query-input': 'required name=search_term_string',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
children: JSON.stringify(schema)
|
children: JSON.stringify(schema),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,36 +71,36 @@ export function useStructuredData() {
|
|||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'BlogPosting',
|
'@type': 'BlogPosting',
|
||||||
'headline': post.title,
|
headline: post.title,
|
||||||
'description': post.description,
|
description: post.description,
|
||||||
'image': `${SITE_URL}${post.image}`,
|
image: `${SITE_URL}${post.image}`,
|
||||||
'author': {
|
author: {
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
'name': post.author
|
name: post.author,
|
||||||
},
|
},
|
||||||
'publisher': {
|
publisher: {
|
||||||
'@type': 'Organization',
|
'@type': 'Organization',
|
||||||
'name': SITE_NAME,
|
name: SITE_NAME,
|
||||||
'logo': {
|
logo: {
|
||||||
'@type': 'ImageObject',
|
'@type': 'ImageObject',
|
||||||
'url': `${SITE_URL}/og-image.png`
|
url: `${SITE_URL}/og-image.png`,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
'datePublished': post.datePublished,
|
datePublished: post.datePublished,
|
||||||
'dateModified': post.dateModified || post.datePublished,
|
dateModified: post.dateModified || post.datePublished,
|
||||||
'mainEntityOfPage': {
|
mainEntityOfPage: {
|
||||||
'@type': 'WebPage',
|
'@type': 'WebPage',
|
||||||
'@id': `${SITE_URL}${post.url}`
|
'@id': `${SITE_URL}${post.url}`,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
children: JSON.stringify(schema)
|
children: JSON.stringify(schema),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,21 +109,21 @@ export function useStructuredData() {
|
|||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'BreadcrumbList',
|
'@type': 'BreadcrumbList',
|
||||||
'itemListElement': items.map((item, index) => ({
|
itemListElement: items.map((item, index) => ({
|
||||||
'@type': 'ListItem',
|
'@type': 'ListItem',
|
||||||
'position': index + 1,
|
position: index + 1,
|
||||||
'name': item.name,
|
name: item.name,
|
||||||
'item': `${SITE_URL}${item.url}`
|
item: `${SITE_URL}${item.url}`,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
children: JSON.stringify(schema)
|
children: JSON.stringify(schema),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,30 +132,30 @@ export function useStructuredData() {
|
|||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'Blog',
|
'@type': 'Blog',
|
||||||
'name': `${SITE_NAME} Blog`,
|
name: `${SITE_NAME} Blog`,
|
||||||
'description': 'Latest articles about sprite sheet generation and game development',
|
description: 'Latest articles about sprite sheet generation and game development',
|
||||||
'url': `${SITE_URL}/blog`,
|
url: `${SITE_URL}/blog`,
|
||||||
'blogPost': posts.map(post => ({
|
blogPost: posts.map(post => ({
|
||||||
'@type': 'BlogPosting',
|
'@type': 'BlogPosting',
|
||||||
'headline': post.title,
|
headline: post.title,
|
||||||
'description': post.description,
|
description: post.description,
|
||||||
'image': `${SITE_URL}${post.image}`,
|
image: `${SITE_URL}${post.image}`,
|
||||||
'author': {
|
author: {
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
'name': post.author
|
name: post.author,
|
||||||
},
|
},
|
||||||
'datePublished': post.datePublished,
|
datePublished: post.datePublished,
|
||||||
'url': `${SITE_URL}${post.url}`
|
url: `${SITE_URL}${post.url}`,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
children: JSON.stringify(schema)
|
children: JSON.stringify(schema),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,6 +164,6 @@ export function useStructuredData() {
|
|||||||
addWebSiteSchema,
|
addWebSiteSchema,
|
||||||
addBlogPostSchema,
|
addBlogPostSchema,
|
||||||
addBreadcrumbSchema,
|
addBreadcrumbSchema,
|
||||||
addBlogListSchema
|
addBlogListSchema,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
description: 'Learn about Spritesheet Generator, a free tool designed to help game developers and artists streamline their workflow with optimized spritesheet creation.',
|
description: 'Learn about Spritesheet Generator, a free tool designed to help game developers and artists streamline their workflow with optimized spritesheet creation.',
|
||||||
url: '/about',
|
url: '/about',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
keywords: 'about spritesheet generator, game development tools, open source sprite editor'
|
keywords: 'about spritesheet generator, game development tools, open source sprite editor',
|
||||||
});
|
});
|
||||||
|
|
||||||
addBreadcrumbSchema([
|
addBreadcrumbSchema([
|
||||||
{ name: 'Home', url: '/' },
|
{ name: 'Home', url: '/' },
|
||||||
{ name: 'About Us', url: '/about' }
|
{ name: 'About Us', url: '/about' },
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,123 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, computed } from 'vue';
|
import { ref, onMounted, computed, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useBlog, type BlogPost } from '../composables/useBlog';
|
import { useBlog, type BlogPost } from '../composables/useBlog';
|
||||||
import { useSEO } from '../composables/useSEO';
|
import { useHead } from '@vueuse/head';
|
||||||
import { useStructuredData } from '../composables/useStructuredData';
|
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { getPost } = useBlog();
|
const { getPost } = useBlog();
|
||||||
const { addBlogPostSchema, addBreadcrumbSchema } = useStructuredData();
|
|
||||||
const post = ref<BlogPost | undefined>(undefined);
|
const post = ref<BlogPost | undefined>(undefined);
|
||||||
const renderedContent = ref('');
|
const renderedContent = ref('');
|
||||||
|
|
||||||
const slug = computed(() => route.params.slug as string);
|
const slug = computed(() => route.params.slug as string);
|
||||||
|
|
||||||
// Initialize with default SEO (will be updated when post loads)
|
// Reactive SEO data that updates when post loads
|
||||||
useSEO({
|
const pageTitle = computed(() => (post.value ? `${post.value.title} - Spritesheet Generator` : 'Blog Post - Spritesheet Generator'));
|
||||||
title: 'Blog Post',
|
|
||||||
description: 'Read our latest article about spritesheet generation and game development.',
|
|
||||||
url: `/blog/${slug.value}`,
|
|
||||||
type: 'article',
|
|
||||||
keywords: 'sprite sheet, game development, blog'
|
|
||||||
});
|
|
||||||
|
|
||||||
addBreadcrumbSchema([
|
const pageDescription = computed(() => post.value?.description || 'Read our latest article about spritesheet generation and game development.');
|
||||||
{ name: 'Home', url: '/' },
|
|
||||||
{ name: 'Blog', url: '/blog' },
|
const pageImage = computed(() => (post.value?.image ? `https://spritesheetgenerator.online${post.value.image}` : 'https://spritesheetgenerator.online/og-image.png'));
|
||||||
{ name: 'Article', url: `/blog/${slug.value}` }
|
|
||||||
]);
|
const pageUrl = computed(() => `https://spritesheetgenerator.online/blog/${slug.value}`);
|
||||||
|
|
||||||
|
const keywords = computed(() => post.value?.keywords || 'sprite sheet, game development, blog');
|
||||||
|
|
||||||
|
// Dynamic meta tags using reactive computed values
|
||||||
|
useHead({
|
||||||
|
title: pageTitle,
|
||||||
|
meta: [
|
||||||
|
{ name: 'title', content: pageTitle },
|
||||||
|
{ name: 'description', content: pageDescription },
|
||||||
|
{ name: 'keywords', content: keywords },
|
||||||
|
{ name: 'robots', content: 'index, follow' },
|
||||||
|
|
||||||
|
// Open Graph
|
||||||
|
{ property: 'og:type', content: 'article' },
|
||||||
|
{ property: 'og:url', content: pageUrl },
|
||||||
|
{ property: 'og:title', content: pageTitle },
|
||||||
|
{ property: 'og:description', content: pageDescription },
|
||||||
|
{ property: 'og:image', content: pageImage },
|
||||||
|
{ property: 'og:site_name', content: 'Spritesheet Generator' },
|
||||||
|
{ property: 'article:author', content: computed(() => post.value?.author || 'nu11ed') },
|
||||||
|
{ property: 'article:published_time', content: computed(() => post.value?.date || '') },
|
||||||
|
|
||||||
|
// Twitter
|
||||||
|
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||||
|
{ name: 'twitter:url', content: pageUrl },
|
||||||
|
{ name: 'twitter:title', content: pageTitle },
|
||||||
|
{ name: 'twitter:description', content: pageDescription },
|
||||||
|
{ name: 'twitter:image', content: pageImage },
|
||||||
|
],
|
||||||
|
link: [{ rel: 'canonical', href: pageUrl }],
|
||||||
|
script: computed(() => {
|
||||||
|
const scripts = [];
|
||||||
|
|
||||||
|
// Breadcrumb schema
|
||||||
|
scripts.push({
|
||||||
|
type: 'application/ld+json',
|
||||||
|
children: JSON.stringify({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 1,
|
||||||
|
name: 'Home',
|
||||||
|
item: 'https://spritesheetgenerator.online/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 2,
|
||||||
|
name: 'Blog',
|
||||||
|
item: 'https://spritesheetgenerator.online/blog',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 3,
|
||||||
|
name: post.value?.title || 'Article',
|
||||||
|
item: pageUrl.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Blog post schema
|
||||||
|
if (post.value) {
|
||||||
|
scripts.push({
|
||||||
|
type: 'application/ld+json',
|
||||||
|
children: JSON.stringify({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BlogPosting',
|
||||||
|
headline: post.value.title,
|
||||||
|
description: post.value.description,
|
||||||
|
image: `https://spritesheetgenerator.online${post.value.image}`,
|
||||||
|
author: {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: post.value.author || 'nu11ed',
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: 'Spritesheet Generator',
|
||||||
|
logo: {
|
||||||
|
'@type': 'ImageObject',
|
||||||
|
url: 'https://spritesheetgenerator.online/og-image.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datePublished: post.value.date,
|
||||||
|
dateModified: post.value.date,
|
||||||
|
mainEntityOfPage: {
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': pageUrl.value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return scripts;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
post.value = await getPost(slug.value);
|
post.value = await getPost(slug.value);
|
||||||
@@ -39,37 +128,6 @@
|
|||||||
router.push({ name: 'blog-overview' });
|
router.push({ name: 'blog-overview' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update SEO and structured data when post loads
|
|
||||||
watch(post, (newPost) => {
|
|
||||||
if (newPost) {
|
|
||||||
useSEO({
|
|
||||||
title: newPost.title,
|
|
||||||
description: newPost.description,
|
|
||||||
image: newPost.image,
|
|
||||||
url: `/blog/${slug.value}`,
|
|
||||||
type: 'article',
|
|
||||||
author: newPost.author || 'nu11ed',
|
|
||||||
publishedTime: newPost.date,
|
|
||||||
keywords: newPost.keywords || 'sprite sheet, game development, blog'
|
|
||||||
});
|
|
||||||
|
|
||||||
addBlogPostSchema({
|
|
||||||
title: newPost.title,
|
|
||||||
description: newPost.description,
|
|
||||||
author: newPost.author || 'nu11ed',
|
|
||||||
datePublished: newPost.date,
|
|
||||||
image: newPost.image,
|
|
||||||
url: `/blog/${slug.value}`
|
|
||||||
});
|
|
||||||
|
|
||||||
addBreadcrumbSchema([
|
|
||||||
{ name: 'Home', url: '/' },
|
|
||||||
{ name: 'Blog', url: '/blog' },
|
|
||||||
{ name: newPost.title, url: `/blog/${slug.value}` }
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useBlog, type BlogPost } from '../composables/useBlog';
|
import { useBlog, type BlogPost } from '../composables/useBlog';
|
||||||
import { useSEO } from '../composables/useSEO';
|
import { useSEO } from '../composables/useSEO';
|
||||||
import { useStructuredData } from '../composables/useStructuredData';
|
import { useStructuredData } from '../composables/useStructuredData';
|
||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
const { getPosts } = useBlog();
|
const { getPosts } = useBlog();
|
||||||
const { addBlogListSchema, addBreadcrumbSchema } = useStructuredData();
|
const { addBreadcrumbSchema } = useStructuredData();
|
||||||
const posts = ref<BlogPost[]>([]);
|
const posts = ref<BlogPost[]>([]);
|
||||||
|
|
||||||
// Set SEO meta tags synchronously
|
// Set SEO meta tags synchronously
|
||||||
@@ -15,34 +15,18 @@
|
|||||||
description: 'Explore our latest articles about sprite sheet generation, game development, pixel art, and sprite animation techniques.',
|
description: 'Explore our latest articles about sprite sheet generation, game development, pixel art, and sprite animation techniques.',
|
||||||
url: '/blog',
|
url: '/blog',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
keywords: 'sprite sheet blog, game development articles, pixel art tutorials, sprite animation'
|
keywords: 'sprite sheet blog, game development articles, pixel art tutorials, sprite animation',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add breadcrumb synchronously
|
// Add breadcrumb synchronously
|
||||||
addBreadcrumbSchema([
|
addBreadcrumbSchema([
|
||||||
{ name: 'Home', url: '/' },
|
{ name: 'Home', url: '/' },
|
||||||
{ name: 'Blog', url: '/blog' }
|
{ name: 'Blog', url: '/blog' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
posts.value = await getPosts();
|
posts.value = await getPosts();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch posts and add structured data when available
|
|
||||||
watch(posts, (newPosts) => {
|
|
||||||
if (newPosts.length > 0) {
|
|
||||||
addBlogListSchema(
|
|
||||||
newPosts.map(post => ({
|
|
||||||
title: post.title,
|
|
||||||
description: post.description,
|
|
||||||
author: post.author || 'nu11ed',
|
|
||||||
datePublished: post.date,
|
|
||||||
image: post.image,
|
|
||||||
url: `/blog/${post.slug}`
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -7,15 +7,15 @@
|
|||||||
// Set SEO synchronously
|
// Set SEO synchronously
|
||||||
useSEO({
|
useSEO({
|
||||||
title: 'Contact Us - Get in Touch',
|
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!',
|
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',
|
url: '/contact',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
keywords: 'contact spritesheet generator, support, discord, gitea'
|
keywords: 'contact spritesheet generator, support, discord, gitea',
|
||||||
});
|
});
|
||||||
|
|
||||||
addBreadcrumbSchema([
|
addBreadcrumbSchema([
|
||||||
{ name: 'Home', url: '/' },
|
{ name: 'Home', url: '/' },
|
||||||
{ name: 'Contact', url: '/contact' }
|
{ name: 'Contact', url: '/contact' },
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -26,7 +26,14 @@
|
|||||||
<div class="space-y-6">
|
<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>
|
<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">
|
<div class="flex flex-col gap-4 mt-8">
|
||||||
<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">
|
<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>
|
<i class="fab fa-discord text-2xl text-[#5865F2]"></i>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold text-gray-900 dark:text-white">Join our Discord</div>
|
<div class="font-bold text-gray-900 dark:text-white">Join our Discord</div>
|
||||||
@@ -34,7 +41,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<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">
|
<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>
|
<i class="fab fa-github text-2xl text-gray-900 dark:text-white"></i>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold text-gray-900 dark:text-white">Source Code</div>
|
<div class="font-bold text-gray-900 dark:text-white">Source Code</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function useHomeViewSEO() {
|
|||||||
description: 'Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.',
|
description: 'Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.',
|
||||||
url: '/',
|
url: '/',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
keywords: 'spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools'
|
keywords: 'spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add organization schema
|
// Add organization schema
|
||||||
@@ -28,35 +28,26 @@ export function useHomeViewSEO() {
|
|||||||
children: JSON.stringify({
|
children: JSON.stringify({
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'SoftwareApplication',
|
'@type': 'SoftwareApplication',
|
||||||
'name': 'Spritesheet Generator',
|
name: 'Spritesheet Generator',
|
||||||
'applicationCategory': 'DesignApplication',
|
applicationCategory: 'DesignApplication',
|
||||||
'offers': {
|
offers: {
|
||||||
'@type': 'Offer',
|
'@type': 'Offer',
|
||||||
'price': '0',
|
price: '0',
|
||||||
'priceCurrency': 'USD'
|
priceCurrency: 'USD',
|
||||||
},
|
},
|
||||||
'operatingSystem': 'Web Browser',
|
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.',
|
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',
|
url: 'https://spritesheetgenerator.online',
|
||||||
'screenshot': 'https://spritesheetgenerator.online/og-image.png',
|
screenshot: 'https://spritesheetgenerator.online/og-image.png',
|
||||||
'featureList': [
|
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)'],
|
||||||
'Free sprite editor',
|
browserRequirements: 'Requires JavaScript. Requires HTML5.',
|
||||||
'Automatic spritesheet generation',
|
aggregateRating: {
|
||||||
'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',
|
'@type': 'AggregateRating',
|
||||||
'ratingValue': '4.8',
|
ratingValue: '4.8',
|
||||||
'ratingCount': '127'
|
ratingCount: '127',
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
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.',
|
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',
|
url: '/privacy-policy',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
keywords: 'privacy policy, data protection, client-side processing'
|
keywords: 'privacy policy, data protection, client-side processing',
|
||||||
});
|
});
|
||||||
|
|
||||||
addBreadcrumbSchema([
|
addBreadcrumbSchema([
|
||||||
{ name: 'Home', url: '/' },
|
{ name: 'Home', url: '/' },
|
||||||
{ name: 'Privacy Policy', url: '/privacy-policy' }
|
{ name: 'Privacy Policy', url: '/privacy-policy' },
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user