mirror of
https://github.com/sergeyyarkov/yarkov.tech.git
synced 2026-10-11 02:19:08 +03:00
upd
This commit is contained in:
parent
35806aa60d
commit
c5d7b7a7ba
@ -8,27 +8,12 @@ import sitemap from "@astrojs/sitemap";
|
|||||||
import solidJs from "@astrojs/solid-js";
|
import solidJs from "@astrojs/solid-js";
|
||||||
import icon from "astro-icon";
|
import icon from "astro-icon";
|
||||||
|
|
||||||
/**
|
|
||||||
* Rehype plugins
|
|
||||||
*/
|
|
||||||
import rehypeSlug from "rehype-slug";
|
|
||||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Other required modules
|
* Other required modules
|
||||||
*/
|
*/
|
||||||
import { h } from "hastscript";
|
|
||||||
import { toString } from "hast-util-to-string";
|
|
||||||
import { escape } from "html-escaper";
|
|
||||||
import languages from "./src/i18n/languages";
|
import languages from "./src/i18n/languages";
|
||||||
import { DEFAULT_LANGUAGE } from "./src/constants";
|
import { DEFAULT_LANGUAGE } from "./src/constants";
|
||||||
|
|
||||||
const createAnchorLabel = (heading) => {
|
|
||||||
const node = h("span.anchor-label", escape(heading));
|
|
||||||
if (node.properties) node.properties["is:raw"] = true;
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: "https://yarkov.tech",
|
site: "https://yarkov.tech",
|
||||||
@ -53,49 +38,6 @@ export default defineConfig({
|
|||||||
icon(),
|
icon(),
|
||||||
solidJs(),
|
solidJs(),
|
||||||
],
|
],
|
||||||
markdown: {
|
|
||||||
syntaxHighlight: "prism",
|
|
||||||
rehypePlugins: [
|
|
||||||
rehypeSlug,
|
|
||||||
[
|
|
||||||
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)),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
trailingSlash: "ignore",
|
trailingSlash: "ignore",
|
||||||
scopedStyleStrategy: "where",
|
scopedStyleStrategy: "where",
|
||||||
});
|
});
|
||||||
|
|||||||
102
directus.ts
102
directus.ts
@ -1,12 +1,14 @@
|
|||||||
import { createDirectus, graphql, staticToken } from "@directus/sdk";
|
import { createDirectus, graphql, staticToken } from "@directus/sdk";
|
||||||
import { DIRECTUS_TOKEN, DIRECTUS_URL } from "astro:env/server";
|
import { DIRECTUS_TOKEN, DIRECTUS_URL } from "astro:env/server";
|
||||||
import type { ArticleTranslation, Schema } from "./directus-schema";
|
import type { Article, ArticleTranslation, Schema, Tag } from "./directus-schema";
|
||||||
|
|
||||||
const directus = createDirectus<Schema>(DIRECTUS_URL)
|
const directus = createDirectus<Schema>(DIRECTUS_URL)
|
||||||
.with(staticToken(DIRECTUS_TOKEN))
|
.with(staticToken(DIRECTUS_TOKEN))
|
||||||
.with(graphql());
|
.with(graphql());
|
||||||
|
|
||||||
export async function fetchArticleBySlug(slug: string): Promise<ArticleTranslation | null> {
|
export async function fetchArticleTranslationsBySlug(
|
||||||
|
slug: string
|
||||||
|
): Promise<ArticleTranslation | null> {
|
||||||
const data = await directus.query<{ article_translations: ArticleTranslation[] }>(`
|
const data = await directus.query<{ article_translations: ArticleTranslation[] }>(`
|
||||||
query Article_translations {
|
query Article_translations {
|
||||||
article_translations(
|
article_translations(
|
||||||
@ -16,6 +18,7 @@ export async function fetchArticleBySlug(slug: string): Promise<ArticleTranslati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
id
|
||||||
title
|
title
|
||||||
tags {
|
tags {
|
||||||
tag_id {
|
tag_id {
|
||||||
@ -33,18 +36,111 @@ export async function fetchArticleBySlug(slug: string): Promise<ArticleTranslati
|
|||||||
views
|
views
|
||||||
languages_code {
|
languages_code {
|
||||||
code
|
code
|
||||||
|
name
|
||||||
}
|
}
|
||||||
cover_image {
|
cover_image {
|
||||||
title
|
title
|
||||||
filename_disk
|
filename_disk
|
||||||
}
|
}
|
||||||
|
article_id {
|
||||||
|
translations {
|
||||||
|
slug
|
||||||
|
languages_code {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
}
|
||||||
|
pub_date
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (data.article_translations.length === 0) return null;
|
if (data.article_translations.length === 0) return null;
|
||||||
|
|
||||||
return data.article_translations[0];
|
return { ...data.article_translations[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchArticles(): Promise<Article[]> {
|
||||||
|
const data = await directus.query<{ article: Article[] }>(`
|
||||||
|
query Article {
|
||||||
|
article {
|
||||||
|
translations(
|
||||||
|
filter: {
|
||||||
|
status: { _eq: "published" }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
tags {
|
||||||
|
tag_id {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description
|
||||||
|
slug
|
||||||
|
pub_date
|
||||||
|
languages_code {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
return data.article;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRecentArticles(): Promise<Article[]> {
|
||||||
|
const data = await directus.query<{ article: Article[] }>(`
|
||||||
|
query Article {
|
||||||
|
article {
|
||||||
|
translations(
|
||||||
|
filter: {
|
||||||
|
status: { _eq: "published" }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
title
|
||||||
|
cover_image {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
slug
|
||||||
|
pub_date
|
||||||
|
views
|
||||||
|
languages_code { code }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
return data.article;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function incArticleTranslationViews(
|
||||||
|
article: Pick<ArticleTranslation, "id" | "views">
|
||||||
|
): Promise<number> {
|
||||||
|
const data = await directus.query<{ update_article_translations_item: { views: number } }>(`
|
||||||
|
mutation Update_article_translations_item {
|
||||||
|
update_article_translations_item(id: ${article.id}, data: { views: ${article.views + 1} }) {
|
||||||
|
views
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
return data.update_article_translations_item.views;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTags(): Promise<Tag[]> {
|
||||||
|
const data = await directus.query<{ tag: Tag[] }>(`
|
||||||
|
query Tag {
|
||||||
|
tag {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
return data.tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default directus;
|
export default directus;
|
||||||
|
|||||||
4212
package-lock.json
generated
4212
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,8 +12,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
|
||||||
"build": "astro check && astro build",
|
"build": "astro check && astro build",
|
||||||
|
"start": "node --env-file=.env ./dist/server/entry.mjs",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"directus:gen-types": "node --env-file=.env ./scripts/gen-types.js"
|
"directus:gen-types": "node --env-file=.env ./scripts/gen-types.js"
|
||||||
@ -26,18 +26,13 @@
|
|||||||
"@astrojs/solid-js": "^5.1.3",
|
"@astrojs/solid-js": "^5.1.3",
|
||||||
"@directus/sdk": "^20.1.1",
|
"@directus/sdk": "^20.1.1",
|
||||||
"@iconify-json/mdi": "^1.2.3",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@netlify/functions": "^2.1.0",
|
|
||||||
"@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-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",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { createDateFormatter } from "@root/utils";
|
import { createDateFormatter } from "@root/utils";
|
||||||
import ArticleCover from "@components/ArticleCover.astro";
|
import ArticleCover from "@components/ArticleCover.astro";
|
||||||
@ -12,29 +11,29 @@ import { DIRECTUS_URL } from "astro:env/server";
|
|||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
article: ArticleTranslation;
|
article: ArticleTranslation;
|
||||||
availableOnLangs?: CollectionEntry<"blog">[];
|
|
||||||
lang: string;
|
lang: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { article, availableOnLangs } = Astro.props as Props;
|
const { article } = 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 { cover_image, title, author, description, tags, pub_date, slug, views } = article;
|
const { cover_image, title, author, description, tags, pub_date, slug, views, article_id } =
|
||||||
|
article;
|
||||||
const t = useTranslation(Astro);
|
const t = useTranslation(Astro);
|
||||||
---
|
---
|
||||||
|
|
||||||
<article itemscope itemtype='http://schema.org/BlogPosting' class='article'>
|
<article itemscope itemtype="http://schema.org/BlogPosting" class="article">
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
tags && (
|
tags && (
|
||||||
<div class='tags'>
|
<div class="tags">
|
||||||
<ul itemprop='keywords'>
|
<ul itemprop="keywords">
|
||||||
{tags.map(
|
{tags.map(
|
||||||
(tag) =>
|
(tag) =>
|
||||||
typeof tag === "object" && (
|
typeof tag === "object" && (
|
||||||
<li>
|
<li>
|
||||||
<TranslatedLink rel='tag' to={`/articles/?tags=${tag.tag_id.title}`}>
|
<TranslatedLink rel="tag" to={`/articles/?tags=${tag.tag_id.title}`}>
|
||||||
{tag.tag_id.title}
|
{tag.tag_id.title}
|
||||||
</TranslatedLink>
|
</TranslatedLink>
|
||||||
</li>
|
</li>
|
||||||
@ -44,37 +43,37 @@ const t = useTranslation(Astro);
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<div class='views'>
|
<div class="views">
|
||||||
<Icon name='eye' width='21' height='21' />
|
<Icon name="eye" width="21" height="21" />
|
||||||
<span><span>{views}</span> {t("article.views")}</span>
|
<span><span>{views}</span> {t("article.views")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='heading'>
|
<div class="heading">
|
||||||
<h1 itemprop='headline'>{title}</h1>
|
<h1 itemprop="headline">{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class='published'>
|
<div class="published">
|
||||||
<time itemprop='datePublished' datetime={pub_date}>
|
<time itemprop="datePublished" datetime={pub_date}>
|
||||||
{createDateFormatter(pageLang).format(new Date(pub_date))}
|
{createDateFormatter(pageLang).format(new Date(pub_date))}
|
||||||
</time>
|
</time>
|
||||||
<span itemprop='author'>{`${author.first_name} ${author.last_name}`}</span>
|
<span itemprop="author">{`${author.first_name} ${author.last_name}`}</span>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
cover_image && (
|
cover_image && (
|
||||||
<ArticleCover src={`${DIRECTUS_URL}/assets/${cover_image.filename_disk}`} alt={title} />
|
<ArticleCover src={`${DIRECTUS_URL}/assets/${cover_image.filename_disk}`} alt={title} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<div class='description'>
|
<div class="description">
|
||||||
<p>
|
<p>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{availableOnLangs && <AvailableOnLangs articles={availableOnLangs} />}
|
<AvailableOnLangs articles={article_id.translations} />
|
||||||
</header>
|
</header>
|
||||||
<slot />
|
<slot />
|
||||||
<ArticleComments url={canonicalUrl.href} id={slug} />
|
<ArticleComments url={canonicalUrl.href} id={slug} />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<style lang='scss' is:global>
|
<style lang="scss" is:global>
|
||||||
.article {
|
.article {
|
||||||
& header {
|
& header {
|
||||||
& div:nth-child(1) {
|
& div:nth-child(1) {
|
||||||
|
|||||||
@ -5,17 +5,11 @@ import SearchInput from "./SearchInput";
|
|||||||
import TagsList from "./TagsList";
|
import TagsList from "./TagsList";
|
||||||
import ArticleItem from "@components/ArticleItem";
|
import ArticleItem from "@components/ArticleItem";
|
||||||
import "./ArticlesList.scss";
|
import "./ArticlesList.scss";
|
||||||
|
import type { ArticleTranslation } from "@/directus-schema";
|
||||||
|
|
||||||
type UiStringsType = { "articles.empty": string; "input.search": string };
|
type UiStringsType = { "articles.empty": string; "input.search": string };
|
||||||
|
|
||||||
type ArticlesType = Array<{
|
type ArticlesBlockType = Record<string, ArticleTranslation[]>;
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
tags: string[];
|
|
||||||
pubDate: Date;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
type ArticlesBlockType = Record<string, ArticlesType>;
|
|
||||||
|
|
||||||
type ArticlesListProps = {
|
type ArticlesListProps = {
|
||||||
articles: ArticlesBlockType;
|
articles: ArticlesBlockType;
|
||||||
@ -40,7 +34,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
|
|||||||
}: {
|
}: {
|
||||||
params: { search: string; tags?: string[] };
|
params: { search: string; tags?: string[] };
|
||||||
}): ArticlesBlockType => {
|
}): ArticlesBlockType => {
|
||||||
const filter = (cb: (articles: ArticlesType) => ArticlesType) => {
|
const filter = (cb: (articles: ArticleTranslation[]) => ArticleTranslation[]) => {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.keys(props.articles)
|
Object.keys(props.articles)
|
||||||
.map((year) => [year, cb(props.articles[year])])
|
.map((year) => [year, cb(props.articles[year])])
|
||||||
@ -75,16 +69,9 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
|
|||||||
{(year) => (
|
{(year) => (
|
||||||
<div class="articles-list__wrapper">
|
<div class="articles-list__wrapper">
|
||||||
<h2>{year}</h2>
|
<h2>{year}</h2>
|
||||||
{/* <For each={articles()[year]}>
|
<For each={articles()[year]}>
|
||||||
{({ id, title, pubDate }) => (
|
{(data) => <ArticleItem data={data} pageLang={props.pageLang} />}
|
||||||
<ArticleItem
|
</For>
|
||||||
id={id}
|
|
||||||
title={title}
|
|
||||||
pubDate={pubDate}
|
|
||||||
pageLang={props.pageLang}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For> */}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@ -23,6 +23,8 @@ const SearchInput: Component<SearchInputProps> = (props) => {
|
|||||||
const onSearch: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
|
const onSearch: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
|
||||||
const value = e.currentTarget.value;
|
const value = e.currentTarget.value;
|
||||||
|
|
||||||
|
console.log(value);
|
||||||
|
|
||||||
if (value.trim().length === 0) {
|
if (value.trim().length === 0) {
|
||||||
resetSearch();
|
resetSearch();
|
||||||
return;
|
return;
|
||||||
@ -62,6 +64,7 @@ const SearchInput: Component<SearchInputProps> = (props) => {
|
|||||||
onInput={onSearch}
|
onInput={onSearch}
|
||||||
value={search()}
|
value={search()}
|
||||||
type="search"
|
type="search"
|
||||||
|
name="search"
|
||||||
placeholder={props.i18n["input.search"] || "Search article by title..."}
|
placeholder={props.i18n["input.search"] || "Search article by title..."}
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
---
|
---
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import { getLanguageFromURL, useTranslation } from "@root/i18n/utils";
|
import { getLanguageFromURL, useTranslation } from "@root/i18n/utils";
|
||||||
import { URL_BLOG_PREFIX } from "@root/constants";
|
|
||||||
import { createRelativeArticleUrl } from "@root/utils";
|
import { createRelativeArticleUrl } from "@root/utils";
|
||||||
import languages from "@i18n/languages";
|
import type { ArticleTranslation } from "@/directus-schema";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
articles: CollectionEntry<"blog">[];
|
articles: ArticleTranslation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
||||||
@ -19,15 +17,17 @@ const { articles } = Astro.props;
|
|||||||
<div>
|
<div>
|
||||||
{t("article.availableOn")}:{" "}
|
{t("article.availableOn")}:{" "}
|
||||||
{articles
|
{articles
|
||||||
.filter((a) => !a.id.startsWith(`${pageLang}/`))
|
.filter((a) => a.languages_code.code.split("-")[0] !== pageLang)
|
||||||
.map(({ id, data: { title, pubDate } }) => (
|
.map((a) => (
|
||||||
<a href={`...`}>{`${languages[id.split("/")[0] as LanguageKeys]}`}</a>
|
<a
|
||||||
|
href={`${createRelativeArticleUrl({ slug: a.slug, articleLang: a.languages_code.code.split('-')[0], pubDate: a.pub_date })}`}
|
||||||
|
>{`${a.languages_code.name}`}</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<style lang='scss'>
|
<style lang="scss">
|
||||||
div {
|
div {
|
||||||
margin: 25px 0;
|
margin: 25px 0;
|
||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
|
|||||||
@ -5,44 +5,16 @@ import ArticleItem from "@components/ArticleItem";
|
|||||||
import EmptyList from "@components/EmptyList.astro";
|
import EmptyList from "@components/EmptyList.astro";
|
||||||
import UIString from "@components/UIString.astro";
|
import UIString from "@components/UIString.astro";
|
||||||
import TranslatedLink from "@components/TranslatedLink.astro";
|
import TranslatedLink from "@components/TranslatedLink.astro";
|
||||||
import directus from "@/directus";
|
import { fetchRecentArticles } from "@/directus";
|
||||||
import type { Article } from "@/directus-schema";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
length?: number | undefined;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecentArticlesQueryResult = {
|
const { limit = 1 } = Astro.props;
|
||||||
article: Array<Article>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
||||||
|
const data = await fetchRecentArticles();
|
||||||
const data = await directus.query<RecentArticlesQueryResult>(`
|
const articles = data.flatMap((a) => utils.removeArticleDuplicates(a.translations, pageLang));
|
||||||
query Article {
|
|
||||||
article {
|
|
||||||
translations(
|
|
||||||
filter: {
|
|
||||||
status: { _eq: "published" }
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
title
|
|
||||||
cover_image {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
slug
|
|
||||||
pub_date
|
|
||||||
views
|
|
||||||
languages_code { code }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const articles = data.article.flatMap((a) =>
|
|
||||||
utils.removeArticleDuplicates(a.translations, pageLang)
|
|
||||||
);
|
|
||||||
const isEmpty = articles.length === 0;
|
const isEmpty = articles.length === 0;
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -58,7 +30,9 @@ const isEmpty = articles.length === 0;
|
|||||||
<div itemscope itemtype="http://schema.org/Blog">
|
<div itemscope itemtype="http://schema.org/Blog">
|
||||||
{
|
{
|
||||||
!isEmpty ? (
|
!isEmpty ? (
|
||||||
articles.map((article) => article && <ArticleItem data={article} pageLang={pageLang} />)
|
articles
|
||||||
|
.slice(0, limit)
|
||||||
|
.map((article) => article && <ArticleItem data={article} pageLang={pageLang} />)
|
||||||
) : (
|
) : (
|
||||||
<EmptyList />
|
<EmptyList />
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,25 +1,23 @@
|
|||||||
---
|
---
|
||||||
import type { MarkdownHeading } from "astro";
|
import type { MarkdownHeading } from "astro";
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import { DEFAULT_LANGUAGE, SITE_METADATA } from "@root/constants";
|
import { DEFAULT_LANGUAGE, SITE_METADATA } from "@root/constants";
|
||||||
import BaseHead from "@components/BaseHead.astro";
|
import BaseHead from "@components/BaseHead.astro";
|
||||||
import BaseLayout from "@components/BaseLayout.astro";
|
import BaseLayout from "@components/BaseLayout.astro";
|
||||||
import ArticleContent from "@components/ArticleContent.astro";
|
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 type { ArticleTranslation } from "@/directus-schema";
|
import type { ArticleTranslation } from "@/directus-schema";
|
||||||
import * as utils from "@utils/article";
|
import * as utils from "@utils/article";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
lang?: string | undefined;
|
lang?: string | undefined;
|
||||||
article: ArticleTranslation;
|
article: ArticleTranslation;
|
||||||
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, availableOnLangs } = Astro.props as Props;
|
const { lang = DEFAULT_LANGUAGE, article } = Astro.props as Props;
|
||||||
const { title, description, cover_image, content } = article;
|
const { title, description, cover_image, content } = article;
|
||||||
const htmlContent = await utils.markdownToHTML(content);
|
const htmlContent = await utils.markdownToHTML(content);
|
||||||
---
|
---
|
||||||
@ -30,18 +28,18 @@ const htmlContent = await utils.markdownToHTML(content);
|
|||||||
title={title}
|
title={title}
|
||||||
siteName={SITE_METADATA.name}
|
siteName={SITE_METADATA.name}
|
||||||
description={description}
|
description={description}
|
||||||
type='article'
|
type="article"
|
||||||
url={canonicalUrl.href}
|
url={canonicalUrl.href}
|
||||||
image={cover_image
|
image={cover_image
|
||||||
? { path: cover_image.filename_disk, width: "700", height: "400", type: cover_image.type }
|
? { 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>
|
||||||
</BaseHead>
|
</BaseHead>
|
||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<div class='container'>
|
<div class="container">
|
||||||
<ArticlePost lang={lang} article={article} availableOnLangs={availableOnLangs}>
|
<ArticlePost lang={lang} article={article}>
|
||||||
<ArticleContent set:html={htmlContent} />
|
<ArticleContent set:html={htmlContent} />
|
||||||
</ArticlePost>
|
</ArticlePost>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
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} />
|
|
||||||
17
src/pages/[blog]/[...url].astro
Normal file
17
src/pages/[blog]/[...url].astro
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import ArticleLayout from "@root/layouts/article.astro";
|
||||||
|
import { fetchArticleTranslationsBySlug, incArticleTranslationViews } from "@/directus";
|
||||||
|
|
||||||
|
const { url } = Astro.params as { blog: string; url: string };
|
||||||
|
const slug = url.split("/").at(-1);
|
||||||
|
|
||||||
|
if (!slug) return Astro.redirect("/404");
|
||||||
|
|
||||||
|
const article = await fetchArticleTranslationsBySlug(slug);
|
||||||
|
|
||||||
|
if (!article) return Astro.redirect("/404");
|
||||||
|
|
||||||
|
if (import.meta.env.PROD) await incArticleTranslationViews(article);
|
||||||
|
---
|
||||||
|
|
||||||
|
<ArticleLayout lang={article.languages_code.code} article={article} />
|
||||||
@ -1,31 +1,19 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from "astro:content";
|
|
||||||
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
|
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
|
||||||
import * as utils from "@utils/index";
|
import * as utils from "@utils/index";
|
||||||
import PageLayout from "@layouts/default.astro";
|
import PageLayout from "@layouts/default.astro";
|
||||||
import PageHeading from "@components/PageHeading.astro";
|
import PageHeading from "@components/PageHeading.astro";
|
||||||
import ArticlesList from "@root/components/ArticlesList";
|
import ArticlesList from "@root/components/ArticlesList";
|
||||||
import directus from "@/directus";
|
import { fetchTags, fetchArticles } from "@/directus";
|
||||||
|
|
||||||
const t = useTranslation(Astro);
|
const t = useTranslation(Astro);
|
||||||
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
const pageLang = getLanguageFromURL(Astro.url.pathname);
|
||||||
// const articles = await getCollection("blog", (e) => !e.data.draft);
|
|
||||||
// const filtered = utils.removeDuplicates(articles, pageLang);
|
|
||||||
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
||||||
const tags = (await directus.query(`query Tag { tag { title } }`)).tag.map((t) => t.title);
|
const tags = (await fetchTags()).map((t) => t.title);
|
||||||
|
const articles = (await fetchArticles()).flatMap((a) =>
|
||||||
const { article_translations } = await directus.query(`
|
utils.removeArticleDuplicates(a.translations, pageLang)
|
||||||
query Article_translations {
|
);
|
||||||
article_translations {
|
const articleBlocks = utils.formatToArticleBlocks(articles);
|
||||||
languages_code {
|
|
||||||
code
|
|
||||||
}
|
|
||||||
slug
|
|
||||||
status
|
|
||||||
pub_date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout
|
<PageLayout
|
||||||
@ -39,7 +27,7 @@ const { article_translations } = await directus.query(`
|
|||||||
<ArticlesList
|
<ArticlesList
|
||||||
client:load
|
client:load
|
||||||
i18n={{ "articles.empty": t("articles.empty"), "input.search": t("input.search") }}
|
i18n={{ "articles.empty": t("articles.empty"), "input.search": t("input.search") }}
|
||||||
articles={utils.formatToArticleBlocks(article_translations)}
|
articles={articleBlocks}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
pageLang={pageLang}
|
pageLang={pageLang}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from "astro:content";
|
|
||||||
import PageLayout from "@layouts/default.astro";
|
import PageLayout from "@layouts/default.astro";
|
||||||
import Splash from "@components/Splash/Splash.astro";
|
import Splash from "@components/Splash/Splash.astro";
|
||||||
import SplashHeading from "@components/Splash/SplashHeading.astro";
|
import SplashHeading from "@components/Splash/SplashHeading.astro";
|
||||||
@ -23,11 +22,11 @@ const t = useTranslation(Astro);
|
|||||||
<p><UIString key={"splash.description"} /></p>
|
<p><UIString key={"splash.description"} /></p>
|
||||||
</SplashHeading>
|
</SplashHeading>
|
||||||
<SplashLinks>
|
<SplashLinks>
|
||||||
<SplashLink icon='information' href='/about/'><UIString key={"splash.about"} /></SplashLink>
|
<SplashLink icon="information" href="/about/"><UIString key={"splash.about"} /></SplashLink>
|
||||||
<SplashLink icon='email_filled' href='/contacts/'>
|
<SplashLink icon="email_filled" href="/contacts/">
|
||||||
<UIString key={"splash.contact"} />
|
<UIString key={"splash.contact"} />
|
||||||
</SplashLink>
|
</SplashLink>
|
||||||
</SplashLinks>
|
</SplashLinks>
|
||||||
</Splash>
|
</Splash>
|
||||||
<RecentArticles />
|
<RecentArticles limit={5} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@ -13,27 +13,27 @@ import rehypeStringify from "rehype-stringify";
|
|||||||
import { escape } from "html-escaper";
|
import { escape } from "html-escaper";
|
||||||
import { h } from "hastscript";
|
import { h } from "hastscript";
|
||||||
import { toString } from "hast-util-to-string";
|
import { toString } from "hast-util-to-string";
|
||||||
import { selectAll } from "hast-util-select";
|
// 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 removeArticleDuplicates(
|
export function removeArticleDuplicates(
|
||||||
articles: ArticleTranslation[] | null,
|
articleTranslations: ArticleTranslation[] | null,
|
||||||
pageLang: LanguageKeys
|
pageLang: LanguageKeys
|
||||||
): ArticleTranslation[] {
|
): ArticleTranslation[] {
|
||||||
if (!articles) return [];
|
if (!articleTranslations) return [];
|
||||||
if (articles && articles.length >= 2) {
|
if (articleTranslations && articleTranslations.length >= 2) {
|
||||||
const pageLangArticles = articles.filter((a) => {
|
const pageLangArticles = articleTranslations.filter((a) => {
|
||||||
return (
|
return (
|
||||||
typeof a.languages_code === "object" && a.languages_code.code.split("-")[0] === pageLang
|
typeof a.languages_code === "object" && a.languages_code.code.split("-")[0] === pageLang
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (pageLangArticles.length !== 0) return pageLangArticles;
|
if (pageLangArticles.length !== 0) return pageLangArticles;
|
||||||
return articles;
|
return articleTranslations;
|
||||||
}
|
}
|
||||||
return articles;
|
return articleTranslations;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,8 +50,9 @@ export function createRelativeArticleUrl(
|
|||||||
return `${lang}/${prefix}/${date}/${slug}`;
|
return `${lang}/${prefix}/${date}/${slug}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatToArticleBlocks(articles: CollectionEntry<"blog">[]) {
|
export function formatToArticleBlocks(articles: ArticleTranslation[]) {
|
||||||
const blocks: Record<string, any[]> = {};
|
const blocks: Record<string, ArticleTranslation[]> = {};
|
||||||
|
|
||||||
articles.forEach((a) => {
|
articles.forEach((a) => {
|
||||||
const year = new Date(a.pub_date).getFullYear();
|
const year = new Date(a.pub_date).getFullYear();
|
||||||
if (!blocks[year]) blocks[year] = [];
|
if (!blocks[year]) blocks[year] = [];
|
||||||
@ -59,7 +60,8 @@ export function formatToArticleBlocks(articles: CollectionEntry<"blog">[]) {
|
|||||||
id: a.id,
|
id: a.id,
|
||||||
title: a.title,
|
title: a.title,
|
||||||
// tags: a.tags,
|
// tags: a.tags,
|
||||||
pubDate: a.pub_date,
|
slug: a.slug,
|
||||||
|
pub_date: a.pub_date,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return blocks;
|
return blocks;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user