index/pages/blog/[slug].vue

233 lines
6.1 KiB
Vue

<script setup lang="ts">
import { formatDate } from 'date-fns'
import chestAnimation from '~/assets/images/chest.webp'
const config = useAppConfig()
const route = useRoute()
let thumbnail: string | null = null
let slug = route.params.slug
if (Array.isArray(slug)) slug = slug.join('/')
const { data, status } = await useAsyncData('home', () =>
queryContent(`/blog/${slug}`).findOne(),
)
const meta = {
title: data.value ? data.value.title : 'Page not found',
description: data.value
? data.value.description
: 'The page you are looking for does not exist. It might have been removed, had its name changed, or is temporarily unavailable.',
url: `${config.url}/blog`,
}
// When defineOgImage is used, useSeoMeta must exclude ogImage and twitterImage properties.
useSeoMeta({
title: meta.title,
description: meta.description,
ogTitle: meta.title,
ogDescription: meta.description,
// ogImage: EXCLUDED
ogUrl: meta.url,
ogType: 'article',
twitterTitle: meta.title,
twitterDescription: meta.description,
// twitterImage: EXCLUDED
twitterCard: 'summary_large_image',
})
if (data.value) {
thumbnail =
data.value.thumbnail ??
`/images/blog/thumbnails/${data.value._path!.split('/').at(-1)}.png`
// Generate the article's Open Graph image.
defineOgImageComponent(
'OgThumbnail',
{
title: data.value.title,
description: data.value.description,
logo: '/images/logo.png',
sectionLogo: '/images/chest.png',
thumbnail,
alt: data.value.title,
},
{
fonts: ['Alexandria:700', 'Lato:700'],
},
)
// Hydrate the rendered items.
onMounted(() => {
document.querySelectorAll('pre').forEach((pre) => {
const icon = document.createElement('iconify-icon')
icon.setAttribute('icon', 'mdi:content-copy')
icon.setAttribute('inline', 'true')
icon.classList.add('button')
pre.appendChild(icon)
})
document
.querySelectorAll('code:not(pre *), pre > iconify-icon.button')
.forEach((code) => {
if (code instanceof HTMLElement) {
code.onclick = () => {
const area = document.createElement('textarea')
area.textContent =
code.nodeName && code.nodeName.toLowerCase() === 'code'
? code.textContent
: code.parentElement!.textContent
// It's necessary to create the textarea element every time you copy to get access to the select() method.
area.setSelectionRange(0, 99999) // An iOS gotcha.
area.select()
// Copy the text inside the textarea.
navigator.clipboard.writeText(area.value)
// TODO: Alert the user text has been successfully copied.
// Remove the textarea element.
area.remove()
}
}
})
})
onUnmounted(() => {
document.querySelectorAll('code').forEach((code) => {
code.onclick = null
})
document.querySelectorAll('pre').forEach((pre) => {
pre.querySelector('.clipboard')?.remove()
})
})
}
useHead({
title: data.value ? data.value.title : 'Page not found',
htmlAttrs: {
lang: config.locale || 'en',
},
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico',
},
],
})
</script>
<template>
<div
v-if="status === 'pending'"
class="flex flex-col items-center justify-center gap-4 w-full select-none text-center"
>
<img draggable="false" :src="chestAnimation" alt="The Ender Chest" />
<div>
<h1 class="font-enchant">Loading document...</h1>
<span class="opacity-0 hover:opacity-100 transition-ease">
<em>Loading document...</em>
</span>
</div>
</div>
<article
v-else-if="status === 'success' && data"
class="flex-grow post snap-normal fade-mask-sm flex flex-col gap-4 overflow-x-hidden overflow-y-auto sm:py-4 sm:pe-4"
>
<div class="grid-thumbnail grid max-w-[768px] snap-end">
<img
draggable="false"
:src="thumbnail!"
alt="Thumbnail"
class="grid-thumbnail-image object-cover object-center rounded-xl aspect-[16/9] select-none"
/>
<NuxtLink to="/blog" class="grid-thumbnail-start button-back p-2">
<iconify-icon
icon="icon-park-solid:back"
width="2em"
height="2em"
class="drop-shadow text-pink-200"
/>
</NuxtLink>
<NuxtLink
class="grid-thumbnail-end button-twitter p-2"
:href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${data.title}&hashtags=${data.tags.slice(0, 3).join(',').replace(/ /g, '')}`"
target="_blank"
>
<iconify-icon
icon="logos:twitter"
width="2em"
height="2em"
class="drop-shadow"
/>
</NuxtLink>
<div
class="grid-thumbnail-title accent-fade w-full rounded-b-lg p-2 mt-16"
>
<h3 class="mb-1">{{ data.title }}</h3>
<div class="flex flex-row flex-wrap gap-x-2 gap-y-0">
<small class="flex flex-row items-center gap-1 whitespace-nowrap">
<iconify-icon icon="mdi:calendar" inline />
<strong>
{{ formatDate(data.created, 'HH:mm • LLLL do, y') }}
</strong>
</small>
<small class="flex flex-row items-center gap-1 whitespace-nowrap">
<iconify-icon icon="mdi:clock-outline" inline />
<strong>
{{ data!.readingTime.text.split(' ')[0] + ' minutes to read' }}
</strong>
</small>
</div>
</div>
</div>
<section class="page snap-start">
<ContentRenderer :value="data" />
</section>
</article>
<NotFound v-else />
</template>
<style scoped lang="scss">
.grid-thumbnail {
&-image {
grid-column: 1;
grid-row: 1;
place-self: start center;
max-width: clamp(1px, 100%, 768px);
min-height: 100%;
}
&-title {
grid-column: 1;
grid-row: 1;
place-self: end center;
}
&-start {
grid-column: 1;
grid-row: 1;
place-self: start start;
}
&-end {
grid-column: 1;
grid-row: 1;
place-self: start end;
}
}
</style>