more blog stuff

This commit is contained in:
Evert Prants 2022-10-16 14:04:03 +03:00
parent 2a48d4b6da
commit 44543b5992
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
17 changed files with 336 additions and 45 deletions

View File

@ -222,4 +222,53 @@
display: inline-block;
}
}
&-archives {
margin: 50px 0;
&__year {
margin-bottom: 1em;
font-size: 0.85em;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 2px;
margin-left: 5px;
line-height: 1em;
a {
color: #999;
text-shadow: 0 1px #fff;
font-weight: bold;
}
}
&__posts {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
&__time {
color: #999 !important;
text-decoration: none;
font-size: 0.85em;
line-height: 1em;
margin-bottom: 0.5em;
display: block;
}
&__post {
padding: 20px;
background-color: #fff;
header {
display: flex;
flex-direction: column;
height: 100%;
h1 {
margin-top: auto;
}
}
}
}
}

View File

@ -0,0 +1,81 @@
<template>
<div class="blog-archives" v-for="entry in yearGroup">
<div class="blog-archives__year">
<NuxtLink :to="'/blog/archive/' + entry.year">{{ entry.year }}</NuxtLink>
</div>
<div class="blog-archives__posts">
<article class="blog-archives__post" v-for="post in entry.posts">
<header>
<NuxtLink
:to="'/blog/' + post.fullSlug"
class="blog-archives__time"
>{{ getStamp(post) }}</NuxtLink
>
<h1>
<NuxtLink :to="'/blog/' + post.fullSlug">{{ post.title }}</NuxtLink>
</h1>
</header>
</article>
</div>
</div>
</template>
<script setup lang="ts">
import { BlogPost } from '~~/lib/types/post';
interface Archive {
year: number;
posts: BlogPost[];
}
const props = defineProps<{ posts: BlogPost[] }>();
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
const yearGroup = computed<Archive[]>(() => {
const groups: Archive[] = [];
props.posts
.sort((a, b) =>
new Date(b.date)
.toISOString()
.localeCompare(new Date(a.date).toISOString(), 'en', { numeric: true })
)
.forEach((post) => {
const date = new Date(post.date);
const year = date.getFullYear();
const contains = groups.find((item) => item.year === year);
if (contains) {
contains.posts.push(post);
return;
}
groups.push({
year,
posts: [post],
});
});
return groups;
});
const getStamp = (post: BlogPost) => {
const date = new Date(post.date);
return `${monthNames[date.getMonth()]} ${date.getDate()}`;
};
</script>

View File

@ -1,11 +1,11 @@
<template>
<article class="blog-post">
<div class="blog-post__meta">
<a :href="'/blog/' + post.fullSlug">
<NuxtLink :to="'/blog/' + post.fullSlug">
<time :datetime="new Date(post.date).toISOString()">
{{ post.date }}
</time>
</a>
</NuxtLink>
</div>
<div class="blog-post__inner">
<header class="blog-post__title">
@ -14,18 +14,18 @@
{{ post.title }}
</template>
<template v-else>
<a :href="'/blog/' + post.fullSlug">{{ post.title }}</a>
<NuxtLink :to="'/blog/' + post.fullSlug">{{ post.title }}</NuxtLink>
</template>
</h1>
</header>
<div class="blog-post__content" v-html="post.html"></div>
<div class="blog-post__footer">
<div class="blog-post__tags">
<a
<NuxtLink
v-for="tag of post.tags"
:href="'/blog/tags/' + tag"
:to="'/blog/tags/' + tag"
class="blog-post__tag"
>#{{ tag }}</a
>#{{ tag }}</NuxtLink
>
</div>
</div>
@ -34,5 +34,7 @@
</template>
<script setup lang="ts">
defineProps<{ post: any; detail?: boolean }>();
import { BlogPost } from '~~/lib/types/post';
defineProps<{ post: BlogPost; detail?: boolean }>();
</script>

View File

@ -3,8 +3,10 @@
<h2>{{ title }}</h2>
<ul>
<li v-for="item in list">
<a :href="item.href" v-bind:target="item.blank ? '_blank' : undefined"
><span :class="item.icon"></span>{{ item.name }}</a
<NuxtLink
:to="item.href"
v-bind:target="item.blank ? '_blank' : undefined"
><span :class="item.icon"></span>{{ item.name }}</NuxtLink
>
</li>
</ul>

View File

