replaced with markdown-to

This commit is contained in:
Sergey Yarkov 2025-12-18 18:34:56 +03:00
parent 838a74c086
commit 00d32bb002
21 changed files with 392 additions and 1542 deletions

0
.dockerfile Normal file
View File

0
.dockerignore Normal file
View File

View File

@ -14,6 +14,7 @@ export default defineConfig({
output: "server",
server: {
port: 9966,
host: true,
allowedHosts: ["showcase.yarkov.tech"],
},
adapter: node({

0
compose.yml Normal file
View File

0
nginx/website.conf Normal file
View File

1343
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "astro dev --host",
"build": "astro check && astro build",
"dev": "astro dev",
"build": "astro build",
"start": "node --env-file=.env ./dist/server/entry.mjs",
"preview": "astro preview",
"astro": "astro",
@ -25,6 +25,7 @@
"@astrojs/rss": "^4.0.13",
"@astrojs/solid-js": "^5.1.3",
"@iconify-json/mdi": "^1.2.3",
"@types/markdown-it": "^14.1.2",
"astro": "^5.15.8",
"astro-icon": "^1.1.5",
"graphql": "^16.12.0",
@ -32,18 +33,13 @@
"hast-util-to-string": "^3.0.0",
"hastscript": "^8.0.0",
"html-escaper": "^3.0.3",
"markdown-it": "^14.1.0",
"markdown-it-prism": "^3.0.1",
"mime-types": "^2.1.35",
"prismjs": "^1.30.0",
"quicklink": "^3.0.1",
"rehype-autolink-headings": "^7.0.0",
"rehype-slug": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-parse": "^11.0.0",
"remark-prism": "^1.3.6",
"remark-rehype": "^11.1.2",
"remark-toc": "^9.0.0",
"sharp": "^0.32.6",
"solid-js": "^1.7.12",
"unified": "^11.0.5"
"solid-js": "^1.7.12"
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.15.1",

View File

@ -0,0 +1,12 @@
---
import { getLanguageFromURL } from "@i18n/utils";
import { getAboutMe } from "../queries/global";
import { markdownToHTML } from "../utils/markdown";
import ArticleContent from "@components/ArticleContent.astro";
const pageLang = getLanguageFromURL(Astro.url.pathname);
const data = await getAboutMe(pageLang);
const aboutMeHtmlContent = await markdownToHTML(data?.about || "");
---
<ArticleContent set:html={aboutMeHtmlContent} />

View File

@ -1,12 +1,10 @@
---
export interface Props {
counter: number;
}
import { getSiteSettings } from '@/src/queries';
const { counter } = Astro.props as Props
const siteSettings = await getSiteSettings();
---
<script type="text/javascript" define:vars={{ counter }} is:inline>
{siteSettings ? <script type="text/javascript" define:vars={{ counter: siteSettings.ym_counter }} is:inline>
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
var z = null;m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
@ -16,6 +14,6 @@ const { counter } = Astro.props as Props
ym(counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce:true
accurateTrackBounce: true,
});
</script>
</script> : null}

View File

@ -11,7 +11,7 @@ const { url, id } = Astro.props as Props;
<div id="disqus_thread"></div>
</footer>
<script define:vars={{ url, id }}>
<script is:inline define:vars={{ url, id }}>
var disqus_config = function () {
this.page.url = url;
this.page.identifier = id;
@ -25,9 +25,9 @@ const { url, id } = Astro.props as Props;
((function () {
var e = document,
t = e.createElement("script");
(t.src = "https://yarkov-tech.disqus.com/embed.js"),
((t.src = "https://yarkov-tech.disqus.com/embed.js"),
t.setAttribute("data-timestamp", +new Date()),
(e.head || e.body).appendChild(t);
(e.head || e.body).appendChild(t));
})(),
t.unobserve(e.target));
});
@ -39,9 +39,6 @@ const { url, id } = Astro.props as Props;
"onthemetoggled",
() =>
document.readyState == "complete" &&
setTimeout(
() => "DISQUS" in window && DISQUS.reset({ reload: true, config: disqus_config }),
100
)
setTimeout(() => "DISQUS" in window && DISQUS.reset({ reload: true, config: disqus_config }), 100)
);
</script>

View File

@ -1,10 +1,8 @@
---
import "@styles/global.scss";
import siteconfig from "siteconfig.json";
import { SITE_METADATA } from "@root/constants";
import YaMetrika from "./Analytics/YaMetrika.astro";
import { useTranslation } from "@root/i18n/utils";
import { getSiteSettings } from "../queries";
export interface Props {
title?: string;
@ -14,60 +12,59 @@ export interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description } = Astro.props;
const siteSettings = await getSiteSettings();
const isDev = import.meta.env.DEV;
const t = useTranslation(Astro);
---
<head>
<!-- Fonts -->
<link rel='preconnect' href='https://fonts.googleapis.com' />
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href='https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono&display=swap'
rel='preload'
as='style'
onload="this.rel='stylesheet'"
href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono&display=swap"
rel="preload"
as="style"
onload={`this.rel='stylesheet'`}
/>
<!-- Canonical URL -->
<link rel='canonical' href={canonicalURL.href} />
<link rel="canonical" href={canonicalURL.href} />
<!-- Base metadata -->
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title || SITE_METADATA.title}</title>
<!-- Metadata -->
<meta name='title' content={title || SITE_METADATA.title} />
<meta name='generator' content={Astro.generator} />
<meta name='description' content={description || t("site.description")} />
<meta name='theme-color' content={SITE_METADATA.themeColor} />
<meta name='msapplication-TileColor' content='#da532c' />
<meta name="title" content={title || SITE_METADATA.title} />
<meta name="generator" content={Astro.generator} />
<meta name="description" content={description || t("site.description")} />
<meta name="theme-color" content={SITE_METADATA.themeColor} />
<meta name="msapplication-TileColor" content="#da532c" />
<!-- Favicons -->
<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link
rel='icon'
type='image/png'
sizes='32x32'
href='/favicon-light-32x32.png'
media='(prefers-color-scheme: light)'
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-light-32x32.png"
media="(prefers-color-scheme: light)"
/>
<link
rel='icon'
type='image/png'
sizes='16x16'
href='/favicon-light-16x16.png'
media='(prefers-color-scheme: light)'
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-light-16x16.png"
media="(prefers-color-scheme: light)"
/>
<link rel='icon' type='image/png' sizes='32x32' href='/favicon-dark-32x32.png' media='(prefers-color-scheme: dark)' />
<link rel='icon' type='image/png' sizes='16x16' href='/favicon-dark-16x16.png' media='(prefers-color-scheme: dark)' />
<link rel='manifest' href='/site.webmanifest' />
<link rel='mask-icon' href='/safari-pinned-tab.svg' color='#5bbad5' />
<link rel='sitemap' href='/sitemap-index.xml' />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-dark-32x32.png" media="(prefers-color-scheme: dark)" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-dark-16x16.png" media="(prefers-color-scheme: dark)" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="sitemap" href="/sitemap-index.xml" />
<slot />
<script is:inline src='/assets/js/detect-theme.js'></script>
<script src='../quicklink.js'></script>
{!isDev && siteSettings?.ym_counter && <YaMetrika counter={siteSettings.ym_counter} />}
<script is:inline src="/assets/js/detect-theme.js"></script>
<!-- <script src="../quicklink.js"></script> -->
{!isDev && <YaMetrika />}
</head>

