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] Dark/Light Theme
- [x] Multilang web-site
- [x] Table of contents
- [x] Content management via [NetlifyCMS](https://www.netlifycms.org/)
- [x] Prefetch pages via [Quicklink](https://getquick.link/)
- [x] Form handling via [Netlify Forms](https://www.netlify.com/products/forms/)

View File

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

View File

@ -2,6 +2,7 @@
<style lang="scss" is:global>
.article-content {
margin-top: 70px;
& h1,
h2,
h3,
@ -42,19 +43,6 @@
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 {
margin: 0 0 1em;
& figcaption {
@ -121,6 +109,7 @@
}
}
& ul {
& li {
list-style: disc;
& li {
@ -132,6 +121,7 @@
line-height: var(--line-height-none);
}
}
}
& blockquote {
margin: 1.5rem 0;

View File

@ -5,15 +5,17 @@ import { createDateFormatter } from "@root/utils";
import ArticleCover from "@components/ArticleCover.astro";
import ArticleComments from "@components/ArticleComments.astro";
import TranslatedLink from "@components/TranslatedLink.astro";
import AvailableOnLangs from "@components/AvailableOnLangs.astro";
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
import * as utils from "@utils/index";
export type Props = {
article: CollectionEntry<"blog">;
availableOnLangs?: CollectionEntry<"blog">[];
lang: string;
};
const { article } = Astro.props as Props;
const { article, availableOnLangs } = Astro.props as Props;
const pageLang = getLanguageFromURL(Astro.url.pathname);
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const {
@ -63,6 +65,7 @@ const t = useTranslation(Astro);
{description}
</p>
</div>
{availableOnLangs && <AvailableOnLangs articles={availableOnLangs} />}
</header>
<slot />
<ArticleComments url={canonicalUrl.href} id={slug} />
@ -119,9 +122,8 @@ const t = useTranslation(Astro);
flex-wrap: wrap;
column-gap: 7px;
row-gap: 7px;
& li:not(:last-child)::after {
& li:not(:last-child) a::after {
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 {
margin: 20px 0;
& 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 type { MarkdownHeading } from "astro";
import TableOfContentsHeading from "./TableOfContentsHeading.astro";
import { useTranslation } from "@root/i18n/utils";
export interface Props {
headings: MarkdownHeading[];
}
const toc = buildToc(Astro.props.headings);
const toc = buildToc(Astro.props.headings, 3);
const t = useTranslation(Astro);
---
<nav class="toc">
<h2>Содержание</h2>
<details class="toc">
<summary>{t("article.tableOfContents")}</summary>
<nav>
<div>
<ul>
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
</ul>
</nav>
</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>
.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>

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.",
"site.underConstruction": "This web-site is under development...",
"article.availableOn": "Available on",
"article.tableOfContents": "Table of contents",
"articles.empty": "No articles found...",
"articlePost.comments": "Comments",
"article.views": "views",

View File

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

View File

@ -8,15 +8,18 @@ import ArticleContent from "@components/ArticleContent.astro";
import ArticlePost from "@components/ArticlePost.astro";
import OpenGraph from "@components/OpenGraph.astro";
import TableOfContents from "@root/components/TableOfContents.astro";
import { useTranslation } from "@root/i18n/utils";
export interface Props {
lang?: string | undefined;
article: CollectionEntry<"blog">;
availableOnLangs?: CollectionEntry<"blog">[];
headings: MarkdownHeading[];
}
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 {
data: { title, description, coverImage },
} = article;
@ -39,8 +42,8 @@ const {
<BaseLayout>
<div class="container">
<!-- <TableOfContents headings={headings} /> -->
<ArticlePost lang={lang} article={article}>
<ArticlePost lang={lang} article={article} availableOnLangs={availableOnLangs}>
<TableOfContents headings={headings} />
<ArticleContent>
<slot />
</ArticleContent>

View File

@ -3,11 +3,6 @@ import { CollectionEntry, getCollection } from "astro:content";
import ArticleLayout from "@root/layouts/article.astro";
import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/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.
@ -26,7 +21,7 @@ export async function getStaticPaths() {
params: { blog: url.slice(1) },
props: {
article,
availableOnLang: collection.filter((e) =>
availableOnLangs: collection.filter((e) =>
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 { article, availableOnLang } = Astro.props;
const { article, availableOnLangs } = Astro.props;
---
<ArticleLayout lang={article.id.split("/")[0]} article={article} headings={headings}>
{
availableOnLang.length > 1 && (
<div class="languages-available">
{t("article.availableOn")}:{" "}
{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>
)
}
<ArticleLayout
lang={article.id.split("/")[0]}
article={article}
headings={headings}
availableOnLangs={availableOnLangs}
>
<Content />
</ArticleLayout>

View File

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