use ghost api for blogposts

This commit is contained in:
Akis 2023-01-01 01:21:53 +02:00
parent 817cd937a6
commit 31df1859cb
Signed by untrusted user: akis
GPG Key ID: 267BF5C6677944ED
18 changed files with 84 additions and 408 deletions

View File

@ -3,6 +3,7 @@ DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_USERNAME=postgres DB_USERNAME=postgres
DB_PASSWORD=your-db-password DB_PASSWORD=your-db-password
GHOST_API_KEY=your-ghost-api-key
AUTH_CLIENT_ID=your-authentik-client-id AUTH_CLIENT_ID=your-authentik-client-id
AUTH_CLIENT_SECRET=your-authentik-client-secret AUTH_CLIENT_SECRET=your-authentik-client-secret
AUTH_ISSUER=https://authentik-domain/application/o/app-name/ AUTH_ISSUER=https://authentik-domain/application/o/app-name/

View File

@ -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 { try {
await sequelize.authenticate(); await sequelize.authenticate();
await sequelize.sync(); await sequelize.sync();

9
src/lib/ghost.ts Normal file
View 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;

View File

@ -2,5 +2,4 @@
<div class="col"> <div class="col">
<a href="/admin/announcements">Announcements</a> <a href="/admin/announcements">Announcements</a>
<a href="/admin/blog">Blog</a>
</div> </div>

View File

@ -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." };
}
}
}
}
}

View File

@ -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>

View File

@ -1,20 +1,10 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import db from "$lib/db"; import fetchApi from "$lib/ghost";
export const load: PageServerLoad = async () => { export const load = (async () => {
const Posts = db.model("Posts"); const data = await fetchApi("posts");
const posts = await Posts.findAll().then((docs) => { return {
return docs.map((doc) => doc.get()); posts: data.posts
}); };
}) satisfies PageServerLoad;
if (posts.length === 0 || posts[0] === undefined) {
return {
posts: []
}
} else {
return {
posts: posts.sort((a, b) => b["created"] - a["created"])
}
}
};

View File

@ -43,19 +43,21 @@
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<div class="i-fa6-solid:tags" /> <div class="i-fa6-solid:tags" />
{#each post.tags as tag} {#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} {/each}
</div> </div>
{/if} {/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 <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> .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: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.readingTime} minute read</span> <span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.reading_time} minute read</span>
</div> </div>
<span>{post.content.split(" ").slice(0, 20).join(" ") + "..."}</span> <span>{post.plaintext.split(" ").slice(0, 20).join(" ") + "..."}</span>
<a href="/blog/{post.title}">Read more...</a> <a href="/blog/{post.slug}">Read more...</a>
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -1,27 +1,10 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import db from "$lib/db"; import fetchApi from "$lib/ghost";
import { compile } from "mdsvex";
export const load: PageServerLoad = async ({ params }) => { export const load = (async ({ params }) => {
const Posts = db.model("Posts"); const data = await fetchApi("posts/slug/" + params.title);
const data = await Posts.findAll({ return {
where: { post: data.posts[0]
title: params.title };
} }) satisfies PageServerLoad;
}).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)
}
}
};

View File

@ -11,16 +11,18 @@
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<div class="i-fa6-solid:tags" /> <div class="i-fa6-solid:tags" />
{#each data.post.tags as tag} {#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} {/each}
</div> </div>
{/if} {/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 <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> .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: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.readingTime} minute read</span> <span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {data.post.reading_time} minute read</span>
</div> </div>
{@html data.content} {@html data.post.html}
</div> </div>

View File

@ -1,23 +1,11 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import db from "$lib/db"; import fetchApi from "$lib/ghost";
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async () => {
const Posts = db.model("Posts"); const data = await fetchApi("authors");
const data = await Posts.findAll({ return {
attributes: ["author"] authors: data.authors
}) };
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
}
}
}; };

View File

@ -7,6 +7,6 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{#each data.authors as author} {#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} {/each}
</div> </div>

View File

@ -1,26 +1,12 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import db from "$lib/db"; import fetchApi from "$lib/ghost";
export const load: PageServerLoad = async ({ params }) => { 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({ return {
where: { posts: data.posts,
author: params.author authorName: params.author
} };
}).then((docs) => {
return docs.map((doc) => doc.get());
});
if (data.length === 0 || data[0] === undefined) {
return {
posts: [],
authorName: params.author
}
} else {
return {
posts: data,
authorName: params.author
}
}
}; };

View File

@ -15,19 +15,21 @@
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<div class="i-fa6-solid:tags" /> <div class="i-fa6-solid:tags" />
{#each post.tags as tag} {#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} {/each}
</div> </div>
{/if} {/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 <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> .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: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.readingTime} minute read</span> <span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.reading_time} minute read</span>
</div> </div>
<span>{post.content.split(" ").slice(0, 20).join(" ") + "..."}</span> <span>{post.plaintext.split(" ").slice(0, 20).join(" ") + "..."}</span>
<a href="/blog/{post.title}">Read more...</a> <a href="/blog/{post.slug}">Read more...</a>
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -1,20 +1,10 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import db from "$lib/db"; import fetchApi from "$lib/ghost";
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
const Posts = db.model("Posts"); const data = await fetchApi("tags");
const data = await Posts.findAll({ attributes: ["tags"] }) return {
tags: data.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
}
} }
}; };

View File

@ -7,6 +7,6 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{#each data.tags as tag} {#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} {/each}
</div> </div>

View File

@ -1,29 +1,11 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
import db from "$lib/db"; import fetchApi from "$lib/ghost";
import { Op } from "sequelize";
export const load: PageServerLoad = async ({ params }) => { 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({ return {
where: { posts: data.posts,
tags: { tagName: params.tag
[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
}
} }
}; };

View File

@ -15,19 +15,21 @@
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<div class="i-fa6-solid:tags" /> <div class="i-fa6-solid:tags" />
{#each post.tags as tag} {#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} {/each}
</div> </div>
{/if} {/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 <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> .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: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.readingTime} minute read</span> <span class="flex items-center gap-2"><div class="i-fa6-solid:book-open-reader" /> {post.reading_time} minute read</span>
</div> </div>
<span>{post.content.split(" ").slice(0, 20).join(" ") + "..."}</span> <span>{post.plaintext.split(" ").slice(0, 20).join(" ") + "..."}</span>
<a href="/blog/{post.title}">Read more...</a> <a href="/blog/{post.slug}">Read more...</a>
</div> </div>
{/each} {/each}
</div> </div>