[FEAT] SEO best practices
This commit is contained in:
@@ -1,34 +1,123 @@
|
||||
<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 { useBlog, type BlogPost } from '../composables/useBlog';
|
||||
import { useSEO } from '../composables/useSEO';
|
||||
import { useStructuredData } from '../composables/useStructuredData';
|
||||
import { useHead } from '@vueuse/head';
|
||||
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('');
|
||||
|
||||
const slug = computed(() => route.params.slug as string);
|
||||
|
||||
// Initialize with default SEO (will be updated when post loads)
|
||||
useSEO({
|
||||
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'
|
||||
});
|
||||
// Reactive SEO data that updates when post loads
|
||||
const pageTitle = computed(() => (post.value ? `${post.value.title} - Spritesheet Generator` : 'Blog Post - Spritesheet Generator'));
|
||||
|
||||
addBreadcrumbSchema([
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Blog', url: '/blog' },
|
||||
{ name: 'Article', url: `/blog/${slug.value}` }
|
||||
]);
|
||||
const pageDescription = computed(() => post.value?.description || 'Read our latest article about spritesheet generation and game development.');
|
||||
|
||||
const pageImage = computed(() => (post.value?.image ? `https://spritesheetgenerator.online${post.value.image}` : 'https://spritesheetgenerator.online/og-image.png'));
|
||||
|
||||
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 () => {
|
||||
post.value = await getPost(slug.value);
|
||||
@@ -39,37 +128,6 @@
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
Reference in New Issue
Block a user