mirror of
https://github.com/sergeyyarkov/yarkov.tech.git
synced 2026-10-11 10:29:21 +03:00
feat: added table of contents
This commit is contained in:
parent
f9d9c7f611
commit
464b6c5c9c
@ -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,6 +109,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& ul {
|
||||||
& li {
|
& li {
|
||||||
list-style: disc;
|
list-style: disc;
|
||||||
& li {
|
& li {
|
||||||
@ -132,6 +121,7 @@
|
|||||||
line-height: var(--line-height-none);
|
line-height: var(--line-height-none);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& blockquote {
|
& blockquote {
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
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 { 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>
|
||||||
|
<nav>
|
||||||
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</nav>
|
</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>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": "просмотров",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user