mirror of
https://github.com/sergeyyarkov/yarkov.tech.git
synced 2026-10-11 10:29:21 +03:00
Merge branch 'release/2.1.0' into main
This commit is contained in:
commit
27078bcdc0
@ -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/)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
42
src/components/AvailableOnLangs.astro
Normal file
42
src/components/AvailableOnLangs.astro
Normal 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>
|
||||
@ -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>
|
||||
</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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -4,6 +4,7 @@ export default {
|
||||
"Здесь я публикую статьи и туториалы на разные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.",
|
||||
"site.underConstruction": "Этот веб-сайт находится в разработке...",
|
||||
"article.availableOn": "Доступно на",
|
||||
"article.tableOfContents": "Оглавление",
|
||||
"articles.empty": "Не найдено ни одной статьи...",
|
||||
"articlePost.comments": "Комментарии",
|
||||
"article.views": "просмотров",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user