Raw blog finished
This commit is contained in:
+145
-98
@@ -1,125 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import { formatDate } from 'date-fns'
|
||||
const { slug } = useRoute().params
|
||||
import { useAsyncData } from '#app'
|
||||
|
||||
const config = useAppConfig()
|
||||
const content = useContent()
|
||||
|
||||
let thumbnail: string | null = null
|
||||
let slug = useRoute().params.slug
|
||||
|
||||
if (Array.isArray(slug)) slug = slug.join('/')
|
||||
|
||||
const { data } = await useAsyncData('home', () =>
|
||||
queryContent(`/blog/${slug}`).findOne(),
|
||||
)
|
||||
|
||||
const meta = {
|
||||
title: `${content.page.title}`,
|
||||
description: content.page.description,
|
||||
image: content.page.thumbnail,
|
||||
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`,
|
||||
}
|
||||
|
||||
defineOgImage()
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
})
|
||||
// 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',
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.querySelectorAll('code').forEach((code) => {
|
||||
code.onclick = null
|
||||
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(
|
||||
'OgImage',
|
||||
{
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
document.querySelectorAll('pre').forEach((pre) => {
|
||||
pre.querySelector('.clipboard')?.remove()
|
||||
onUnmounted(() => {
|
||||
document.querySelectorAll('code').forEach((code) => {
|
||||
code.onclick = null
|
||||
})
|
||||
|
||||
document.querySelectorAll('pre').forEach((pre) => {
|
||||
pre.querySelector('.clipboard')?.remove()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'Page not found',
|
||||
htmlAttrs: {
|
||||
lang: config.locale || 'en',
|
||||
},
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/favicon.ico',
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
v-if="data"
|
||||
class="post scrollbar fade-mask-sm d-flex flex-column gap-3 flex-grow-1 overflow-x-hidden overflow-y-auto py-sm-4 pe-sm-3"
|
||||
>
|
||||
<ContentDoc :path="`/blog/${slug}`">
|
||||
<template #default="{ doc }">
|
||||
<div
|
||||
class="post-preamble"
|
||||
:style="{ backgroundImage: 'url(' + doc.thumbnail + ')' }"
|
||||
>
|
||||
<div class="p-2">
|
||||
<h3 class="alex">{{ doc.title }}</h3>
|
||||
<div class="d-flex flex-row row-gap-0 column-gap-2 flex-wrap">
|
||||
<div class="d-flex flex-row gap-1 align-items-center">
|
||||
<iconify-icon icon="mdi:calendar" />
|
||||
<strong class="nobr font-small">
|
||||
{{ formatDate(doc.created, 'LLLL do, y – HH:mm') }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="d-flex flex-row gap-1 align-items-center">
|
||||
<iconify-icon icon="mdi:clock-outline" />
|
||||
<strong class="nobr font-small">
|
||||
{{ doc.readingTime.text.split(' ')[0] + ' minutes to read' }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="post-preamble"
|
||||
:style="{ backgroundImage: 'url(' + thumbnail + ')' }"
|
||||
>
|
||||
<div class="p-2">
|
||||
<h3 class="alex">{{ data.title }}</h3>
|
||||
<div class="d-flex flex-row row-gap-0 column-gap-2 flex-wrap">
|
||||
<div class="d-flex flex-row gap-1 align-items-center">
|
||||
<iconify-icon icon="mdi:calendar" />
|
||||
<strong class="nobr font-small">
|
||||
{{ formatDate(data.created, 'LLLL do, y – HH:mm') }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="d-flex flex-row gap-1 align-items-center">
|
||||
<iconify-icon icon="mdi:clock-outline" />
|
||||
<strong class="nobr font-small">
|
||||
{{ data!.readingTime.text.split(' ')[0] + ' minutes to read' }}
|
||||
</strong>
|
||||
</div>
|
||||
<NuxtLink
|
||||
:href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${doc.title}&hashtags=${doc.tags.slice(0, 3).join(',').replace(/ /g, '')}`"
|
||||
target="_blank"
|
||||
class="link post-preamble-share px-2 py-1"
|
||||
>
|
||||
<iconify-icon icon="mdi:twitter"></iconify-icon>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/blog" class="link post-preamble-control px-2 py-1">
|
||||
<iconify-icon icon="icon-park-solid:back"></iconify-icon>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<Separator class="mt-0" />
|
||||
<ContentRenderer :value="doc" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #not-found>
|
||||
<div
|
||||
class="d-flex flex-column justify-content-center align-items-center gap-3"
|
||||
>
|
||||
<span>Blog post not found 🙁</span>
|
||||
<NuxtLink to="/blog" class="link-hi">Back to blog</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
</ContentDoc>
|
||||
</div>
|
||||
<NuxtLink
|
||||
:href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${data.title}&hashtags=${data.tags.slice(0, 3).join(',').replace(/ /g, '')}`"
|
||||
target="_blank"
|
||||
class="link post-preamble-share px-2 py-1"
|
||||
>
|
||||
<iconify-icon icon="mdi:twitter"></iconify-icon>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/blog" class="link post-preamble-control px-2 py-1">
|
||||
<iconify-icon icon="icon-park-solid:back"></iconify-icon>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<Separator class="mt-0" />
|
||||
<ContentRenderer :value="data" />
|
||||
</div>
|
||||
</article>
|
||||
<NotFound v-else />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
+10
-2
@@ -41,7 +41,12 @@ useHead({
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h3 class="alex">Recent posts</h3>
|
||||
<h3 class="alex">The Enderchest</h3>
|
||||
<p>
|
||||
You're browsing the enderchest — a blog about software development,
|
||||
scientific research and Windows quirks.
|
||||
</p>
|
||||
<h4 class="alex">Recent posts</h4>
|
||||
<ContentList
|
||||
:query="{
|
||||
path: '/blog',
|
||||
@@ -64,7 +69,10 @@ useHead({
|
||||
<div class="post-thumb">
|
||||
<img
|
||||
draggable="false"
|
||||
:src="post.thumbnail"
|
||||
:src="
|
||||
post.thumbnail ??
|
||||
`/images/blog/thumbnails/${post._path!.split('/').at(-1)}.png`
|
||||
"
|
||||
:alt="post.title"
|
||||
class="rounded-4"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user