This commit is contained in:
Sergey Yarkov 2025-11-18 22:49:03 +03:00
parent d40d8bb730
commit 35806aa60d
7 changed files with 1129 additions and 1321 deletions

View File

@ -1,9 +1,50 @@
import { createDirectus, graphql, staticToken } from "@directus/sdk";
import { DIRECTUS_TOKEN, DIRECTUS_URL } from "astro:env/server";
import type { Schema } from "./directus-schema";
import type { ArticleTranslation, Schema } from "./directus-schema";
const directus = createDirectus<Schema>(DIRECTUS_URL)
.with(staticToken(DIRECTUS_TOKEN))
.with(graphql());
export async function fetchArticleBySlug(slug: string): Promise<ArticleTranslation | null> {
const data = await directus.query<{ article_translations: ArticleTranslation[] }>(`
query Article_translations {
article_translations(
filter: {
slug: {
_eq: "${slug}"
}
}
) {
title
tags {
tag_id {
title
}
}
description
slug
content
author {
first_name
last_name
}
pub_date
views
languages_code {
code
}
cover_image {
title
filename_disk
}
}
}
`);
if (data.article_translations.length === 0) return null;
return data.article_translations[0];
}
export default directus;

2226
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,49 +19,51 @@ export type Props = {
const { article, availableOnLangs } = Astro.props as Props;
const pageLang = getLanguageFromURL(Astro.url.pathname);
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const { id, cover_image, title, author, description, tags, pub_date, slug } = article;
const { cover_image, title, author, description, tags, pub_date, slug, views } = article;
const t = useTranslation(Astro);
---
<article itemscope itemtype="http://schema.org/BlogPosting" class="article">
<article itemscope itemtype='http://schema.org/BlogPosting' class='article'>
<header>
<div>
{
tags && (
<div class="tags">
<ul itemprop="keywords">
{tags.map((tag) => (
<li>
<TranslatedLink rel="tag" to={`/articles/?tags=${tag}`}>
{tag}
</TranslatedLink>
</li>
))}
<div class='tags'>
<ul itemprop='keywords'>
{tags.map(
(tag) =>
typeof tag === "object" && (
<li>
<TranslatedLink rel='tag' to={`/articles/?tags=${tag.tag_id.title}`}>
{tag.tag_id.title}
</TranslatedLink>
</li>
)
)}
</ul>
</div>
)
}
<div class="views">
<Icon name="eye" width="21" height="21" />
<span><span id="views">---</span>&nbsp;{t("article.views")}</span>
<div class='views'>
<Icon name='eye' width='21' height='21' />
<span><span>{views}</span>&nbsp;{t("article.views")}</span>
</div>
</div>
<div class="heading">
<h1 itemprop="headline">{title}</h1>
<div class='heading'>
<h1 itemprop='headline'>{title}</h1>
</div>
<div class="published">
<time itemprop="datePublished" datetime={pub_date}>
<div class='published'>
<time itemprop='datePublished' datetime={pub_date}>
{createDateFormatter(pageLang).format(new Date(pub_date))}&nbsp;
</time>
<span itemprop="author">{author}</span>
<span itemprop='author'>{`${author.first_name} ${author.last_name}`}</span>
</div>
{
cover_image && (
<ArticleCover src={`${DIRECTUS_URL}/assets/${cover_image.filename_disk}`} alt={title} />
)
}
<div class="description">
<div class='description'>
<p>
{description}
</p>
@ -72,19 +74,7 @@ const t = useTranslation(Astro);
<ArticleComments url={canonicalUrl.href} id={slug} />
</article>
<script define:vars={{ slug, IS_DEV: import.meta.env.DEV }}>
(async () => {
if (IS_DEV) return;
const response = await fetch(`/api/views?slug=${slug}`, { method: "POST" });
if (!response.ok) return;
const views = await response.text();
const $views = document.getElementById("views");
if (isNaN(parseInt(views, 10))) return;
if ($views) $views.textContent = views;
})();
</script>
<style lang="scss" is:global>
<style lang='scss' is:global>
.article {
& header {
& div:nth-child(1) {

View File

@ -19,7 +19,7 @@ export interface Props {
}
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const { lang = DEFAULT_LANGUAGE, article, headings, availableOnLangs } = Astro.props as Props;
const { lang = DEFAULT_LANGUAGE, article, availableOnLangs } = Astro.props as Props;
const { title, description, cover_image, content } = article;
const htmlContent = await utils.markdownToHTML(content);
---
@ -30,19 +30,18 @@ const htmlContent = await utils.markdownToHTML(content);
title={title}
siteName={SITE_METADATA.name}
description={description}
type="article"
type='article'
url={canonicalUrl.href}
image={cover_image
? { path: cover_image.filename_disk, width: "700", height: "400", type: cover_image.type }
: undefined}
/>
<script is:inline defer src="/assets/js/load-code-highlight.js"></script>
<script is:inline defer src='/assets/js/load-code-highlight.js'></script>
</BaseHead>
<BaseLayout>
<div class="container">
<div class='container'>
<ArticlePost lang={lang} article={article} availableOnLangs={availableOnLangs}>
<!-- <TableOfContents headings={headings} /> -->
<ArticleContent set:html={htmlContent} />
</ArticlePost>
</div>

View File

@ -0,0 +1,15 @@
---
import ArticleLayout from "@root/layouts/article.astro";
import { fetchArticleBySlug } from "@/directus";
const { path } = Astro.params as { blog: string; path: string };
const slug = path.split("/").at(-1);
if (!slug) return Astro.redirect("/404");
const article = await fetchArticleBySlug(slug);
if (!article) return Astro.redirect("/404");
---
<ArticleLayout lang={article.languages_code.code} article={article} />

View File

@ -1,91 +0,0 @@
---
import type { CollectionEntry } from "astro:content";
import { getCollection, getEntry, render } from "astro:content";
import slugify from "@sindresorhus/slugify";
import ArticleLayout from "@root/layouts/article.astro";
import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils";
import directus from "@/directus";
import type { ArticleTranslation } from "@/directus-schema";
/**
* Generate articles pages from `blog` collection.
* The final url will be `http://website.com/blog/2023-01-01/my-article` with default language
* and `http://website.com/{language-code}/blog/2023-01-01/my-article` with other languages.
*/
// export async function getStaticPaths() {
// const collection = await getCollection("blog", (e) => !e.data.draft);
// return collection.map((article) => {
// const {
// id,
// data: { title, pubDate },
// } = article;
// const url = createRelativeArticleUrl({ id, title, pubDate });
// return {
// params: { blog: url.slice(1) },
// props: {
// article,
// availableOnLangs: collection.filter((e) =>
// e.id.includes(removeLanguageCodeFromPath(article.id))
// ),
// },
// };
// });
// }
type Props = { article: CollectionEntry<"blog">; availableOnLangs: CollectionEntry<"blog">[] };
const { slug } = Astro.params as { blog: string; date: string; slug: string };
const { article_translations: data } = await directus.query<{
article_translations: ArticleTranslation[];
}>(`
query Article_translations {
article_translations(
filter: {
slug: {
_eq: "${slug}"
}
}
) {
title
tags {
tag_id {
title
}
}
description
slug
content
author {
first_name
last_name
}
pub_date
views
languages_code {
code
}
cover_image {
title
filename_disk
}
}
}
`);
// const { blog } = Astro.params as { blog: string };
// const collection = await getCollection("blog", (e) => !e.data.draft);
// const slug = blog.split("/").at(-1);
// const article = collection.find(
// (a) => slugify(a.slug.split("/").at(-1) || "", { lowercase: true }) === slug
// );
if (data.length === 0) return Astro.redirect("/404");
const article = data[0];
// const { Content, headings } = await render(article);
// const { article, availableOnLangs } = Astro.props;
---
<ArticleLayout lang={article.languages_code.code} article={article} />

View File

@ -65,10 +65,6 @@ export function formatToArticleBlocks(articles: CollectionEntry<"blog">[]) {
return blocks;
}
export function getUniqueTags(articles: CollectionEntry<"blog">[]): string[] {
return Array.from(new Set(articles.map((a) => a.data.tags).flat()));
}
export type MarkdownHeadingToc = MarkdownHeading & { subheadings?: MarkdownHeading[] };
export function buildToc(headings: MarkdownHeading[], maxDepth: number = 2) {
@ -96,8 +92,8 @@ export async function markdownToHTML(content: string): Promise<string> {
const file = await unified()
.use(remarkParse)
.use(remarkPrism)
.use(remarkToc)
.use(remarkToc, { heading: "(table[ -]of[ -])?contents?|toc|содержание", maxDepth: 2 })
.use(remarkPrism as any)
.use(remarkRehype)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, {