add: SEO for articles
SEO and SchemaORg for articles
This commit is contained in:
parent
f1cb4048a4
commit
d6859cdaad
22 changed files with 198 additions and 125 deletions
|
@ -3,14 +3,39 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|
||||||
|
& .meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .article-content {
|
||||||
|
background: var(--color-black);
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .article-text {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& header {
|
||||||
|
padding: 2rem 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
|
||||||
& .image {
|
& .image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 450px;
|
height: 450px;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem 1rem 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #000;
|
background: #000;
|
||||||
|
|
||||||
|
@ -19,6 +44,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
& > .image {
|
& > .image {
|
||||||
flex: 0 0 200px;
|
flex: 0 0 200px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
& img {
|
& img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -159,6 +159,15 @@ span.chip {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
transition: var(--transition-time);
|
transition: var(--transition-time);
|
||||||
|
|
||||||
|
&.interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-orange-dark);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.dark) {
|
&:not(.dark) {
|
||||||
--background: var(--color-orange);
|
--background: var(--color-orange);
|
||||||
--color: var(--color-black);
|
--color: var(--color-black);
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<aside class="BlogAuthor">
|
<aside class="BlogAuthor">
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img :src="image" :alt="`Bild von ${name}`"/>
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
:srcset="imageSet.join(', ')"
|
||||||
|
:src="initialImage"
|
||||||
|
aria-hidden="true"
|
||||||
|
:alt="`Profilbild von ${name}`"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span class="name">{{ name }}</span>
|
<span class="name">{{ name }}</span>
|
||||||
|
@ -11,13 +19,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { getImageSet, getInitialImage } from '../../utils/image'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string
|
name: string
|
||||||
image: string
|
|
||||||
date: string
|
date: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { date } = defineProps<Props>()
|
const { name, date } = defineProps<Props>()
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat('de-DE', {
|
const formatter = new Intl.DateTimeFormat('de-DE', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
@ -26,4 +35,7 @@ const formatter = new Intl.DateTimeFormat('de-DE', {
|
||||||
})
|
})
|
||||||
|
|
||||||
const dateFormatted = computed(() => formatter.format(new Date(date)))
|
const dateFormatted = computed(() => formatter.format(new Date(date)))
|
||||||
|
|
||||||
|
const imageSet = getImageSet('/img/blog/authors/', name)
|
||||||
|
const initialImage = getInitialImage('/img/blog/authors/', name)
|
||||||
</script>
|
</script>
|
|
@ -10,15 +10,11 @@
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<p>
|
<p>
|
||||||
{{ generatePlainText(excerpt.value).at(0)?.text ?? '' }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<BlogAuthor :name="author.name" :image="author.image" :date="date"/>
|
<BlogAuthor :name="author.name" :date="date"/>
|
||||||
<div class="tags">
|
|
||||||
<span>tags</span>
|
|
||||||
<span class="tag" v-for="tag in tags">{{ tag }}</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
@ -26,20 +22,16 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Category } from './types'
|
import type { Category } from './types'
|
||||||
import type { MinimalNode } from '@nuxt/content'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
image: string
|
image: string
|
||||||
date: string
|
date: string
|
||||||
excerpt: { type: string, value: MinimalNode[], children?: any }
|
|
||||||
link: string
|
link: string
|
||||||
tags: string[]
|
|
||||||
category: Category
|
category: Category
|
||||||
author: {
|
author: {
|
||||||
name: string
|
name: string
|
||||||
image: string
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,9 @@
|
||||||
<span class="my-name-wrapper">Ich bin <Highlight>Fiona</Highlight>.</span>
|
<span class="my-name-wrapper">Ich bin <Highlight>Fiona</Highlight>.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2>
|
<h2>
|
||||||
Component <Highlight>&</Highlight> API Entwicklerin
|
Component
|
||||||
|
<Highlight>&</Highlight>
|
||||||
|
API Entwicklerin
|
||||||
</h2>
|
</h2>
|
||||||
<p class="fulltext">
|
<p class="fulltext">
|
||||||
Ich unterstütze Unternehmen dabei, ihre Daten von verschiedenen Endpunkten sauber aufzubereiten
|
Ich unterstütze Unternehmen dabei, ihre Daten von verschiedenen Endpunkten sauber aufzubereiten
|
||||||
|
@ -21,14 +23,14 @@
|
||||||
Ich biete dir genau das, was du brauchst, um eine individuelle WebApp in Fahrt zu bringen, deren Inhalte einfach zu verändern sind.
|
Ich biete dir genau das, was du brauchst, um eine individuelle WebApp in Fahrt zu bringen, deren Inhalte einfach zu verändern sind.
|
||||||
</p>
|
</p>
|
||||||
<Button class="cta" href="#skills">
|
<Button class="cta" href="#skills">
|
||||||
<Icon name="ph:lightbulb-duotone" size="1.5em" mode="svg" />
|
<Icon name="ph:lightbulb-duotone" size="1.5em" mode="svg"/>
|
||||||
Fussel erklärt's dir
|
Fussel erklärt's dir
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="intro-img">
|
<div class="intro-img">
|
||||||
<picture>
|
<picture>
|
||||||
<source width="750" height="866" media="(min-width: 431px)" srcset="/img/profile_big.webp" />
|
<source width="750" height="866" media="(min-width: 431px)" srcset="/img/profile_big.webp"/>
|
||||||
<img width="430" height="866" src="/img/profile_small.webp" alt="Bild von Fiona Urban" />
|
<img width="430" height="866" src="/img/profile_small.webp" alt="Bild von Fiona Urban"/>
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
20
app/error.vue
Normal file
20
app/error.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<section class="bg-radial">
|
||||||
|
<NuxtLayout>
|
||||||
|
<section class="content">
|
||||||
|
<h1>{{ error?.statusCode }}</h1>
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
</section>
|
||||||
|
</NuxtLayout>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NuxtError } from 'nuxt/app'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
error: NuxtError
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = defineProps<Props>()
|
||||||
|
const message = getErrorMessage(error)
|
||||||
|
</script>
|
|
@ -5,29 +5,54 @@
|
||||||
Zurück zur Übersicht
|
Zurück zur Übersicht
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<p v-if="!article">
|
<main v-if="article" class="article-content z-3">
|
||||||
Sorry bro, aber der Artikel existiert einfach nicht.
|
<div class="image z-2">
|
||||||
</p>
|
<img :src="article.image" alt="Artikelbild" aria-hidden="true"/>
|
||||||
|
</div>
|
||||||
<div v-else>
|
|
||||||
<header>
|
<header>
|
||||||
<div class="image z-2">
|
<div class="meta">
|
||||||
<img :src="article.image" alt="Artikelbild" aria-hidden="true"/>
|
<NuxtLink :to="`/blog/?category=${article.category}`">
|
||||||
|
<span class="chip interactive"><BlogCategory :name="article.category as Category"/></span>
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="margin-top">{{ article.title }}</h1>
|
<h1 class="margin-top">{{ article.title }}</h1>
|
||||||
<h2>{{ article.description }}</h2>
|
<h2>{{ article.description }}</h2>
|
||||||
</header>
|
</header>
|
||||||
<div class="flex-col gap-default">
|
<div class="flex-col gap-default article-text">
|
||||||
<ContentRenderer v-if="article" :value="article" :style="{ display: 'contents' }"/>
|
<ContentRenderer v-if="article" :value="article" :style="{ display: 'contents' }"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Category } from '../../components/Blog/types'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const { data: article } = await useAsyncData('article', () => queryCollection('blog').path(route.path).where('date', '<', tomorrow(new Date())).first())
|
const { data: article } = await useAsyncData('article', () => queryCollection('blog').path(route.path).where('date', '<', tomorrow(new Date())).first())
|
||||||
|
|
||||||
|
if (!article.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Page Not Found',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
useHead(article.value.head || {})
|
||||||
|
useSeoMeta({
|
||||||
|
...(article.value.seo || {}),
|
||||||
|
ogTitle: article.value.title,
|
||||||
|
ogDescription: article.value.description,
|
||||||
|
ogImage: article.value.image,
|
||||||
|
ogImageAlt: article.value.description,
|
||||||
|
ogUrl: `https://webfussel.de${article.value.path}`,
|
||||||
|
twitterTitle: article.value.title,
|
||||||
|
twitterDescription: article.value.description,
|
||||||
|
twitterImage: article.value.image,
|
||||||
|
twitterImageAlt: article.value.description,
|
||||||
|
twitterUrl: `https://webfussel.de${article.value.path}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<section id="blog" class="BlogOverview content">
|
<section id="blog" class="BlogOverview content">
|
||||||
<main>
|
<h1>Blogfussel - für mehr Fussel im Blog</h1>
|
||||||
|
<p>Hey! Hier sammel ich alles, was mir so durch den Kopf geht - von handfesten Tutorials und Code-Snippets über Freelancing-Stories
|
||||||
|
bis hin zu News meiner Apps und random Gedanken. Manchmal ausführlich, manchmal nur kurz angerissen.</p>
|
||||||
|
|
||||||
|
<main class="margin-top">
|
||||||
<ul class="category-list">
|
<ul class="category-list">
|
||||||
<li>
|
<li>
|
||||||
<NuxtLink class="inline-flex-row gap-sm side" to="/blog">
|
<NuxtLink class="inline-flex-row gap-sm side" to="/blog">
|
||||||
<span class="chip" :class="{ 'dark' : route.query.category}">Alle {{ articles?.length }}</span>
|
<span class="chip"
|
||||||
|
:class="{ 'dark' : route.query.category && Object.keys(allCategoriesAndCount).includes(route.query.category as string)}">Alle {{
|
||||||
|
articles?.length
|
||||||
|
}}</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
<li v-for="(count, category) in allCategoriesAndCount">
|
<li v-for="(count, category) in allCategoriesAndCount">
|
||||||
|
@ -15,6 +22,8 @@
|
||||||
</ul>
|
</ul>
|
||||||
<div class="grid margin-top-middle article-overview">
|
<div class="grid margin-top-middle article-overview">
|
||||||
<BlogCard v-for="article in firstTen" v-bind="makeBlogCard(article)"/>
|
<BlogCard v-for="article in firstTen" v-bind="makeBlogCard(article)"/>
|
||||||
|
<div v-if="firstTen.length < 2"/>
|
||||||
|
<div v-if="firstTen.length < 3"/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
|
@ -33,8 +42,8 @@ const { data: articles } = await useAsyncData('articles', () => queryCollection(
|
||||||
)
|
)
|
||||||
|
|
||||||
const firstTen = computed(() => {
|
const firstTen = computed(() => {
|
||||||
if (route.query.category) {
|
if (route.query.category && Object.keys(allCategoriesAndCount.value).includes(route.query.category as Category)) {
|
||||||
return articles.value?.filter(article => article.meta.category === route.query.category).slice(0, 10) ?? []
|
return articles.value?.filter(article => article.category === route.query.category).slice(0, 10) ?? []
|
||||||
}
|
}
|
||||||
return articles.value?.slice(0, 10) ?? []
|
return articles.value?.slice(0, 10) ?? []
|
||||||
})
|
})
|
||||||
|
@ -42,7 +51,7 @@ const firstTen = computed(() => {
|
||||||
const allCategoriesAndCount = computed(() => {
|
const allCategoriesAndCount = computed(() => {
|
||||||
const categories = {} as Record<Category, number>
|
const categories = {} as Record<Category, number>
|
||||||
articles.value?.forEach(article => {
|
articles.value?.forEach(article => {
|
||||||
const category = article.meta.category as Category
|
const category = article.category as Category
|
||||||
if (category) {
|
if (category) {
|
||||||
categories[category] = (categories[category] ?? 0) + 1
|
categories[category] = (categories[category] ?? 0) + 1
|
||||||
}
|
}
|
||||||
|
@ -55,9 +64,7 @@ const makeBlogCard = (article: BlogCollectionItem) => ({
|
||||||
description: article.description,
|
description: article.description,
|
||||||
image: article.thumbnail as string,
|
image: article.thumbnail as string,
|
||||||
date: article.date as string,
|
date: article.date as string,
|
||||||
excerpt: article.excerpt as any,
|
|
||||||
link: article.path,
|
link: article.path,
|
||||||
tags: article.tags as string[],
|
|
||||||
category: article.category as Category,
|
category: article.category as Category,
|
||||||
author: article.author as { name: string, image: string },
|
author: article.author as { name: string, image: string },
|
||||||
})
|
})
|
||||||
|
|
15
app/utils/error.ts
Normal file
15
app/utils/error.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import type { NuxtError } from 'nuxt/app'
|
||||||
|
|
||||||
|
const codes: Record<number, string> = {
|
||||||
|
400: 'Hey, deine Anfrage war irgendwie kaputt. Check nochmal!',
|
||||||
|
404: 'Sorry bro, aber diese Seite existiert einfach nicht.',
|
||||||
|
408: 'Alter, das hat viel zu lange gedauert. Timeout!',
|
||||||
|
410: 'Diese Seite ist für immer weg. Gone!',
|
||||||
|
500: 'Crap, da ging was im Hintergrund schief. Sorry!',
|
||||||
|
501: 'Das haben wir noch nicht implementiert. Oops!',
|
||||||
|
502: 'Der Server dahinter spinnt gerade rum.',
|
||||||
|
503: 'Service ist gerade down. Versuch es später nochmal.',
|
||||||
|
504: 'Gateway Timeout - da hängt was fest.',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getErrorMessage = (error: NuxtError) => codes[error.statusCode] ?? 'Sorry, da ist etwas schief gelaufen.'
|
|
@ -1,8 +1,9 @@
|
||||||
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
|
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
|
||||||
|
import { asSchemaOrgCollection } from 'nuxt-schema-org/content'
|
||||||
|
|
||||||
export default defineContentConfig({
|
export default defineContentConfig({
|
||||||
collections: {
|
collections: {
|
||||||
blog: defineCollection({
|
blog: defineCollection(asSchemaOrgCollection({
|
||||||
type: 'page',
|
type: 'page',
|
||||||
source: 'blog/*.md',
|
source: 'blog/*.md',
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
|
@ -10,17 +11,12 @@ export default defineContentConfig({
|
||||||
image: z.string(),
|
image: z.string(),
|
||||||
thumbnail: z.string(),
|
thumbnail: z.string(),
|
||||||
category: z.string(),
|
category: z.string(),
|
||||||
tags: z.array(z.string()),
|
|
||||||
author: z.object({
|
author: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
image: z.string(),
|
image: z.string(),
|
||||||
}),
|
}),
|
||||||
excerpt: z.object({
|
|
||||||
type: z.string(),
|
|
||||||
children: z.any(),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
}),
|
})),
|
||||||
|
|
||||||
skills: defineCollection({
|
skills: defineCollection({
|
||||||
type: 'page',
|
type: 'page',
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
---
|
---
|
||||||
image: 'https://picsum.photos/1920/450?random=4'
|
image: '/img/blog/posts/0000.start.webp'
|
||||||
thumbnail: 'https://picsum.photos/750/250?random=4'
|
thumbnail: '/img/blog/posts/0000.start_thumb.webp'
|
||||||
title: 'Blogfussel - für mehr Fussel im Blog'
|
title: 'Blogfussel - schon wieder'
|
||||||
|
description: 'Warum ich mich dazu entschlossen habe, jetzt doch wieder mit dem Bloggen anzufangen? Da muss ich etwas ausholen.'
|
||||||
navigation: true
|
navigation: true
|
||||||
date: 2025-06-11
|
date: 2025-06-11
|
||||||
category: 'story'
|
category: 'story'
|
||||||
tags: [ 'start', 'story' ]
|
|
||||||
author:
|
author:
|
||||||
name: 'webfussel'
|
name: 'webfussel'
|
||||||
image: '/img/og.webp'
|
image: '/img/blog/authors/webfussel@3x.webp'
|
||||||
|
schemaOrg:
|
||||||
|
type: 'BlogPosting'
|
||||||
|
headline: 'Warum es wieder einen blogfussel gibt'
|
||||||
|
author:
|
||||||
|
type: 'Person'
|
||||||
|
name: 'Fiona Urban aka webfussel'
|
||||||
|
datePublished: '2025-06-11'
|
||||||
---
|
---
|
||||||
|
|
||||||
::blog-excerpt
|
Irgendein Text
|
||||||
Warum ich mich dazu entschlossen habe jetzt doch wieder mit dem Bloggen anzufangen? Da muss ich etwas ausholen.
|
|
||||||
::
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
Noch mehr Text
|
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
image: 'https://picsum.photos/600/250?random=3'
|
|
||||||
title: 'Post 1'
|
|
||||||
description: 'Blablabla'
|
|
||||||
navigation: true
|
|
||||||
date: 2025-05-20
|
|
||||||
category: 'tutorial'
|
|
||||||
tags: [ 'test', 'wasd' ]
|
|
||||||
author:
|
|
||||||
name: 'webfussel'
|
|
||||||
image: '/img/og.webp'
|
|
||||||
---
|
|
||||||
|
|
||||||
Blablabla test 1 2 3
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
Noch mehr Text
|
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
image: 'https://picsum.photos/600/250?random=2'
|
|
||||||
title: 'Post 2'
|
|
||||||
description: 'Blablabla'
|
|
||||||
navigation: true
|
|
||||||
date: 2025-05-16
|
|
||||||
category: 'snippet'
|
|
||||||
tags: [ 'test', 'asdf' ]
|
|
||||||
author:
|
|
||||||
name: 'webfussel'
|
|
||||||
image: '/img/og.webp'
|
|
||||||
---
|
|
||||||
|
|
||||||
Blablabla test 1 2 3
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
Noch mehr Text
|
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
image: 'https://picsum.photos/600/250?random=1'
|
|
||||||
title: 'Post 3'
|
|
||||||
description: 'Blablabla'
|
|
||||||
navigation: true
|
|
||||||
date: 2025-05-25
|
|
||||||
category: 'freelancing'
|
|
||||||
tags: [ 'test', '123' ]
|
|
||||||
author:
|
|
||||||
name: 'webfussel'
|
|
||||||
image: '/img/og.webp'
|
|
||||||
---
|
|
||||||
|
|
||||||
Blablabla test 1 2 3
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
Noch mehr Text
|
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
image: 'https://picsum.photos/600/250?random=5'
|
|
||||||
title: 'Post 4'
|
|
||||||
description: 'Blablabla'
|
|
||||||
navigation: true
|
|
||||||
date: 2025-05-25
|
|
||||||
category: 'news'
|
|
||||||
tags: [ 'test', '123' ]
|
|
||||||
author:
|
|
||||||
name: 'webfussel'
|
|
||||||
image: '/img/og.webp'
|
|
||||||
---
|
|
||||||
|
|
||||||
Blablabla test 1 2 3
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
Noch mehr Text
|
|
BIN
public/img/blog/authors/webfussel@1x.webp
Normal file
BIN
public/img/blog/authors/webfussel@1x.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 458 B |
BIN
public/img/blog/authors/webfussel@2x.webp
Normal file
BIN
public/img/blog/authors/webfussel@2x.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
BIN
public/img/blog/authors/webfussel@3x.webp
Normal file
BIN
public/img/blog/authors/webfussel@3x.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
public/img/blog/posts/0000.start.webp
Normal file
BIN
public/img/blog/posts/0000.start.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
BIN
public/img/blog/posts/0000.start_thumb.webp
Normal file
BIN
public/img/blog/posts/0000.start_thumb.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
39
server/routes/blog/posts.json.ts
Normal file
39
server/routes/blog/posts.json.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { BlogCollectionItem } from '@nuxt/content'
|
||||||
|
|
||||||
|
|
||||||
|
const simpleDate = (date: Date) => {
|
||||||
|
date.setDate(date.getDate() + 1)
|
||||||
|
return `${date.getFullYear()}-${`${date.getMonth() + 1}`.padStart(2, '0')}-${date.getDate()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const author = (baseUrl: string) => ({
|
||||||
|
name: 'webfussel',
|
||||||
|
img: `${baseUrl}/img/blog/authors/webfussel@3x.webp`,
|
||||||
|
img1x: `${baseUrl}/img/blog/authors/webfussel@1x.webp`,
|
||||||
|
img2x: `${baseUrl}/img/blog/authors/webfussel@2x.webp`,
|
||||||
|
img3x: `${baseUrl}/img/blog/authors/webfussel@3x.webp`,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
const url = getRequestURL(event)
|
||||||
|
const baseUrl = `${url.protocol}//${url.host}`
|
||||||
|
|
||||||
|
const articles = await queryCollection(event, 'blog')
|
||||||
|
.where('date', '<', simpleDate(new Date()))
|
||||||
|
.order('date', 'DESC')
|
||||||
|
.all()
|
||||||
|
|
||||||
|
const entries = articles.map(article => ({
|
||||||
|
url: `${baseUrl}${article.path}`,
|
||||||
|
title: article.title,
|
||||||
|
excerpt: article.description,
|
||||||
|
date: article.date,
|
||||||
|
thumbnail: `${baseUrl}${article.thumbnail}`,
|
||||||
|
category: article.category,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
author: author(baseUrl),
|
||||||
|
posts: entries,
|
||||||
|
}
|
||||||
|
})
|
Loading…
Add table
Add a link
Reference in a new issue