forked from ProjectSegfault/website
use ghost api for blogposts
This commit is contained in:
parent
817cd937a6
commit
31df1859cb
@ -3,6 +3,7 @@ DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=your-db-password
|
||||
GHOST_API_KEY=your-ghost-api-key
|
||||
AUTH_CLIENT_ID=your-authentik-client-id
|
||||
AUTH_CLIENT_SECRET=your-authentik-client-secret
|
||||
AUTH_ISSUER=https://authentik-domain/application/o/app-name/
|
||||
|
@ -34,42 +34,6 @@ sequelize.define("Announcements", {
|
||||
}
|
||||
});
|
||||
|
||||
sequelize.define("Posts", {
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
allowNull: false
|
||||
},
|
||||
author: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
created: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
updated: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
},
|
||||
words: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
readingTime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
await sequelize.sync();
|
||||
|
9
src/lib/ghost.ts
Normal file
9
src/lib/ghost.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { env } from "$env/dynamic/private";
|
||||
|
||||
const fetchApi = async (action: string, additional?: string) => {
|
||||
const data = await fetch("https://blog.projectsegfau.lt/ghost/api/content/" + action + "/?key=" + env.GHOST_API_KEY + "&include=authors,tags&limit=all&formats=html,plaintext" + (additional ? additional : ""));
|
||||
|
||||
return data.json();
|
||||
};
|
||||
|
||||
export default fetchApi;
|
@ -2,5 +2,4 @@
|
||||
|
||||
<div class="col">
|
||||
<a href="/admin/announcements">Announcements</a>
|
||||
<a href="/admin/blog">Blog</a>
|
||||
</div>
|
@ -1,143 +0,0 @@
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import db from "$lib/db";
|
||||
import Joi from "joi";
|
||||
import { fail } from "@sveltejs/kit";
|
||||
|
||||
export const load = ( async () => {
|
||||
const Posts = db.model("Posts");
|
||||
|
||||
return {
|
||||
postTitles: await Posts.findAll({ attributes: ["title"] }).then((docs) => {
|
||||
const titles = docs.map((doc) => doc.get("title"));
|
||||
return titles;
|
||||
})
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions: Actions = {
|
||||
add: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const AddPostTypeSchema = Joi.object({
|
||||
title: Joi.string().required(),
|
||||
content: Joi.string().required(),
|
||||
tags: Joi.string().optional().allow(""),
|
||||
author: Joi.string().required()
|
||||
});
|
||||
|
||||
if (AddPostTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
|
||||
return fail(400, { addError: true, addMessage: String(AddPostTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
|
||||
} else {
|
||||
const Posts = db.model("Posts");
|
||||
|
||||
//@ts-ignore
|
||||
const words = formData.get("content")!.trim().split(/\s+/).length;
|
||||
|
||||
//@ts-ignore
|
||||
const tags = formData.get("tags") ? formData.get("tags").split(" ") : [];
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const data = {
|
||||
title: formData.get("title"),
|
||||
content: formData.get("content"),
|
||||
tags: tags,
|
||||
author: formData.get("author"),
|
||||
created: now,
|
||||
words: words,
|
||||
readingTime: Math.ceil(words / 225)
|
||||
};
|
||||
|
||||
if (await Posts.findOne({ where: { title: data.title } })) {
|
||||
return fail(409, { addError: true, addMessage: "A post with that title already exists." });
|
||||
} else {
|
||||
await Posts.create(data);
|
||||
|
||||
return { addSuccess: true, addMessage: "Your post has been posted." };
|
||||
}
|
||||
}
|
||||
},
|
||||
delete: async ({ request }) => {
|
||||
const Posts = db.model("Posts");
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const deleteFromDb = await Posts.destroy({ where: { title: formData.get("title") } });
|
||||
|
||||
if (!deleteFromDb) {
|
||||
return fail(404, { deleteError: true, deleteMessage: "A post with that title does not exist." });
|
||||
} else {
|
||||
return { deleteSuccess: true, deleteMessage: "Your post has been deleted." };
|
||||
}
|
||||
},
|
||||
edit: async ({ request }) => {
|
||||
const EditPostTypeSchema = Joi.object({
|
||||
title: Joi.string().required(),
|
||||
newTitle: Joi.string().optional().allow(""),
|
||||
content: Joi.string().optional().allow(""),
|
||||
tags: Joi.string().optional().allow(""),
|
||||
area: Joi.string().required().allow("title", "content", "tags")
|
||||
});
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
if (EditPostTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
|
||||
return fail(400, { editError: true, editMessage: String(EditPostTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
|
||||
} else {
|
||||
if (formData.get("area") === "title") {
|
||||
const Posts = db.model("Posts");
|
||||
|
||||
const updateOnDb = await Posts.update(
|
||||
{ title: formData.get("newTitle") },
|
||||
{ where: { title: formData.get("title") } }
|
||||
);
|
||||
|
||||
if (updateOnDb[0] === 0) {
|
||||
return fail(404, { editError: true, editMessage: "A post with that title does not exist." });
|
||||
} else {
|
||||
return { editSuccess: true, editMessage: "Your post has been edited." };
|
||||
}
|
||||
} else if (formData.get("area") === "content") {
|
||||
const Posts = db.model("Posts");
|
||||
|
||||
//@ts-ignore
|
||||
const words = formData.get("content")!.trim().split(/\s+/).length;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const updateonDb = await Posts.update(
|
||||
{
|
||||
content: formData.get("content"),
|
||||
words: words,
|
||||
readingTime: Math.ceil(words / 225),
|
||||
updated: now
|
||||
},
|
||||
{ where: { title: formData.get("title") } }
|
||||
);
|
||||
|
||||
if (updateonDb[0] === 0) {
|
||||
return fail(404, { editError: true, editMessage: "A post with that title does not exist." });
|
||||
} else {
|
||||
return { editSuccess: true, editMessage: "Your post has been edited." };
|
||||
}
|
||||
} else if (formData.get("area") === "tags") {
|
||||
const Posts = db.model("Posts");
|
||||
|
||||
//@ts-ignore
|
||||
const tags = formData.get("tags") ? formData.get("tags").split(" ") : [];
|
||||
|
||||
const updateOnDb = await Posts.update(
|
||||
{ tags: tags
|
||||
},
|
||||
{ where: { title: formData.get("title") } }
|
||||
);
|
||||
|
||||
if (updateOnDb[0] === 0) {
|
||||
return fail(404, { editError: true, editMessage: "A post with that title does not exist." });
|
||||
} else {
|
||||
return { editSuccess: true, editMessage: "Your post has been edited." };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { ActionData, PageData } from '.$/types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
export let form: ActionData;
|
||||
</script>
|
||||
|
||||
<h1>Add post</h1>
|
||||
|
||||
<form action="?/add" method="POST" class="col">
|
||||
<input type="text" name="title" placeholder="Title" required />
|
||||
<textarea
|
||||
name="content"
|
||||
placeholder="Content"
|
||||
rows="4"
|
||||
cols="25"
|
||||
required
|
||||
></textarea>
|
||||
<input type="text" name="tags" placeholder="Tags" />
|
||||
<input type="text" name="author" placeholder="Author" required />
|
||||
{#if form?.addSuccess}
|
||||
{form.addMessage}
|
||||
{/if}
|
||||
|
||||
{#if form?.addError}
|
||||
{form.addMessage}
|
||||
{/if}
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<h1>Delete post</h1>
|
||||
|
||||
<form action="?/delete" method="POST" class="col">
|
||||
<select name="title" required>
|
||||
{#each data.postTitles as title}
|
||||
<option value="{title}">{title}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if form?.deleteSuccess}
|
||||
{form.deleteMessage}
|
||||
{/if}
|
||||
|
||||
{#if form?.deleteError}
|
||||
{form.deleteMessage}
|
||||
{/if}
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<h1>Edit post</h1>
|
||||
|
||||
<form action="?/edit" method="POST" class="col">
|
||||
<select name="title" required>
|
||||
<option disabled>Post title</option>
|
||||
{#each data.postTitles as title}
|
||||
<option value="{title}">{title}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select name="area">
|
||||
<option disabled>Area to change</option>
|
||||
<option value="title">Title</option>
|
||||
<option value="content">Content</option>
|
||||
<option value="tags">Tags</option>
|
||||
</select>
|
||||
<input type="text" name="newTitle" placeholder="New title" />
|
||||
<textarea
|
||||
name="content"
|
||||
placeholder="New content"
|
||||
rows="4"
|
||||
cols="25"
|
||||
></textarea>
|
||||
<input type="text" name="tags" placeholder="New tags" />
|
||||
{#if form?.editSuccess}
|
||||
{form.editMessage}
|
||||
{/if}
|
||||
|
||||
{#if form?.editError}
|
||||
{form.editMessage}
|
||||
{/if}
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
@ -1,20 +1,10 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import db from "$lib/db";
|
||||
import fetchApi from "$lib/ghost";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const Posts = db.model("Posts");
|
||||
export const load = (async () => {
|
||||
const data = await fetchApi("posts");
|
||||
|
||||
const posts = await Posts.findAll().then((docs) => {
|
||||
return docs.map((doc) => doc.get());
|
||||
});
|
||||
|
||||
if (posts.length === 0 || posts[0] === undefined) {
|
||||
return {
|
||||
posts: []
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
posts: posts.sort((a, b) => b["created"] - a["created"])
|
||||
}
|
||||
}
|
||||
};
|
||||
return {
|
||||
posts: data.posts
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
@ -43,19 +43,21 @@
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="i-fa6-solid:tags" />
|
||||
{#each post.tags as tag}
|
||||
<a href="/blog/tags/{tag}" class="no-underline">{tag}</a>
|
||||
<a href="/blog/tags/{tag.slug}" class="no-underline">{tag.name}</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<a href="/blog/authors/{post.author}" class="flex items-center gap-2 no-underline"><div class="i-fa6-solid:user" />{post.author}</a>
|
||||
{#each post.authors as author}
|
||||
<a href="/blog/authors/{author.slug}" class="flex items-center gap-2 no-underline"><div class="i-fa6-solid:user" />{author.name}</a>
|
||||
{/each}
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:calendar" /> {dayjs
|
||||
.unix(post.created)
|
||||
(post.published_at)
|
||||
.format("ddd, DD MMM YYYY HH:mm")}</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:pencil" /> {post.words} words</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.readingTime} minute read</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:pencil" /> {post.plaintext.trim().split(/\s+/).length} words</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.reading_time} minute read</span>
|
||||
</div>
|
||||
<span>{post.content.split(" ").slice(0, 20).join(" ") + "..."}</span>
|
||||
<a href="/blog/{post.title}">Read more...</a>
|
||||
<span>{post.plaintext.split(" ").slice(0, 20).join(" ") + "..."}</span>
|
||||
<a href="/blog/{post.slug}">Read more...</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
@ -1,27 +1,10 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import db from "$lib/db";
|
||||
import { compile } from "mdsvex";
|
||||
import fetchApi from "$lib/ghost";
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const Posts = db.model("Posts");
|
||||
export const load = (async ({ params }) => {
|
||||
const data = await fetchApi("posts/slug/" + params.title);
|
||||
|
||||
const data = await Posts.findAll({
|
||||
where: {
|
||||
title: params.title
|
||||
}
|
||||
}).then((docs) => {
|
||||
return docs.map((doc) => doc.get());
|
||||
});
|
||||
|
||||
if (data.length === 0 || data[0] === undefined) {
|
||||
return {
|
||||
post: {},
|
||||
content: {}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
post: data[0],
|
||||
content: compile(data[0].content).then((res) => res?.code)
|
||||
}
|
||||
}
|
||||
};
|
||||
return {
|
||||
post: data.posts[0]
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
@ -11,16 +11,18 @@
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="i-fa6-solid:tags" />
|
||||
{#each data.post.tags as tag}
|
||||
<a href="/blog/tags/{tag}" class="no-underline">{tag}</a>
|
||||
<a href="/blog/tags/{tag.slug}" class="no-underline">{tag.name}</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<a href="/blog/authors/{data.post.author}" class="flex items-center gap-2 no-underline"><div class="i-fa6-solid:user" />{data.post.author}</a>
|
||||
{#each data.post.authors as author}
|
||||
<a href="/blog/authors/{author.slug}" class="flex items-center gap-2 no-underline"><div class="i-fa6-solid:user" />{author.name}</a>
|
||||
{/each}
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:calendar" /> {dayjs
|
||||
.unix(data.post.created)
|
||||
(data.post.published_at)
|
||||
.format("ddd, DD MMM YYYY HH:mm")}</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:pencil" /> {data.post.words} words</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {data.post.readingTime} minute read</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:pencil" /> {data.post.plaintext.trim().split(/\s+/).length} words</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {data.post.reading_time} minute read</span>
|
||||
</div>
|
||||
{@html data.content}
|
||||
{@html data.post.html}
|
||||
</div>
|
@ -1,23 +1,11 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import db from "$lib/db";
|
||||
import fetchApi from "$lib/ghost";
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const Posts = db.model("Posts");
|
||||
export const load: PageServerLoad = async () => {
|
||||
const data = await fetchApi("authors");
|
||||
|
||||
const data = await Posts.findAll({
|
||||
attributes: ["author"]
|
||||
})
|
||||
|
||||
if (data.length === 0 || data[0] === undefined) {
|
||||
return {
|
||||
authors: []
|
||||
}
|
||||
} else {
|
||||
const authors = data.map((post) => post["author"]);
|
||||
const uniqueAuthors = [...new Set(authors)];
|
||||
|
||||
return {
|
||||
authors: uniqueAuthors
|
||||
}
|
||||
}
|
||||
return {
|
||||
authors: data.authors
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,6 @@
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each data.authors as author}
|
||||
<a href="/blog/authors/{author}" class="bg-secondary w-fit p-2 rounded-2 no-underline">{author}</a>
|
||||
<a href="/blog/authors/{author.slug}" class="bg-secondary w-fit p-2 rounded-2 no-underline">{author.name}</a>
|
||||
{/each}
|
||||
</div>
|
@ -1,26 +1,12 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import db from "$lib/db";
|
||||
import fetchApi from "$lib/ghost";
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const Posts = db.model("Posts");
|
||||
const data = await fetchApi("posts", "&filter=author:" + params.author);
|
||||
|
||||
const data = await Posts.findAll({
|
||||
where: {
|
||||
author: params.author
|
||||
}
|
||||
}).then((docs) => {
|
||||
return docs.map((doc) => doc.get());
|
||||
});
|
||||
return {
|
||||
posts: data.posts,
|
||||
authorName: params.author
|
||||
};
|
||||
};
|
||||
|
||||
if (data.length === 0 || data[0] === undefined) {
|
||||
return {
|
||||
posts: [],
|
||||
authorName: params.author
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
posts: data,
|
||||
authorName: params.author
|
||||
}
|
||||
}
|
||||
};
|
@ -15,19 +15,21 @@
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="i-fa6-solid:tags" />
|
||||
{#each post.tags as tag}
|
||||
<a href="/blog/tags/{tag}" class="no-underline">{tag}</a>
|
||||
<a href="/blog/tags/{tag.slug}" class="no-underline">{tag.name}</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<a href="/blog/authors/{post.author}" class="flex items-center gap-2 no-underline"><div class="i-fa6-solid:user" />{post.author}</a>
|
||||
{#each post.authors as author}
|
||||
<a href="/blog/authors/{author.slug}" class="flex items-center gap-2 no-underline"><div class="i-fa6-solid:user" />{author.name}</a>
|
||||
{/each}
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:calendar" /> {dayjs
|
||||
.unix(post.created)
|
||||
(post.published_at)
|
||||
.format("ddd, DD MMM YYYY HH:mm")}</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:pencil" /> {post.words} words</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.readingTime} minute read</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:pencil" /> {post.plaintext.trim().split(/\s+/).length} words</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.reading_time} minute read</span>
|
||||
</div>
|
||||
<span>{post.content.split(" ").slice(0, 20).join(" ") + "..."}</span>
|
||||
<a href="/blog/{post.title}">Read more...</a>
|
||||
<span>{post.plaintext.split(" ").slice(0, 20).join(" ") + "..."}</span>
|
||||
<a href="/blog/{post.slug}">Read more...</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
@ -1,20 +1,10 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import db from "$lib/db";
|
||||
import fetchApi from "$lib/ghost";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const Posts = db.model("Posts");
|
||||
const data = await fetchApi("tags");
|
||||
|
||||
const data = await Posts.findAll({ attributes: ["tags"] })
|
||||
|
||||
if (data.length === 0 || data[0] === undefined) {
|
||||
return {
|
||||
tags: []
|
||||
}
|
||||
} else {
|
||||
const tags = data.map((post) => post["tags"]).flat();
|
||||
const uniqueTags = [...new Set(tags)];
|
||||
return {
|
||||
tags: uniqueTags
|
||||
}
|
||||
return {
|
||||
tags: data.tags
|
||||
}
|
||||
};
|
||||
|
@ -7,6 +7,6 @@
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each data.tags as tag}
|
||||
<a href="/blog/tags/{tag}" class="bg-secondary w-fit p-2 rounded-2 no-underline">{tag}</a>
|
||||
<a href="/blog/tags/{tag.slug}" class="bg-secondary w-fit p-2 rounded-2 no-underline">{tag.name}</a>
|
||||
{/each}
|
||||
</div>
|
@ -1,29 +1,11 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import db from "$lib/db";
|
||||
import { Op } from "sequelize";
|
||||
import fetchApi from "$lib/ghost";
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const Posts = db.model("Posts");
|
||||
const data = await fetchApi("posts", "&filter=tags:" + params.tag);
|
||||
|
||||
const data = await Posts.findAll({
|
||||
where: {
|
||||
tags: {
|
||||
[Op.contains]: [params.tag]
|
||||
}
|
||||
}
|
||||
}).then((docs) => {
|
||||
return docs.map((doc) => doc.get());
|
||||
});
|
||||
|
||||
if (data.length === 0 || data[0] === undefined) {
|
||||
return {
|
||||
posts: [],
|
||||
tagName: params.tag
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
posts: data,
|
||||
tagName: params.tag
|
||||
}
|
||||
return {
|
||||
posts: data.posts,
|
||||
tagName: params.tag
|
||||
}
|
||||
};
|
@ -15,19 +15,21 @@
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="i-fa6-solid:tags" />
|
||||
{#each post.tags as tag}
|
||||
<a href="/blog/tags/{tag}" class="no-underline">{tag}</a>
|
||||
<a href="/blog/tags/{tag.slug}" class="no-underline">{tag.name}</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<a href="/blog/authors/{post.author}" class="flex items-center gap-2 no-underline"><div class="i-fa6-solid:user" />{post.author}</a>
|
||||
{#each post.authors as author}
|
||||
<a href="/blog/authors/{author.slug}" class="flex items-center gap-2 no-underline"><div class="i-fa6-solid:user" />{author.name}</a>
|
||||
{/each}
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:calendar" /> {dayjs
|
||||
.unix(post.created)
|
||||
(post.published_at)
|
||||
.format("ddd, DD MMM YYYY HH:mm")}</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:pencil" /> {post.words} words</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.readingTime} minute read</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:pencil" /> {post.plaintext.trim().split(/\s+/).length} words</span>
|
||||
<span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.reading_time} minute read</span>
|
||||
</div>
|
||||
<span>{post.content.split(" ").slice(0, 20).join(" ") + "..."}</span>
|
||||
<a href="/blog/{post.title}">Read more...</a>
|
||||
<span>{post.plaintext.split(" ").slice(0, 20).join(" ") + "..."}</span>
|
||||
<a href="/blog/{post.slug}">Read more...</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user