-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/Mastodon-DE/joinmastodon
- Loading branch information
Showing
14 changed files
with
409 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { access, opendir, readFile } from "node:fs/promises"; | ||
import { join } from "node:path"; | ||
import type { FrontMatter, Post } from "~/types/posts"; | ||
import { | ||
getMarkdownRenderer, | ||
parseFrontMatter, | ||
stripFrontMatter, | ||
} from "~/utils/markdown"; | ||
|
||
export const usePost = async (path: string): Promise<Post | null> => { | ||
if (import.meta.client) { | ||
return null; | ||
} | ||
|
||
const filePath = join(process.cwd(), "content", `${path}.md`); | ||
|
||
if ( | ||
!(await access(filePath) | ||
.then(() => true) | ||
.catch(() => false)) | ||
) { | ||
return null; | ||
} | ||
|
||
const contents = await readFile(filePath, "utf8"); | ||
|
||
const header = parseFrontMatter<FrontMatter>(contents); | ||
|
||
if (!header) { | ||
return null; | ||
} | ||
|
||
const renderedBody = await getMarkdownRenderer().then((renderer) => | ||
renderer.render(stripFrontMatter(contents)), | ||
); | ||
|
||
return { | ||
author: { | ||
handle: header.author_handle, | ||
image: header.author_image, | ||
name: header.author, | ||
}, | ||
private: header.private === "true", | ||
content: renderedBody, | ||
created_at: new Date(Number(header.created_at || 0)).toISOString(), | ||
description: header.description, | ||
image: header.image, | ||
title: header.title, | ||
path, | ||
}; | ||
}; | ||
|
||
export const usePostList = async (): Promise<Post[] | null> => { | ||
if (import.meta.client) { | ||
return null; | ||
} | ||
|
||
const directoryPath = join(process.cwd(), "content"); | ||
|
||
const dir = await opendir(directoryPath, { | ||
recursive: true, | ||
}); | ||
|
||
const files = []; | ||
|
||
for await (const dirent of dir) { | ||
if (dirent.isFile() && dirent.name.endsWith(".md")) { | ||
files.push( | ||
// Remove process.cwd() and .md from the path | ||
join(dirent.path, dirent.name.replace(".md", "")).replace( | ||
join(process.cwd(), "content"), | ||
"", | ||
), | ||
); | ||
} | ||
} | ||
|
||
const results: Post[] = []; | ||
|
||
for (const file of files) { | ||
const post = await usePost(file.replace(".md", "")); | ||
if (post) { | ||
results.push(post); | ||
} | ||
} | ||
|
||
return results.sort( | ||
(a, b) => | ||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,64 +1,99 @@ | ||
<script setup lang="ts"> | ||
const { locale } = useI18n(); | ||
import type { Post } from "~/types/posts"; | ||
let post: { | ||
author: string; | ||
created_at: string; | ||
description: string; | ||
image: string; | ||
title: string; | ||
} | null; | ||
try { | ||
post = await queryContent( | ||
`/blog/${(useRoute().params.path as string[]).join("/") ?? ""}` | ||
) | ||
.locale(locale.value) | ||
.findOne(); | ||
} catch { | ||
post = null; | ||
const filePath = (useRoute().params.path as string[]).join("/"); | ||
const { data: post } = await useFetch<Post>( | ||
`/api/article?path=${encodeURIComponent(`/${filePath}`)}` | ||
); | ||
if (!post.value) { | ||
throw createError({ | ||
statusCode: 404, | ||
message: "Post not found", | ||
}); | ||
} | ||
useSchemaOrg([ | ||
defineArticle({ | ||
author: post?.author, | ||
datePublished: post?.created_at, | ||
image: post?.title, | ||
description: post?.description, | ||
inLanguage: locale.value, | ||
thumbnailUrl: post?.image, | ||
author: post.value.author.name, | ||
datePublished: post.value.created_at, | ||
image: post.value.image, | ||
description: post.value.description, | ||
inLanguage: "en-US", | ||
thumbnailUrl: post.value.image, | ||
}), | ||
]); | ||
useServerSeoMeta({ | ||
title: post?.title, | ||
description: post?.description, | ||
ogImage: post?.image, | ||
title: post.value.title, | ||
ogTitle: post.value.title, | ||
author: post.value.author.name, | ||
description: post.value.description, | ||
ogDescription: post.value.description, | ||
ogImage: post.value.image, | ||
twitterCard: "summary_large_image", | ||
}); | ||
let body = post.value.content; | ||
// Fix for optimizing images during prerendering | ||
const img = useImage(); | ||
// Find all links of type /_ipx/ in body | ||
const ipxLinks = body.match(/\/_ipx\/[^"]+/g) || []; | ||
for (const ipxLink of ipxLinks) { | ||
body = body.replace( | ||
ipxLink, | ||
// Replace the link with the optimized image | ||
img(`/${ipxLink.split("/").slice(3).join("/")}` || "", { | ||
width: 800, | ||
format: "webp", | ||
}) | ||
); | ||
} | ||
const formatDate = (date?: string) => { | ||
return new Intl.DateTimeFormat(undefined, { | ||
year: "numeric", | ||
month: "long", | ||
day: "numeric", | ||
hour: "numeric", | ||
minute: "numeric", | ||
}).format(Date.parse(date ?? new Date().toISOString())); | ||
}; | ||
</script> | ||
|
||
<template> | ||
<HeadersNavbar /> | ||
|
||
<div | ||
v-if="post" | ||
class="mx-auto max-w-7xl flex flex-col pt-20 pb-24 sm:pb-32 px-6 lg:px-8"> | ||
<div class="mx-auto max-w-2xl text-center mt-32"> | ||
<div v-if="post" class="mx-auto max-w-7xl pb-24 sm:pb-32 px-6 lg:px-8 pt-1"> | ||
<div class="mx-auto max-w-2xl text-center mt-40"> | ||
<h1 | ||
v-if="post.title" | ||
class="text-4xl font-bold tracking-tight text-gray-50 sm:text-5xl"> | ||
{{ post.title }} | ||
</h1> | ||
|
||
<div class="mt-8 mx-auto"> | ||
<time | ||
v-if="post.created_at" | ||
:datetime="post.created_at" | ||
class="text-gray-500" | ||
>{{ formatDate(post.created_at) }}</time | ||
> | ||
</div> | ||
</div> | ||
<img | ||
<nuxt-img | ||
v-if="post.image" | ||
:src="post.image" | ||
width="800" | ||
format="webp" | ||
alt="" | ||
class="aspect-[16/9] mt-32 w-full max-w-3xl mx-auto rounded-2xl bg-dark-100 object-cover sm:aspect-[2/1] lg:aspect-[3/2]" /> | ||
<div | ||
class="mx-auto max-w-3xl prose prose-invert prose-gray pb-24 sm:pb-32 mt-16 [&_figcaption]:mt-4 [&_img]:mt-6 [&_img]:rounded-xl"> | ||
<ContentRenderer :value="post" /> | ||
</div> | ||
class="aspect-[16/9] drop-shadow-2xl mt-20 w-full max-w-3xl mx-auto rounded bg-dark-100 object-cover sm:aspect-[2/1] lg:aspect-[3/2]" /> | ||
<article | ||
class="mx-auto max-w-3xl prose prose-invert mt-10 content prose-code:before:content-none prose-code:after:content-none prose-a:text-orange-500 prose-a:underline" | ||
v-html="body"></article> | ||
</div> | ||
<Errors404 v-else /> | ||
</template> |
Oops, something went wrong.