more blog stuff
This commit is contained in:
parent
2a48d4b6da
commit
44543b5992
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
81
components/blog-archive.vue
Normal file
81
components/blog-archive.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
16
lib/types/post.ts
Normal 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[];
|
||||
}
|
@ -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');
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
16
pages/blog/archive/index.vue
Normal file
16
pages/blog/archive/index.vue
Normal 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
22
pages/blog/tags/[tag].vue
Normal 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>
|
@ -140,7 +140,7 @@ const linksList = [
|
||||
{
|
||||
name: 'Web apps',
|
||||
icon: 'icon-controller-classic',
|
||||
href: '/apps',
|
||||
href: 'https://lunasqu.ee/apps',
|
||||
},
|
||||
{
|
||||
name: 'GnuPG Public Key',
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user