View File

@ -1,24 +1,21 @@
---
import { Icon } from "astro-icon/components";
import { IS_SITE_UNDER_CONSTRUCTION, SITE_METADATA } from "@root/constants";
import ThemeSwitcher from "@components/ThemeSwticher";
import LanguageSelect from "@components/LanguageSelect.astro";
import NavToggle from "@components/NavToggle";
import UIString from "@components/UIString.astro";
import TranslatedLink from "@components/TranslatedLink.astro";
import { getContacts } from "../queries";
const contacts = await getContacts();
import HeaderContacts from "./HeaderContacts.astro";
---
<header {...IS_SITE_UNDER_CONSTRUCTION ? { style: "top: 45px;" } : {}}>
<div class='container'>
<div class='logo'>
<TranslatedLink to='/'>
<div class="container">
<div class="logo">
<TranslatedLink to="/">
{SITE_METADATA.name}
</TranslatedLink>
</div>
<nav itemscope itemtype='http://schema.org/SiteNavigationElement'>
<nav itemscope itemtype="http://schema.org/SiteNavigationElement">
<ul>
{
SITE_METADATA.nav.map((link) => (
@ -26,32 +23,16 @@ const contacts = await getContacts();
<TranslatedLink
itemprop={"url"}
to={link.url}
class:list={[{ active: Astro.url.pathname.includes(link.url) }]}>
class:list={[{ active: Astro.url.pathname.includes(link.url) }]}
>
<UIString key={link.key} />
</TranslatedLink>
</li>
))
}
</ul>
{
contacts && (
<div class='contacts'>
<a target='_blank' href={"mailto:" + contacts.email}>
Email
<Icon name='email' width='18' height='18' />
</a>
<a target='_blank' href={contacts.github}>
Github
<Icon name='github_filled' width='18' height='18' />
</a>
<a target='_blank' href={contacts.telegram}>
Telegram
<Icon name='telegram' width='18' height='18' />
</a>
</div>
)
}
<div class='flex settings'>
<HeaderContacts />
<div class="flex settings">
<LanguageSelect />
<ThemeSwitcher client:visible />
</div>
@ -60,7 +41,7 @@ const contacts = await getContacts();
</div>
</header>
<style lang='scss'>
<style lang="scss">
header {
position: sticky;
top: 0;
@ -106,19 +87,6 @@ const contacts = await getContacts();
text-transform: lowercase;
}
}
& .contacts {
display: flex;
align-items: center;
column-gap: 10px;
margin-right: 30px;
& a {
text-decoration: none;
font-size: 0;
&:hover {
opacity: var(--opacity-hover);
}
}
}
& .settings {
column-gap: 13.5px;
}
@ -164,17 +132,6 @@ const contacts = await getContacts();
}
}
}
& .contacts {
margin-right: 0;
margin-top: 2.5rem;
& a {
margin-right: 0.2rem;
}
& svg {
width: 21px;
height: 21px;
}
}
}
}
}

