Merge branch 'release/2.1.0' into main

This commit is contained in:
Sergey Yarkov 2023-04-16 22:27:51 +03:00
commit 27078bcdc0
No known key found for this signature in database
GPG Key ID: E91CE1FABC619B6F
11 changed files with 139 additions and 60 deletions

View File

@ -11,6 +11,7 @@ Personal website created with Astro.
- [x] Search articles by title or description - [x] Search articles by title or description
- [x] Dark/Light Theme - [x] Dark/Light Theme
- [x] Multilang web-site - [x] Multilang web-site
- [x] Table of contents
- [x] Content management via [NetlifyCMS](https://www.netlifycms.org/) - [x] Content management via [NetlifyCMS](https://www.netlifycms.org/)
- [x] Prefetch pages via [Quicklink](https://getquick.link/) - [x] Prefetch pages via [Quicklink](https://getquick.link/)
- [x] Form handling via [Netlify Forms](https://www.netlify.com/products/forms/) - [x] Form handling via [Netlify Forms](https://www.netlify.com/products/forms/)

View File

@ -1,7 +1,7 @@
{ {
"name": "yarkov.tech", "name": "yarkov.tech",
"private": false, "private": false,
"version": "2.0.0", "version": "2.1.0",
"description": "Personal website created with Astro.", "description": "Personal website created with Astro.",
"author": { "author": {
"name": "Sergey Yarkov", "name": "Sergey Yarkov",

View File

@ -2,6 +2,7 @@
<style lang="scss" is:global> <style lang="scss" is:global>
.article-content { .article-content {
margin-top: 70px;
& h1, & h1,
h2, h2,
h3, h3,
@ -42,19 +43,6 @@
quotes: "« " " »"; quotes: "« " " »";
} }
& a {
color: var(--font-color);
text-decoration: underline;
text-decoration-color: var(--color-blue-500);
text-underline-offset: 2.2px;
text-decoration-thickness: 2px;
font-weight: 500;
&:hover {
color: var(--color-blue-500);
text-decoration-thickness: 2px;
}
}
& figure { & figure {
margin: 0 0 1em; margin: 0 0 1em;
& figcaption { & figcaption {
@ -121,15 +109,17 @@
} }
} }
& li { & ul {
list-style: disc;
& li { & li {
list-style-type: circle; list-style: disc;
} & li {
&::marker { list-style-type: circle;
color: var(--color-blue-500); }
font-size: 1.3rem; &::marker {
line-height: var(--line-height-none); color: var(--color-blue-500);
font-size: 1.3rem;
line-height: var(--line-height-none);
}
} }
} }

View File

@ -5,15 +5,17 @@ import { createDateFormatter } from "@root/utils";
import ArticleCover from "@components/ArticleCover.astro"; import ArticleCover from "@components/ArticleCover.astro";
import ArticleComments from "@components/ArticleComments.astro"; 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 { useTranslation, getLanguageFromURL } from "@i18n/utils"; import { useTranslation, getLanguageFromURL } from "@i18n/utils";
import * as utils from "@utils/index"; import * as utils from "@utils/index";
export type Props = { export type Props = {
article: CollectionEntry<"blog">; article: CollectionEntry<"blog">;
availableOnLangs?: CollectionEntry<"blog">[];
lang: string; lang: string;
}; };
const { article } = 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 {
@ -63,6 +65,7 @@ const t = useTranslation(Astro);
{description} {description}
</p> </p>
</div> </div>
{availableOnLangs && <AvailableOnLangs articles={availableOnLangs} />}
</header> </header>
<slot /> <slot />
<ArticleComments url={canonicalUrl.href} id={slug} /> <ArticleComments url={canonicalUrl.href} id={slug} />
@ -119,9 +122,8 @@ const t = useTranslation(Astro);
flex-wrap: wrap; flex-wrap: wrap;
column-gap: 7px; column-gap: 7px;
row-gap: 7px; row-gap: 7px;
& li:not(:last-child)::after { & li:not(:last-child) a::after {
content: ","; content: ",";
color: var(--color-blue-400);
} }
} }
} }
@ -132,6 +134,19 @@ const t = useTranslation(Astro);
} }
} }
& a {
color: var(--font-color);
text-decoration: underline;
text-decoration-color: var(--color-blue-500);
text-underline-offset: 2.2px;
text-decoration-thickness: 2px;
font-weight: 500;
&:hover {
color: var(--color-blue-500);
text-decoration-thickness: 2px;
}
}
& .cover { & .cover {
margin: 20px 0; margin: 20px 0;
& figure { & figure {

View File

@ -0,0 +1,42 @@
---
import type { CollectionEntry } from "astro:content";
import { getLanguageFromURL, useTranslation } from "@root/i18n/utils";
import { URL_BLOG_PREFIX } from "@root/constants";
import { createRelativeArticleUrl } from "@root/utils";
import languages from "@i18n/languages";
export interface Props {
articles: CollectionEntry<"blog">[];
}
const pageLang = getLanguageFromURL(Astro.url.pathname);
const t = useTranslation(Astro);
const { articles } = Astro.props;
---
{
articles.length !== 1 && (
<div>
{t("article.availableOn")}:{" "}
{articles
.filter((a) => !a.id.startsWith(`${pageLang}/`))
.map(({ id, data: { title, pubDate } }) => (
<a href={`${createRelativeArticleUrl({ id, title, pubDate }, URL_BLOG_PREFIX)}`}>{`${
languages[id.split("/")[0] as LanguageKeys]
}`}</a>
))}
</div>
)
}
<style lang="scss">
div {
margin: 25px 0;
font-size: var(--font-size-lg);
& a {
&:not(:last-child)::after {
content: ",";
}
}
}
</style>

View File

@ -2,22 +2,62 @@
import { buildToc } from "@root/utils"; import { buildToc } from "@root/utils";
import type { MarkdownHeading } from "astro"; import type { MarkdownHeading } from "astro";
import TableOfContentsHeading from "./TableOfContentsHeading.astro"; import TableOfContentsHeading from "./TableOfContentsHeading.astro";
import { useTranslation } from "@root/i18n/utils";
export interface Props { export interface Props {
headings: MarkdownHeading[]; headings: MarkdownHeading[];
} }
const toc = buildToc(Astro.props.headings); const toc = buildToc(Astro.props.headings, 3);
const t = useTranslation(Astro);
--- ---
<nav class="toc"> <details class="toc">
<h2>Содержание</h2> <summary>{t("article.tableOfContents")}</summary>
<ul> <nav>
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)} <div>
</ul> <ul>
</nav> {toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
</ul>
</div>
</nav>
</details>
<script>
var $toc = document.querySelector(".toc");
if ($toc) {
$toc.addEventListener(
"click",
(e) => (e.target as HTMLElement).tagName === "A" && $toc!.removeAttribute("open")
);
}
</script>
<style lang="scss" is:global> <style lang="scss" is:global>
.toc { .toc {
cursor: pointer;
display: block;
position: absolute;
& summary {
font-size: var(--font-size-lg);
}
& nav {
background-color: var(--background-card);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-lg);
margin-top: 10px;
padding: 10px 16px;
& ul {
& a {
display: block;
margin-bottom: 10px;
color: var(--font-color);
text-decoration: none;
}
& li ul {
margin-left: 15px;
}
}
}
} }
</style> </style>

View File

@ -4,6 +4,7 @@ export default {
"Here I publish articles and tutorials on various IT topics, and I also talk about my projects that I have done over the past few years.", "Here I publish articles and tutorials on various IT topics, and I also talk about my projects that I have done over the past few years.",
"site.underConstruction": "This web-site is under development...", "site.underConstruction": "This web-site is under development...",
"article.availableOn": "Available on", "article.availableOn": "Available on",
"article.tableOfContents": "Table of contents",
"articles.empty": "No articles found...", "articles.empty": "No articles found...",
"articlePost.comments": "Comments", "articlePost.comments": "Comments",
"article.views": "views", "article.views": "views",

View File

@ -4,6 +4,7 @@ export default {
"Здесь я публикую статьи и туториалы на разные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.", "Здесь я публикую статьи и туториалы на разные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.",
"site.underConstruction": "Этот веб-сайт находится в разработке...", "site.underConstruction": "Этот веб-сайт находится в разработке...",
"article.availableOn": "Доступно на", "article.availableOn": "Доступно на",
"article.tableOfContents": "Оглавление",
"articles.empty": "Не найдено ни одной статьи...", "articles.empty": "Не найдено ни одной статьи...",
"articlePost.comments": "Комментарии", "articlePost.comments": "Комментарии",
"article.views": "просмотров", "article.views": "просмотров",

View File

@ -8,15 +8,18 @@ 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";
export interface Props { export interface Props {
lang?: string | undefined; lang?: string | undefined;
article: CollectionEntry<"blog">; article: 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 } = Astro.props as Props; const t = useTranslation(Astro);
const { lang = DEFAULT_LANGUAGE, article, headings, availableOnLangs } = Astro.props as Props;
const { const {
data: { title, description, coverImage }, data: { title, description, coverImage },
} = article; } = article;
@ -39,8 +42,8 @@ const {
<BaseLayout> <BaseLayout>
<div class="container"> <div class="container">
<!-- <TableOfContents headings={headings} /> --> <ArticlePost lang={lang} article={article} availableOnLangs={availableOnLangs}>
<ArticlePost lang={lang} article={article}> <TableOfContents headings={headings} />
<ArticleContent> <ArticleContent>
<slot /> <slot />
</ArticleContent> </ArticleContent>

View File

@ -3,11 +3,6 @@ import { CollectionEntry, getCollection } from "astro:content";
import ArticleLayout from "@root/layouts/article.astro"; import ArticleLayout from "@root/layouts/article.astro";
import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils"; import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils";
import { getLanguageFromURL, useTranslation } from "@i18n/utils"; import { getLanguageFromURL, useTranslation } from "@i18n/utils";
import { URL_BLOG_PREFIX } from "@root/constants";
import languages from "@i18n/languages";
const t = useTranslation(Astro);
const pageLang = getLanguageFromURL(Astro.url.pathname);
/** /**
* Generate articles pages from `blog` collection. * Generate articles pages from `blog` collection.
@ -26,7 +21,7 @@ export async function getStaticPaths() {
params: { blog: url.slice(1) }, params: { blog: url.slice(1) },
props: { props: {
article, article,
availableOnLang: collection.filter((e) => availableOnLangs: collection.filter((e) =>
e.id.includes(removeLanguageCodeFromPath(article.id)) e.id.includes(removeLanguageCodeFromPath(article.id))
), ),
}, },
@ -34,26 +29,17 @@ export async function getStaticPaths() {
}); });
} }
type Props = { article: CollectionEntry<"blog">; availableOnLang: CollectionEntry<"blog">[] }; type Props = { article: CollectionEntry<"blog">; availableOnLangs: CollectionEntry<"blog">[] };
const { Content, headings } = await Astro.props.article.render(); const { Content, headings } = await Astro.props.article.render();
const { article, availableOnLang } = Astro.props; const { article, availableOnLangs } = Astro.props;
--- ---
<ArticleLayout lang={article.id.split("/")[0]} article={article} headings={headings}> <ArticleLayout
{ lang={article.id.split("/")[0]}
availableOnLang.length > 1 && ( article={article}
<div class="languages-available"> headings={headings}
{t("article.availableOn")}:{" "} availableOnLangs={availableOnLangs}
{availableOnLang >
.filter((a) => !a.id.startsWith(`${pageLang}/`))
.map(({ id, data: { title, pubDate } }) => (
<a href={`${createRelativeArticleUrl({ id, title, pubDate }, URL_BLOG_PREFIX)}`}>{`${
languages[id.split("/")[0] as LanguageKeys]
}`}</a>
))}
</div>
)
}
<Content /> <Content />
</ArticleLayout> </ArticleLayout>

View File

@ -66,7 +66,7 @@ export function getUniqueTags(articles: CollectionEntry<"blog">[]): string[] {
export type MarkdownHeadingToc = MarkdownHeading & { subheadings: MarkdownHeading[] }; export type MarkdownHeadingToc = MarkdownHeading & { subheadings: MarkdownHeading[] };
export function buildToc(headings: MarkdownHeading[]) { export function buildToc(headings: MarkdownHeading[], maxDepth: number = 2) {
const toc: MarkdownHeadingToc[] = []; const toc: MarkdownHeadingToc[] = [];
const parentHeadings = new Map(); const parentHeadings = new Map();
headings.forEach((h) => { headings.forEach((h) => {
@ -74,7 +74,7 @@ export function buildToc(headings: MarkdownHeading[]) {
parentHeadings.set(heading.depth, heading); parentHeadings.set(heading.depth, heading);
if (heading.depth === 1) { if (heading.depth === 1) {
toc.push(heading); toc.push(heading);
} else { } else if (heading.depth <= maxDepth) {
parentHeadings.get(heading.depth - 1).subheadings.push(heading); parentHeadings.get(heading.depth - 1).subheadings.push(heading);
} }
}); });