216 lines
8.0 KiB
Vue
216 lines
8.0 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, computed, watch } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useBlog, type BlogPost } from '../composables/useBlog';
|
|
import { useHead } from '@vueuse/head';
|
|
import { marked } from 'marked';
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const { getPost } = useBlog();
|
|
const post = ref<BlogPost | undefined>(undefined);
|
|
const renderedContent = ref('');
|
|
|
|
const slug = computed(() => route.params.slug as string);
|
|
|
|
const pageTitle = computed(() => (post.value ? `${post.value.title} - Spritesheet Generator` : 'Blog Post - Spritesheet Generator'));
|
|
|
|
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');
|
|
|
|
useHead({
|
|
title: pageTitle,
|
|
meta: [
|
|
{ name: 'title', content: pageTitle },
|
|
{ name: 'description', content: pageDescription },
|
|
{ name: 'keywords', content: keywords },
|
|
{ name: 'robots', content: 'index, follow' },
|
|
|
|
{ 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 || 'streetshadow') },
|
|
{ property: 'article:published_time', content: computed(() => post.value?.date || '') },
|
|
|
|
{ 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 = [];
|
|
|
|
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,
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
|
|
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 || 'streetshadow',
|
|
},
|
|
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);
|
|
|
|
if (post.value) {
|
|
renderedContent.value = await marked(post.value.content);
|
|
} else {
|
|
router.push({ name: 'blog-overview' });
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="w-full">
|
|
<div v-if="post" class="glass-panel rounded-3xl shadow-2xl p-8 sm:p-12">
|
|
<RouterLink :to="{ name: 'blog-overview' }" class="inline-flex items-center text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-6 transition-colors" title="Return to blog overview" aria-label="Navigate back to blog overview page">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Back to overview
|
|
</RouterLink>
|
|
|
|
<div class="mb-10 sm:mb-12 max-w-3xl">
|
|
<div class="flex items-center gap-3 text-sm text-gray-500 dark:text-gray-400 mb-6 font-medium">
|
|
<time :datetime="post.date">
|
|
{{ new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }}
|
|
</time>
|
|
<span class="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
|
|
<span v-if="post.author">
|
|
{{ post.author }}
|
|
</span>
|
|
</div>
|
|
|
|
<h1 class="text-3xl sm:text-5xl font-bold tracking-tight text-gray-900 dark:text-white leading-tight">
|
|
{{ post.title }}
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Image is intentionally omitted here as per requirements -->
|
|
|
|
<div
|
|
class="markdown-content prose prose-lg dark:prose-invert max-w-none prose-headings:font-bold prose-headings:tracking-tight prose-headings:text-gray-900 dark:prose-headings:text-gray-100 prose-p:leading-relaxed prose-a:text-primary-600 prose-a:no-underline prose-a:border-b prose-a:border-primary-300 hover:prose-a:border-primary-600 prose-a:transition-colors prose-blockquote:not-italic prose-blockquote:font-normal prose-blockquote:text-gray-600 dark:prose-blockquote:text-gray-300 prose-blockquote:border-l-4 prose-blockquote:border-primary-500 prose-blockquote:bg-gray-50 dark:prose-blockquote:bg-gray-800/30 prose-blockquote:rounded-r prose-code:text-primary-700 dark:prose-code:text-primary-300 prose-code:bg-primary-50 dark:prose-code:bg-primary-900/30 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-code:font-mono prose-code:text-sm prose-pre:bg-gray-900 prose-pre:text-gray-200 prose-pre:shadow-lg prose-pre:rounded-xl prose-img:rounded-xl prose-img:shadow-lg prose-img:border prose-img:border-gray-200 dark:prose-img:border-gray-700 prose-hr:border-gray-200 dark:prose-hr:border-gray-800 prose-li:marker:text-primary-500"
|
|
v-html="renderedContent"
|
|
></div>
|
|
</div>
|
|
<div v-else class="text-center py-12">
|
|
<p class="text-xl text-gray-600 dark:text-gray-400">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Force spacing for markdown content since prose modifiers can be overridden */
|
|
.markdown-content :deep(p) {
|
|
margin-top: 1.5em;
|
|
margin-bottom: 1.5em;
|
|
}
|
|
|
|
.markdown-content :deep(h2),
|
|
.markdown-content :deep(h3),
|
|
.markdown-content :deep(h4) {
|
|
margin-top: 2em;
|
|
margin-bottom: 1em;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.markdown-content :deep(img) {
|
|
margin-top: 2.5em;
|
|
margin-bottom: 2.5em;
|
|
}
|
|
|
|
.markdown-content :deep(ul),
|
|
.markdown-content :deep(ol) {
|
|
margin-top: 1.5em;
|
|
margin-bottom: 1.5em;
|
|
}
|
|
|
|
.markdown-content :deep(li) {
|
|
margin-top: 0.5em;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
|
|
.markdown-content :deep(blockquote) {
|
|
margin-top: 2em;
|
|
margin-bottom: 2em;
|
|
padding-top: 1em;
|
|
padding-bottom: 1em;
|
|
padding-left: 1.5em;
|
|
}
|
|
|
|
.markdown-content :deep(pre) {
|
|
margin-top: 2em;
|
|
margin-bottom: 2em;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.markdown-content :deep(hr) {
|
|
margin-top: 3em;
|
|
margin-bottom: 3em;
|
|
}
|
|
</style>
|