mirror of
https://github.com/sergeyyarkov/yarkov.tech.git
synced 2026-10-11 10:29:21 +03:00
article page
This commit is contained in:
parent
6d9809f4e1
commit
d40d8bb730
@ -1 +1,2 @@
|
|||||||
|
DIRECTUS_URL=
|
||||||
DIRECTUS_TOKEN=
|
DIRECTUS_TOKEN=
|
||||||
@ -1,7 +1,6 @@
|
|||||||
export interface Article {
|
export interface Article {
|
||||||
/** @primaryKey */
|
/** @primaryKey */
|
||||||
id: number;
|
id: number;
|
||||||
tags?: ArticleTag[] | string[];
|
|
||||||
/** @required */
|
/** @required */
|
||||||
translations: ArticleTranslation[] | null;
|
translations: ArticleTranslation[] | null;
|
||||||
}
|
}
|
||||||
@ -36,6 +35,15 @@ export interface ArticleTranslation {
|
|||||||
/** @required */
|
/** @required */
|
||||||
views: number;
|
views: number;
|
||||||
cover_image?: DirectusFile | string | null;
|
cover_image?: DirectusFile | string | null;
|
||||||
|
/** @required */
|
||||||
|
tags: ArticleTranslationsTag[] | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleTranslationsTag {
|
||||||
|
/** @primaryKey */
|
||||||
|
id: number;
|
||||||
|
article_translations_id?: ArticleTranslation | string | null;
|
||||||
|
tag_id?: Tag | string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Global {
|
export interface Global {
|
||||||
@ -43,6 +51,12 @@ export interface Global {
|
|||||||
id: number;
|
id: number;
|
||||||
/** @required */
|
/** @required */
|
||||||
title: string;
|
title: string;
|
||||||
|
ym_counter?: number | null;
|
||||||
|
github?: string | null;
|
||||||
|
telegram?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
/** @required */
|
||||||
|
recent_articles_limit: number;
|
||||||
translations?: GlobalTranslation[] | null;
|
translations?: GlobalTranslation[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,8 +91,32 @@ export interface HomePageTranslation {
|
|||||||
export interface Language {
|
export interface Language {
|
||||||
/** @primaryKey */
|
/** @primaryKey */
|
||||||
code: string;
|
code: string;
|
||||||
name?: string | null;
|
/** @required */
|
||||||
direction?: 'ltr' | 'rtl' | null;
|
name: string;
|
||||||
|
/** @required */
|
||||||
|
direction: 'ltr' | 'rtl';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
/** @primaryKey */
|
||||||
|
id: number;
|
||||||
|
/** @required */
|
||||||
|
translations: ProjectTranslation[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectTranslation {
|
||||||
|
/** @primaryKey */
|
||||||
|
id: number;
|
||||||
|
project_id?: Project | string | null;
|
||||||
|
languages_code?: Language | string | null;
|
||||||
|
/** @required */
|
||||||
|
title: string;
|
||||||
|
/** @required */
|
||||||
|
year: string;
|
||||||
|
/** @required */
|
||||||
|
description: string;
|
||||||
|
/** @required */
|
||||||
|
source_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tag {
|
export interface Tag {
|
||||||
@ -539,11 +577,14 @@ export interface Schema {
|
|||||||
article: Article[];
|
article: Article[];
|
||||||
article_tag: ArticleTag[];
|
article_tag: ArticleTag[];
|
||||||
article_translations: ArticleTranslation[];
|
article_translations: ArticleTranslation[];
|
||||||
|
article_translations_tag: ArticleTranslationsTag[];
|
||||||
global: Global;
|
global: Global;
|
||||||
global_translations: GlobalTranslation[];
|
global_translations: GlobalTranslation[];
|
||||||
home_page: HomePage;
|
home_page: HomePage;
|
||||||
home_page_translations: HomePageTranslation[];
|
home_page_translations: HomePageTranslation[];
|
||||||
languages: Language[];
|
languages: Language[];
|
||||||
|
project: Project[];
|
||||||
|
project_translations: ProjectTranslation[];
|
||||||
tag: Tag[];
|
tag: Tag[];
|
||||||
directus_access: DirectusAccess[];
|
directus_access: DirectusAccess[];
|
||||||
directus_activity: DirectusActivity[];
|
directus_activity: DirectusActivity[];
|
||||||
@ -578,11 +619,14 @@ export enum CollectionNames {
|
|||||||
article = 'article',
|
article = 'article',
|
||||||
article_tag = 'article_tag',
|
article_tag = 'article_tag',
|
||||||
article_translations = 'article_translations',
|
article_translations = 'article_translations',
|
||||||
|
article_translations_tag = 'article_translations_tag',
|
||||||
global = 'global',
|
global = 'global',
|
||||||
global_translations = 'global_translations',
|
global_translations = 'global_translations',
|
||||||
home_page = 'home_page',
|
home_page = 'home_page',
|
||||||
home_page_translations = 'home_page_translations',
|
home_page_translations = 'home_page_translations',
|
||||||
languages = 'languages',
|
languages = 'languages',
|
||||||
|
project = 'project',
|
||||||
|
project_translations = 'project_translations',
|
||||||
tag = 'tag',
|
tag = 'tag',
|
||||||
directus_access = 'directus_access',
|
directus_access = 'directus_access',
|
||||||
directus_activity = 'directus_activity',
|
directus_activity = 'directus_activity',
|
||||||
|
|||||||
11538
package-lock.json
generated
11538
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -30,19 +30,31 @@
|
|||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"astro": "^5.15.8",
|
"astro": "^5.15.8",
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.5",
|
||||||
|
"hast-util-select": "^6.0.4",
|
||||||
"hast-util-to-string": "^3.0.0",
|
"hast-util-to-string": "^3.0.0",
|
||||||
"hastscript": "^8.0.0",
|
"hastscript": "^8.0.0",
|
||||||
"html-escaper": "^3.0.3",
|
"html-escaper": "^3.0.3",
|
||||||
|
"i": "^0.3.7",
|
||||||
|
"marked": "^17.0.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
"npm": "^11.6.2",
|
||||||
"quicklink": "^3.0.1",
|
"quicklink": "^3.0.1",
|
||||||
"rehype-autolink-headings": "^7.0.0",
|
"rehype-autolink-headings": "^7.0.0",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
|
"rehype-stringify": "^10.0.1",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-prism": "^1.3.6",
|
||||||
|
"remark-rehype": "^11.1.2",
|
||||||
|
"remark-toc": "^9.0.0",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"solid-js": "^1.7.12",
|
"solid-js": "^1.7.12",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3",
|
||||||
|
"unified": "^11.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/html-escaper": "^3.0.4",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
|
"@types/remark-prism": "^1.3.7",
|
||||||
"directus-sdk-typegen": "^0.2.1",
|
"directus-sdk-typegen": "^0.2.1",
|
||||||
"sass": "^1.68.0"
|
"sass": "^1.68.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,25 +2,22 @@ import type { Component } from "solid-js";
|
|||||||
import { createDateFormatter, createRelativeArticleUrl } from "@root/utils";
|
import { createDateFormatter, createRelativeArticleUrl } from "@root/utils";
|
||||||
import { DEFAULT_LANGUAGE, URL_BLOG_PREFIX } from "@root/constants";
|
import { DEFAULT_LANGUAGE, URL_BLOG_PREFIX } from "@root/constants";
|
||||||
import "./ArticleItem.scss";
|
import "./ArticleItem.scss";
|
||||||
import type { Language } from "@/directus-schema";
|
import type { ArticleTranslation } from "@/directus-schema";
|
||||||
|
|
||||||
export type ArticleItemProps = {
|
export type ArticleItemProps = {
|
||||||
id: number;
|
data: ArticleTranslation;
|
||||||
title: string;
|
|
||||||
pubDate: string;
|
|
||||||
pageLang: string;
|
pageLang: string;
|
||||||
articleLang: string | Language;
|
|
||||||
slug: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArticleItem: Component<ArticleItemProps> = (props) => {
|
const ArticleItem: Component<ArticleItemProps> = (props) => {
|
||||||
|
const {
|
||||||
|
data: { title, pub_date: pubDate, slug, languages_code },
|
||||||
|
pageLang,
|
||||||
|
} = props;
|
||||||
const articleLang =
|
const articleLang =
|
||||||
typeof props.articleLang === "object" ? props.articleLang.code.split("-")[0] : DEFAULT_LANGUAGE;
|
typeof languages_code === "object" ? languages_code.code.split("-")[0] : DEFAULT_LANGUAGE;
|
||||||
|
|
||||||
const href = createRelativeArticleUrl(
|
const href = createRelativeArticleUrl({ pubDate, slug, articleLang }, URL_BLOG_PREFIX);
|
||||||
{ pubDate: props.pubDate, slug: props.slug, articleLang },
|
|
||||||
URL_BLOG_PREFIX
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
@ -31,13 +28,11 @@ const ArticleItem: Component<ArticleItemProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<a href={href}>
|
<a href={href}>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<h3 itemprop="headline">{props.title}</h3>
|
<h3 itemprop="headline">{title}</h3>
|
||||||
{articleLang !== props.pageLang && <sup>{articleLang.toLocaleUpperCase()}</sup>}
|
{articleLang !== pageLang && <sup>{articleLang.toLocaleUpperCase()}</sup>}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<time datetime={props.pubDate}>
|
<time datetime={pubDate}>{createDateFormatter(pageLang).format(new Date(pubDate))}</time>
|
||||||
{createDateFormatter(props.pageLang).format(new Date(props.pubDate))}
|
|
||||||
</time>
|
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import ArticleComments from "@components/ArticleComments.astro";
|
|||||||
import TranslatedLink from "@components/TranslatedLink.astro";
|
import TranslatedLink from "@components/TranslatedLink.astro";
|
||||||
import AvailableOnLangs from "@components/AvailableOnLangs.astro";
|
import AvailableOnLangs from "@components/AvailableOnLangs.astro";
|
||||||
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
|
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
|
||||||
import * as utils from "@utils/index";
|
import type { ArticleTranslation } from "@/directus-schema";
|
||||||
|
import { DIRECTUS_URL } from "astro:env/server";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
article: CollectionEntry<"blog">;
|
article: ArticleTranslation;
|
||||||
availableOnLangs?: CollectionEntry<"blog">[];
|
availableOnLangs?: CollectionEntry<"blog">[];
|
||||||
lang: string;
|
lang: string;
|
||||||
};
|
};
|
||||||
@ -18,12 +19,8 @@ export type Props = {
|
|||||||
const { article, availableOnLangs } = Astro.props as Props;
|
const { article, availableOnLangs } = Astro.props as Props;
|
||||||
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
||||||
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
||||||
const {
|
const { id, cover_image, title, author, description, tags, pub_date, slug } = article;
|
||||||
id,
|
|
||||||
data: { coverImage, title, author, description, tags, pubDate },
|
|
||||||
} = article;
|
|
||||||
|
|
||||||
const slug = utils.encodeString(id.split("/")[1]);
|
|
||||||
const t = useTranslation(Astro);
|
const t = useTranslation(Astro);
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -54,12 +51,16 @@ const t = useTranslation(Astro);
|
|||||||
<h1 itemprop="headline">{title}</h1>
|
<h1 itemprop="headline">{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="published">
|
<div class="published">
|
||||||
<time itemprop="datePublished" datetime={pubDate.toString()}>
|
<time itemprop="datePublished" datetime={pub_date}>
|
||||||
{createDateFormatter(pageLang).format(pubDate)}
|
{createDateFormatter(pageLang).format(new Date(pub_date))}
|
||||||
</time>
|
</time>
|
||||||
<span itemprop="author">{author}</span>
|
<span itemprop="author">{author}</span>
|
||||||
</div>
|
</div>
|
||||||
{coverImage && <ArticleCover src={coverImage} alt={title} />}
|
{
|
||||||
|
cover_image && (
|
||||||
|
<ArticleCover src={`${DIRECTUS_URL}/assets/${cover_image.filename_disk}`} alt={title} />
|
||||||
|
)
|
||||||
|
}
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<p>
|
<p>
|
||||||
{description}
|
{description}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
import siteconfig from "siteconfig.json";
|
|
||||||
import { getLanguageFromURL } from "@i18n/utils";
|
import { getLanguageFromURL } from "@i18n/utils";
|
||||||
import * as utils from "@utils/index";
|
import * as utils from "@utils/index";
|
||||||
import ArticleItem from "@components/ArticleItem";
|
import ArticleItem from "@components/ArticleItem";
|
||||||
@ -41,12 +40,9 @@ const data = await directus.query<RecentArticlesQueryResult>(`
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const articles = data.article.flatMap((a) => a.translations);
|
const articles = data.article.flatMap((a) =>
|
||||||
|
utils.removeArticleDuplicates(a.translations, pageLang)
|
||||||
console.log(articles);
|
);
|
||||||
|
|
||||||
// const { length = siteconfig.articles.recentLimit } = Astro.props as Props;
|
|
||||||
// const filtered = utils.removeDuplicates(articles.slice(0, length), pageLang);
|
|
||||||
const isEmpty = articles.length === 0;
|
const isEmpty = articles.length === 0;
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -55,26 +51,14 @@ const isEmpty = articles.length === 0;
|
|||||||
<h2>
|
<h2>
|
||||||
<UIString key={"recentArticles.heading"} />
|
<UIString key={"recentArticles.heading"} />
|
||||||
</h2>
|
</h2>
|
||||||
<TranslatedLink to='/articles/'>
|
<TranslatedLink to="/articles/">
|
||||||
<UIString key={"recentArticles.all"} />
|
<UIString key={"recentArticles.all"} />
|
||||||
</TranslatedLink>
|
</TranslatedLink>
|
||||||
</div>
|
</div>
|
||||||
<div itemscope itemtype='http://schema.org/Blog'>
|
<div itemscope itemtype="http://schema.org/Blog">
|
||||||
{
|
{
|
||||||
!isEmpty ? (
|
!isEmpty ? (
|
||||||
articles.map(
|
articles.map((article) => article && <ArticleItem data={article} pageLang={pageLang} />)
|
||||||
(article) =>
|
|
||||||
article && (
|
|
||||||
<ArticleItem
|
|
||||||
id={article.id}
|
|
||||||
title={article.title}
|
|
||||||
pubDate={article.pub_date}
|
|
||||||
slug={article.slug}
|
|
||||||
articleLang={article.languages_code}
|
|
||||||
pageLang={pageLang}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<EmptyList />
|
<EmptyList />
|
||||||
)
|
)
|
||||||
@ -82,7 +66,7 @@ const isEmpty = articles.length === 0;
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style lang='scss'>
|
<style lang="scss">
|
||||||
section {
|
section {
|
||||||
& > div:nth-child(1) {
|
& > div:nth-child(1) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -8,20 +8,20 @@ import ArticleContent from "@components/ArticleContent.astro";
|
|||||||
import ArticlePost from "@components/ArticlePost.astro";
|
import ArticlePost from "@components/ArticlePost.astro";
|
||||||
import OpenGraph from "@components/OpenGraph.astro";
|
import OpenGraph from "@components/OpenGraph.astro";
|
||||||
import TableOfContents from "@root/components/TableOfContents.astro";
|
import TableOfContents from "@root/components/TableOfContents.astro";
|
||||||
import { useTranslation } from "@root/i18n/utils";
|
import type { ArticleTranslation } from "@/directus-schema";
|
||||||
|
import * as utils from "@utils/article";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
lang?: string | undefined;
|
lang?: string | undefined;
|
||||||
article: CollectionEntry<"blog">;
|
article: ArticleTranslation;
|
||||||
availableOnLangs?: CollectionEntry<"blog">[];
|
availableOnLangs?: CollectionEntry<"blog">[];
|
||||||
headings: MarkdownHeading[];
|
headings: MarkdownHeading[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
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, headings, availableOnLangs } = Astro.props as Props;
|
||||||
const {
|
const { title, description, cover_image, content } = article;
|
||||||
data: { title, description, coverImage },
|
const htmlContent = await utils.markdownToHTML(content);
|
||||||
} = article;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang={lang}>
|
<html lang={lang}>
|
||||||
@ -32,8 +32,8 @@ const {
|
|||||||
description={description}
|
description={description}
|
||||||
type="article"
|
type="article"
|
||||||
url={canonicalUrl.href}
|
url={canonicalUrl.href}
|
||||||
image={coverImage
|
image={cover_image
|
||||||
? { path: coverImage, width: "700", height: "400", type: coverImage }
|
? { path: cover_image.filename_disk, width: "700", height: "400", type: cover_image.type }
|
||||||
: undefined}
|
: 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>
|
||||||
@ -42,10 +42,8 @@ const {
|
|||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ArticlePost lang={lang} article={article} availableOnLangs={availableOnLangs}>
|
<ArticlePost lang={lang} article={article} availableOnLangs={availableOnLangs}>
|
||||||
<TableOfContents headings={headings} />
|
<!-- <TableOfContents headings={headings} /> -->
|
||||||
<ArticleContent>
|
<ArticleContent set:html={htmlContent} />
|
||||||
<slot />
|
|
||||||
</ArticleContent>
|
|
||||||
</ArticlePost>
|
</ArticlePost>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|||||||
@ -1,50 +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";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 { 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 (article === undefined) return Astro.redirect("/404");
|
|
||||||
|
|
||||||
const { Content, headings } = await render(article);
|
|
||||||
// const { article, availableOnLangs } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<ArticleLayout lang={article.id.split("/")[0]} article={article} headings={headings}>
|
|
||||||
<Content />
|
|
||||||
</ArticleLayout>
|
|
||||||
91
src/pages/[blog]/[date]/[...slug].astro
Normal file
91
src/pages/[blog]/[date]/[...slug].astro
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
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} />
|
||||||
@ -1,34 +1,39 @@
|
|||||||
import type { MarkdownHeading } from "astro";
|
import type { MarkdownHeading } from "astro";
|
||||||
import type { CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import { DEFAULT_LANGUAGE } from "@root/constants";
|
import { DEFAULT_LANGUAGE } from "@root/constants";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import type { ArticleTranslation } from "@/directus-schema";
|
||||||
import { findDuplicates } from "./string";
|
import { unified } from "unified";
|
||||||
import { removeLanguageCodeFromPath } from "./url";
|
import remarkParse from "remark-parse";
|
||||||
|
import remarkPrism from "remark-prism";
|
||||||
|
import remarkToc from "remark-toc";
|
||||||
|
import remarkRehype from "remark-rehype";
|
||||||
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||||
|
import rehypeSlug from "rehype-slug";
|
||||||
|
import rehypeStringify from "rehype-stringify";
|
||||||
|
import { escape } from "html-escaper";
|
||||||
|
import { h } from "hastscript";
|
||||||
|
import { toString } from "hast-util-to-string";
|
||||||
|
import { selectAll } from "hast-util-select";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If there is a translation of an article in the current language of the page,
|
* If there is a translation of an article in the current language of the page,
|
||||||
* the remaining articles with other translations should be removed.
|
* the remaining articles with other translations should be removed.
|
||||||
*/
|
*/
|
||||||
export function removeDuplicates(
|
export function removeArticleDuplicates(
|
||||||
entries: CollectionEntry<"blog">[],
|
articles: ArticleTranslation[] | null,
|
||||||
pageLang: LanguageKeys
|
pageLang: LanguageKeys
|
||||||
): CollectionEntry<"blog">[] {
|
): ArticleTranslation[] {
|
||||||
const ids = entries.map((a) => a.id) as string[];
|
if (!articles) return [];
|
||||||
const duplicates = findDuplicates(ids.map((id) => removeLanguageCodeFromPath(id)));
|
if (articles && articles.length >= 2) {
|
||||||
|
const pageLangArticles = articles.filter((a) => {
|
||||||
duplicates.forEach((id) => {
|
return (
|
||||||
const toDeleteIdxs: number[] = [];
|
typeof a.languages_code === "object" && a.languages_code.code.split("-")[0] === pageLang
|
||||||
if (ids.indexOf(`${pageLang}/${id}`) !== -1) {
|
);
|
||||||
entries.forEach((a, i) => {
|
});
|
||||||
if (a.id !== `${pageLang}/${id}` && a.id.includes(id)) {
|
if (pageLangArticles.length !== 0) return pageLangArticles;
|
||||||
toDeleteIdxs.push(i);
|
return articles;
|
||||||
}
|
}
|
||||||
});
|
return articles;
|
||||||
}
|
|
||||||
toDeleteIdxs.forEach((i) => entries.splice(i, 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,3 +86,56 @@ export function buildToc(headings: MarkdownHeading[], maxDepth: number = 2) {
|
|||||||
});
|
});
|
||||||
return toc;
|
return toc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function markdownToHTML(content: string): Promise<string> {
|
||||||
|
const createAnchorLabel = (heading: string) => {
|
||||||
|
const node = h("span.anchor-label", escape(heading));
|
||||||
|
if (node.properties) node.properties["is:raw"] = true;
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
const file = await unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkPrism)
|
||||||
|
.use(remarkToc)
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(rehypeSlug)
|
||||||
|
.use(rehypeAutolinkHeadings, {
|
||||||
|
properties: {
|
||||||
|
class: "anchor-link",
|
||||||
|
},
|
||||||
|
behavior: "after",
|
||||||
|
group: ({ tagName }) =>
|
||||||
|
h(`div.heading-wrapper.level-${tagName}`, {
|
||||||
|
tabIndex: -1,
|
||||||
|
}),
|
||||||
|
content: (heading) => [
|
||||||
|
h(
|
||||||
|
`span.anchor-icon`,
|
||||||
|
undefined,
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
version: 1.1,
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
xlmns: "http://www.w3.org/2000/svg",
|
||||||
|
ariaHidden: "true",
|
||||||
|
"data-filter": "none",
|
||||||
|
},
|
||||||
|
h("path", {
|
||||||
|
fillRule: "evenodd",
|
||||||
|
fill: "currentcolor",
|
||||||
|
d: "M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
createAnchorLabel(toString(heading)),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.use(rehypeStringify)
|
||||||
|
.process(content);
|
||||||
|
|
||||||
|
return String(file);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user