View File

@ -0,0 +1,54 @@
---
import { Icon } from "astro-icon/components";
import { getContacts } from "../queries";
const contacts = await getContacts();
---
{
contacts && (
<div class="contacts">
<a target="_blank" href={"mailto:" + contacts.email}>
Email
<Icon name="email" width="18" height="18" />
</a>
<a target="_blank" href={contacts.github}>
Github
<Icon name="github_filled" width="18" height="18" />
</a>
<a target="_blank" href={contacts.telegram}>
Telegram
<Icon name="telegram" width="18" height="18" />
</a>
</div>
)
}
<style lang="scss">
.contacts {
display: flex;
align-items: center;
column-gap: 10px;
margin-right: 30px;
& a {
text-decoration: none;
font-size: 0;
&:hover {
opacity: var(--opacity-hover);
}
}
}
@media screen and (max-width: 768px) {
.contacts {
margin-right: 0;
margin-top: 2.5rem;
& a {
margin-right: 0.2rem;
}
& svg {
width: 21px;
height: 21px;
}
}
}
</style>

View File

@ -19,7 +19,7 @@ const lang = getLanguageFromURL(Astro.url.pathname);
</select>
</div>
<script define:vars={{ lang, DEFAULT_LANGUAGE, URL_BLOG_PREFIX }}>
<script is:inline define:vars={{ lang, DEFAULT_LANGUAGE, URL_BLOG_PREFIX }}>
const $select = document.getElementById("languageSelect");
const changeLanguageHandler = (e) => {
@ -37,8 +37,7 @@ const lang = getLanguageFromURL(Astro.url.pathname);
/* Move user to articles page with new language */
if (window.location.pathname.includes(`/${URL_BLOG_PREFIX}/`)) {
window.location.pathname =
`${newLang === DEFAULT_LANGUAGE ? "" : newLang + "/"}` + "articles/";
window.location.pathname = `${newLang === DEFAULT_LANGUAGE ? "" : newLang + "/"}` + "articles/";
return;
}

View File

@ -1,69 +0,0 @@
---
import { Icon } from "astro-icon/components";
import type { CollectionEntry } from "astro:content";
export interface Props {
data: CollectionEntry<"skills">["data"];
}
const {
data: { title, list },
} = Astro.props as Props;
---
<div class="skills-block">
<div>
<h3>{title}</h3>
</div>
<div>
<ul>
{
list.map((entry) => (
<li>
{entry.icon && <Icon name={entry.icon} width="20" height="20" />}
{entry.title}
</li>
))
}
</ul>
</div>
</div>
<style lang="scss">
.skills-block {
& h3 {
font-size: var(--font-size-md);
color: var(--font-color-grayer);
font-weight: 500;
}
& ul {
margin: 0;
padding: 0;
list-style: none;
& li {
font-weight: 400;
margin-bottom: 10px;
font-size: var(--font-size-sm);
display: flex;
align-items: center;
column-gap: 5px;
}
}
}
@media screen and (max-width: 500px) {
.skills-block {
& h3 {
text-align: center;
}
& ul {
display: flex;
flex-direction: column;
align-items: center;
}
}
}
</style>

View File

@ -1,20 +0,0 @@
---
---
<div class="skills-list"><slot /></div>
<style lang="scss">
.skills-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
column-gap: 40px;
row-gap: 30px;
margin-top: 5rem;
}
@media screen and (max-width: 500px) {
.skills-list {
display: flex;
flex-direction: column;
}
}
</style>