@ -1,7 +1,16 @@
<template>
<div class="blog">
<Head>
<Meta property="og:type" content="website" />
<Meta property="og:title" content="Evert's Blog" />
<Meta property="og:url" content="https://lunasqu.ee/blog/index.html" />
<Meta property="og:site_name" content="Evert's Blog" />
<Meta property="og:locale" content="en_US" />
<Meta property="article:author" content="Evert Prants" />
<Meta name="twitter:card" content="summary" />
</Head>
<header class="blog__header">
<h1><a href="/blog">Blog</a></h1>
<h1><NuxtLink to="/blog">Blog</NuxtLink></h1>
</header>
<section class="blog__content">
@ -13,18 +22,18 @@
<BlogSidebar title="Tags">
<ul>
<li v-for="tag of tags">
<a :href="'/tags/' + tag.name">{{ tag.name }}</a>
<NuxtLink :to="'/blog/tags/' + tag.name">{{ tag.name }}</NuxtLink>
</li>
</ul>
</BlogSidebar>
<BlogSidebar title="Tag cloud">
<div class="tag-cloud">
<a
<NuxtLink
v-for="tag of tags"
:href="'/blog/tags/' + tag.name"
:to="'/blog/tags/' + tag.name"
:style="{ fontSize: getFontSize(tag) }"
>{{ tag.name }}</a
>{{ tag.name }}</NuxtLink
>
</div>
</BlogSidebar>
@ -32,7 +41,7 @@
<BlogSidebar title="Archive">
<ul>
<li v-for="archive of monthList">
<a :href="archive.href">{{ archive.name }}</a>
<NuxtLink :href="archive.href">{{ archive.name }}</NuxtLink>
</li>
</ul>
</BlogSidebar>
@ -42,6 +51,8 @@
</template>
<script setup lang="ts">
import { BlogPostTag } from '~~/lib/types/post';
const { data: tags } = await useFetch('/api/blog/tags');
const { data: archive } = await useFetch('/api/blog/archive');
@ -83,7 +94,7 @@ const monthList = computed(() => {
const minTag = computed(() =>
tags.value.reduce<number>(
(min, current) => (min > current.count ? current.count : min),
100
1000
)
);
@ -98,7 +109,7 @@ function convertRange(value: number, r1: number[], r2: number[]) {
return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0];
}
const getFontSize = (tag: any): string => {
const getFontSize = (tag: BlogPostTag): string => {
return convertRange(tag.count, [minTag.value, maxTag.value], [10, 20]) + 'px';
};
</script>

View File

@ -4,6 +4,7 @@ import yaml from 'yaml';
import { marked } from 'marked';
import hljs from 'highlight.js';
import { getDateObject } from '../utils/date-object';
import { BlogPost, BlogPostTag } from '../types/post';
const dir = join('content', 'blog');
@ -14,11 +15,16 @@ marked.use({
const created = hljs.highlight(code, { language }).value;
return `<div class="codeblock"><pre><code class="hljs language-${language}">${created}</code></pre></div>`;
},
link: (href: string, title: string, text: string): string => {
return `<a href="${href}" rel="nofollow" target="_blank"${
title ? ` title="${title}"` : ''
}>${text}</a>`;
},
},
});
export async function readBlogPosts(
condition?: (body: any) => boolean,
condition?: (body: BlogPost) => boolean,
render = true,
includeContent = true
) {
@ -45,10 +51,10 @@ export async function readBlogPosts(
export async function readBlogPost(
slug: string,
condition?: (body: any) => boolean,
condition?: (body: BlogPost) => boolean,
render = true,
includeContent = true
): Promise<any> {
): Promise<BlogPost[]> {
const decoded = decodeURIComponent(slug);
if (!slug || decoded.includes('/') || decoded.includes('.'))
throw new Error('Invalid post slug');
@ -80,9 +86,9 @@ export async function readBlogPost(
return content;
}
export async function getTags() {
export async function getTags(): Promise<BlogPostTag[]> {
const posts = await readBlogPosts(undefined, false, false);
const obj = [];
const obj: BlogPostTag[] = [];
for (const post of posts) {
for (const tag of post.tags || []) {
@ -104,8 +110,8 @@ export async function getTags() {
return obj;
}
export async function getArchiveTree() {
const posts = await readBlogPosts(undefined, false, false);
export async function getArchiveTree(condition?: (body: BlogPost) => boolean) {
const posts = await readBlogPosts(condition, false, false);
const obj = {};
for (const post of posts) {

16
lib/types/post.ts Normal file
View File

@ -0,0 +1,16 @@
export interface BlogPost {
date: string;
title: string;
tags: string[];
file: string;
slug: string;
fullSlug: string;
markdown: string;
html: string;
}
export interface BlogPostTag {
name: string;
count: number;
posts: string[];
}

View File

@ -1,4 +1,6 @@
export function getDateObject(post) {
import { BlogPost } from '../types/post';
export function getDateObject(post: BlogPost) {
const date = new Date(post.date);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');

View File

@ -1,10 +1,44 @@
<template>
<NuxtLayout name="blog">
<Head>
<Meta name="description" :content="preview" />
<Meta property="og:type" content="article" />
<Meta property="og:title" content="Self-hosting, Part 1" />
<Meta
property="og:url"
:content="'https://lunasqu.ee/blog/' + post.fullSlug"
/>
<Meta property="og:site_name" content="Evert's Blog" />
<Meta property="og:description" :content="preview" />
<Meta property="og:locale" content="en_US" />
<Meta property="article:published_time" :content="isostamp" />
<Meta property="article:modified_time" :content="isostamp" />
<Meta property="article:author" content="Evert Prants" />
<Meta v-for="tag in post.tags" property="article:tag" :content="tag" />
<Meta name="twitter:card" content="summary" />
</Head>
<BlogPost :post="post" :detail="true" />
</NuxtLayout>
</template>
<script setup lang="ts">
import type { BlogPost } from '~~/lib/types/post';
const route = useRoute();
const { data: post } = await useFetch(`/api/blog/${route.params.slug}`);
const { data: post, refresh } = await useFetch<BlogPost>(
`/api/blog/${route.params.slug}`
);
const isostamp = computed(() => new Date(post.value.date).toISOString());
const preview = computed(() =>
post.value.html
.replace(/<[^>]*>?/gm, '')
.replace('\n', ' ')
.substring(0, 120)
);
onMounted(() => {
refresh();
});
</script>

View File

@ -1,14 +1,24 @@
<template>
<NuxtLayout name="blog">
<template v-for="post of posts">
<BlogPost :post="post" :detail="false" />
</template>
<BlogArchive :posts="posts" />
</NuxtLayout>
</template>
<script setup lang="ts">
import type { BlogPost } from '~~/lib/types/post';
const route = useRoute();
const { data: posts } = await useFetch(
`/api/blog?year=${route.params.year}&month=${route.params.month}&day=${route.params.day}`
);
const { data: posts, refresh } = await useFetch<BlogPost[]>(`/api/blog`, {
params: {
year: route.params.year,
month: route.params.month,
day: route.params.day,
body: false,
render: false,
},
});
onMounted(() => {
refresh();
});
</script>

View File

@ -1,14 +1,23 @@
<template>
<NuxtLayout name="blog">
<template v-for="post of posts">
<BlogPost :post="post" :detail="false" />
</template>
<BlogArchive :posts="posts" />
</NuxtLayout>
</template>
<script setup lang="ts">
import type { BlogPost } from '~~/lib/types/post';
const route = useRoute();
const { data: posts } = await useFetch(
`/api/blog?year=${route.params.year}&month=${route.params.month}`
);
const { data: posts, refresh } = await useFetch<BlogPost[]>(`/api/blog`, {
params: {
year: route.params.year,
month: route.params.month,
body: false,
render: false,
},
});
onMounted(() => {
refresh();
});
</script>

View File

@ -1,12 +1,22 @@
<template>
<NuxtLayout name="blog">
<template v-for="post of posts">
<BlogPost :post="post" :detail="false" />
</template>
<BlogArchive :posts="posts" />
</NuxtLayout>
</template>
<script setup lang="ts">
import type { BlogPost } from '~~/lib/types/post';
const route = useRoute();
const { data: posts } = await useFetch(`/api/blog?year=${route.params.year}`);
const { data: posts, refresh } = await useFetch<BlogPost[]>(`/api/blog`, {
params: {
year: route.params.year,
body: false,
render: false,
},
});
onMounted(() => {
refresh();
});
</script>

View File

@ -0,0 +1,16 @@
<template>
<NuxtLayout name="blog">
<BlogArchive :posts="posts" />
</NuxtLayout>
</template>
<script setup lang="ts">
import type { BlogPost } from '~~/lib/types/post';
const { data: posts } = await useFetch<BlogPost[]>(`/api/blog`, {
params: {
body: false,
render: false,
},
});
</script>

22
pages/blog/tags/[tag].vue Normal file
View File

@ -0,0 +1,22 @@
<template>
<NuxtLayout name="blog">
<BlogArchive :posts="posts" />
</NuxtLayout>
</template>
<script setup lang="ts">
import type { BlogPost } from '~~/lib/types/post';
const route = useRoute();
const { data: posts, refresh } = await useFetch<BlogPost[]>(`/api/blog`, {
params: {
tag: route.params.tag,
body: false,
render: false,
},
});
onMounted(() => {
refresh();
});
</script>

View File

@ -140,7 +140,7 @@ const linksList = [
{
name: 'Web apps',
icon: 'icon-controller-classic',
href: '/apps',
href: 'https://lunasqu.ee/apps',
},
{
name: 'GnuPG Public Key',

View File

@ -1,5 +1,16 @@
import { getArchiveTree } from '~~/lib/blog/read-posts';
export default defineEventHandler(async (event) => {
return getArchiveTree();
const query = getQuery(event);
const include = (content) => {
if (query.tag) {
if (!content.tags?.length || !content.tags.includes(query.tag)) {
return false;
}
}
return true;
};
return getArchiveTree(include);
});

View File

@ -23,8 +23,18 @@ export default defineEventHandler(async (event) => {
}
}
if (query.tag) {
if (!content.tags?.length || !content.tags.includes(query.tag)) {
return false;
}
}
return true;
};
return readBlogPosts(include);
return readBlogPosts(
include,
query.render !== 'false',
query.body !== 'false'
);
});