View File

@ -1,5 +1,4 @@
---
import type { MarkdownHeading } from "astro";
import { DEFAULT_LANGUAGE, SITE_METADATA } from "@root/constants";
import BaseHead from "@components/BaseHead.astro";
import BaseLayout from "@components/BaseLayout.astro";

View File

@ -2,79 +2,42 @@ import type { APIContext } from "astro";
// import { getCollection } from 'astro:content';
export async function GET(context: APIContext) {
return new Response("sitemap", { status: 200 });
const site = "https://yarkov.tech";
const urls: Array<{ loc: string; changefreq?: string; priority?: number }> = [];
urls.push(
{ loc: `${site}/`, changefreq: "weekly", priority: 1.0 },
{ loc: `${site}/articles/`, changefreq: "weekly", priority: 0.9 },
{ loc: `${site}/projects/`, changefreq: "monthly", priority: 0.8 },
{ loc: `${site}/about/`, changefreq: "monthly", priority: 0.7 }
);
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls
.map(
(url) => `<url>
<loc>${url.loc}</loc>${
url.changefreq
? `
<changefreq>${url.changefreq}</changefreq>`
: ""
}${
url.priority !== undefined
? `
<priority>${url.priority}</priority>`
: ""
}
</url>`
)
.join("\n")}
</urlset>`;
// export async function GET(context: APIContext) {
// const site = context.site?.toString() || 'https://yourdomain.com';
// // Get all collections
// const posts = await getCollection('blog');
// const projects = await getCollection('projects');
// // Generate URLs for all content
// const urls: Array<{ loc: string; changefreq?: string; priority?: number }> = [];
// // Static pages
// urls.push(
// { loc: `${site}/`, changefreq: 'weekly', priority: 1.0 },
// { loc: `${site}/blog/`, changefreq: 'weekly', priority: 0.9 },
// { loc: `${site}/projects/`, changefreq: 'monthly', priority: 0.8 },
// { loc: `${site}/about/`, changefreq: 'monthly', priority: 0.7 },
// );
// // Blog posts
// posts
// .filter(post => !post.data.draft)
// .forEach(post => {
// urls.push({
// loc: `${site}/blog/${post.slug}/`,
// changefreq: 'monthly',
// priority: 0.8
// });
// });
// // Projects
// projects
// .filter(project => !project.data.draft)
// .forEach(project => {
// urls.push({
// loc: `${site}/projects/${project.slug}/`,
// changefreq: 'monthly',
// priority: 0.7
// });
// });
// // Get all unique tags
// const allTags = new Set<string>();
// posts.forEach(post => {
// post.data.tags?.forEach(tag => allTags.add(tag));
// });
// // Add tag pages
// allTags.forEach(tag => {
// urls.push({
// loc: `${site}/tags/${encodeURIComponent(tag)}/`,
// changefreq: 'weekly',
// priority: 0.5
// });
// });
// // Generate sitemap XML
// const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
// <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
// ${urls.map(url => ` <url>
// <loc>${url.loc}</loc>${url.changefreq ? `
// <changefreq>${url.changefreq}</changefreq>` : ''}${url.priority !== undefined ? `
// <priority>${url.priority}</priority>` : ''}
// </url>`).join('\n')}
// </urlset>`;
// return new Response(sitemap, {
// status: 200,
// headers: {
// 'Content-Type': 'application/xml',
// 'Cache-Control': 'public, max-age=3600' // Cache for 1 hour
// }
// });
// }
return new Response(sitemap, {
status: 200,
headers: {
"Content-Type": "application/xml",
"Cache-Control": "public, max-age=3600",
},
});
}

View File

@ -2,23 +2,15 @@
import { getLanguageFromURL, useTranslation } from "@i18n/utils";
import PageLayout from "@layouts/default.astro";
import PageHeading from "@components/PageHeading.astro";
import ArticleContent from "@components/ArticleContent.astro";
import { getAboutMe } from "../queries/global";
import { markdownToHTML } from "../utils/markdown";
import AboutMeContent from "@components/AboutMeContent.astro";
const pageLang = getLanguageFromURL(Astro.url.pathname);
const data = await getAboutMe(pageLang);
if (!data?.about) return Astro.redirect("/404", 404);
const aboutMeHtmlContent = await markdownToHTML(data.about);
const t = useTranslation(Astro);
---
<PageLayout noIndex lang={pageLang} title={t("pageTitle.about")}>
<PageHeading heading={t("pageTitle.about")} />
<section>
<ArticleContent set:html={aboutMeHtmlContent} />
<!-- <SkillsList>
{skills.map((block) => <SkillsBlock data={block.data} />)}
</SkillsList> -->
<AboutMeContent />
</section>
</PageLayout>

View File

@ -1,6 +1,6 @@
import type { MarkdownHeading } from "astro";
import { DEFAULT_LANGUAGE } from "@root/constants";
import { ArticleQuery, RecentArticleQuery } from "../graphql/graphql";
import { ArticleQuery } from "../graphql/graphql";
/**
* If there is a translation of an article in the current language of the page,

View File

@ -1,65 +1,64 @@
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkPrism from "remark-prism";
import remarkToc from "remark-toc";
import remarkRehype from "remark-rehype";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeSlug from "rehype-slug";
import rehypeStringify from "rehype-stringify";
import { escape } from "html-escaper";
import { h } from "hastscript";
import { toString } from "hast-util-to-string";
// import { escape } from "html-escaper";
// import { h } from "hastscript";
// import { toString } from "hast-util-to-string";
import markdownit from "markdown-it";
import prism from "markdown-it-prism";
// import { selectAll } from "hast-util-select";
const md = markdownit({
html: true,
}).use(prism);
// const createAnchorLabel = (heading: string) => {
// const node = h("span.anchor-label", escape(heading));
// if (node.properties) node.properties["is:raw"] = true;
// return node;
// };
// const processor = unified()
// .use(remarkParse)
// .use(remarkToc, { heading: "(table[ -]of[ -])?contents?|toc|содержание", maxDepth: 2 })
// .use(remarkPrism as any)
// .use(remarkRehype, { allowDangerousHtml: true })
// .use(rehypeSlug)
// .use(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)),
// ],
// });
export async function markdownToHTML(content: string): Promise<string> {
const createAnchorLabel = (heading: string) => {
const node = h("span.anchor-label", escape(heading));
if (node.properties) node.properties["is:raw"] = true;
return node;
};
const file = await unified()
.use(remarkParse)
.use(remarkToc, { heading: "(table[ -]of[ -])?contents?|toc|содержание", maxDepth: 2 })
.use(remarkPrism as any)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeSlug)
.use(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)),
],
})
.use(rehypeStringify, { allowDangerousHtml: true })
.process(content);
return String(file);
const start = performance.now();
const html = md.render(content);
console.log(performance.now() - start);
return html;
}