diff --git a/public/avatar.webp b/public/avatar.webp
index 3b74e68..94241c4 100644
Binary files a/public/avatar.webp and b/public/avatar.webp differ
diff --git a/src/app/about/components/social-media.tsx b/src/app/about/components/social-media.tsx
index 79538eb..d755e4a 100644
--- a/src/app/about/components/social-media.tsx
+++ b/src/app/about/components/social-media.tsx
@@ -38,9 +38,9 @@ export const SocialMedia = ({
icon: FaSquareXTwitter,
},
{
- href: "https://t.me/khaykingleb_blog",
+ href: "https://t.me/blog_khaykingleb",
platform: "Telegram",
- label: "@khaykingleb_blog",
+ label: "@blog_khaykingleb",
icon: FaTelegram,
},
{
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index bf11c3b..987e824 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -57,8 +57,8 @@ export default function AboutPage() {
-
- Full-stack developer focused on MLOps/DevOps engineering,
- passionate about building scalable systems and apps
+ ML platform engineer working on large-scale ML systems,
+ infrastructure, and DevOps
-
Currently working on the Model Shaping team at{" "}
@@ -71,10 +71,11 @@ export default function AboutPage() {
hover:underline hover:opacity-80
`}
>
- Together AI
+ Together.ai
, building infrastructure and tooling for fine-tuning,
- evaluation, and continual improvement of open-source LLMs
+ reinforcement learning, evaluation, quantization, and
+ distillation of open-source LLMs
-
Studied{" "}
@@ -112,11 +113,10 @@ export default function AboutPage() {
`}
>
National Research University "Higher School of
- Economics"
+ Economics "
{" "}
- — this dual background equips me with a comprehensive
- understanding of both the business and technical aspects of
- projects
+ which equips me with a comprehensive understanding of both
+ the business and technical aspects of projects
-
Passed{" "}
diff --git a/src/app/blog/[slug]/components/notion-page.tsx b/src/app/blog/[slug]/components/notion-page.tsx
index f457a4e..94d822d 100644
--- a/src/app/blog/[slug]/components/notion-page.tsx
+++ b/src/app/blog/[slug]/components/notion-page.tsx
@@ -1,16 +1,31 @@
import { unstable_cache } from "next/cache";
import { NotionAPI } from "notion-client";
import type { ExtendedRecordMap } from "notion-types";
+import { getPageTableOfContents } from "notion-utils";
-import NotionRendererClient from "@/app/blog/[slug]/components/notion-renderer"; // ← direct import
+import NotionRendererClient from "@/app/blog/[slug]/components/notion-renderer";
+
+/**
+ * Converts text to a URL-friendly slug.
+ *
+ * @param text - The text to convert.
+ * @returns A URL-friendly slug.
+ */
+const slugify = (text: string) =>
+ text
+ .toLowerCase()
+ .trim()
+ .replace(/[^\w\s-]/g, "")
+ .replace(/[\s_-]+/g, "-")
+ .replace(/^-+|-+$/g, "");
/**
* Fetches a Notion page using the provided page ID.
*
- * @param page_id - The ID of the Notion page to fetch.
- * @returns The record map of the Notion page.
+ * @param pageId - The ID of the Notion page.
+ * @returns The Notion page record map.
*/
-async function fetchNotionPage(page_id: string): Promise {
+async function fetchNotionPage(pageId: string): Promise {
const notion = new NotionAPI({
// Hotfix: https://github.com/NotionX/react-notion-x/issues/659
// Thanks Notion for breaking changes :)
@@ -19,7 +34,6 @@ async function fetchNotionPage(page_id: string): Promise {
beforeRequest: [
(request, options) => {
const url = request.url.toString();
-
if (url.includes("/api/v3/syncRecordValues")) {
return new Request(
url.replace(
@@ -29,21 +43,49 @@ async function fetchNotionPage(page_id: string): Promise {
options,
);
}
-
return request;
},
],
},
},
});
- const recordMap = await notion.getPage(page_id);
- return recordMap;
+ return notion.getPage(pageId);
}
const getNotionPage = unstable_cache(fetchNotionPage, ["notionPage"], {
revalidate: 60,
});
+/**
+ * Builds a mapping from block IDs to readable header slugs.
+ *
+ * @param recordMap - The Notion page record map.
+ * @returns A mapping from block IDs (with and without dashes) to header slugs.
+ */
+function buildHeaderSlugMap(
+ recordMap: ExtendedRecordMap,
+): Record {
+ const pageId = Object.keys(recordMap.block)[0];
+ const pageBlock = recordMap.block[pageId]?.value;
+
+ if (!pageBlock || pageBlock.type !== "page") {
+ return {};
+ }
+
+ const toc = getPageTableOfContents(pageBlock, recordMap);
+ const mapping: Record = {};
+
+ for (const entry of toc) {
+ if (entry.text) {
+ const slug = slugify(entry.text);
+ mapping[entry.id] = slug;
+ mapping[entry.id.replace(/-/g, "")] = slug;
+ }
+ }
+
+ return mapping;
+}
+
/**
* Renders a Notion page using the provided page ID.
*
@@ -52,5 +94,11 @@ const getNotionPage = unstable_cache(fetchNotionPage, ["notionPage"], {
*/
export default async function NotionPage({ page_id }: { page_id: string }) {
const recordMap = await getNotionPage(page_id);
- return ;
+
+ return (
+
+ );
}
diff --git a/src/app/blog/[slug]/components/notion-renderer.tsx b/src/app/blog/[slug]/components/notion-renderer.tsx
index feee77d..6c1edf8 100644
--- a/src/app/blog/[slug]/components/notion-renderer.tsx
+++ b/src/app/blog/[slug]/components/notion-renderer.tsx
@@ -2,6 +2,7 @@
import dynamic from "next/dynamic";
import type { ExtendedRecordMap } from "notion-types";
+import { useEffect } from "react";
import { NotionRenderer } from "react-notion-x";
const Code = dynamic(() =>
@@ -34,28 +35,81 @@ const Equation = dynamic(() =>
const Pdf = dynamic(
() => import("react-notion-x/build/third-party/pdf").then((m) => m.Pdf),
- {
- ssr: false,
- },
+ { ssr: false },
);
/**
- * Renders the Notion content using the NotionRenderer component.
+ * Rewrites Notion's block IDs to human-readable slugs in the DOM.
+ * Also handles initial scroll to hash anchor on page load.
*
- * @param recordMap - The record map containing the Notion data.
+ * @param headerSlugMap - Mapping from block IDs to header slugs.
+ */
+function useHeaderSlugRewrites(headerSlugMap: Record) {
+ useEffect(() => {
+ for (const [blockId, slug] of Object.entries(headerSlugMap)) {
+ const cleanId = blockId.replace(/-/g, "");
+
+ // Update anchor element ID
+ const anchor = document.getElementById(cleanId);
+ if (anchor?.classList.contains("notion-header-anchor")) {
+ anchor.id = slug;
+ }
+
+ // Update hash link href
+ document
+ .querySelector(`a.notion-hash-link[href="#${cleanId}"]`)
+ ?.setAttribute("href", `#${slug}`);
+ }
+
+ // Update table of contents links
+ for (const item of document.querySelectorAll(
+ ".notion-table-of-contents-item",
+ )) {
+ const href = item.getAttribute("href");
+ if (href?.startsWith("#")) {
+ const slug = headerSlugMap[href.substring(1)];
+ if (slug) item.setAttribute("href", `#${slug}`);
+ }
+ }
+
+ // Scroll to anchor after DOM updates (for direct link navigation)
+ const { hash } = window.location;
+ if (hash) {
+ setTimeout(() => {
+ document
+ .getElementById(hash.substring(1))
+ ?.scrollIntoView({ behavior: "instant" });
+ }, 50);
+ }
+ }, [headerSlugMap]);
+}
+
+/**
+ * Renders Notion content with human-readable anchor slugs.
+ *
+ * @param recordMap - The Notion page record map.
+ * @param headerSlugMap - Mapping from block IDs to header slugs.
* @returns The rendered Notion content.
*/
export default function NotionRendererClient({
recordMap,
+ headerSlugMap,
}: {
recordMap: ExtendedRecordMap;
+ headerSlugMap: Record;
}) {
+ useHeaderSlugRewrites(headerSlugMap);
+
return (
{
+ const cleanId = pageId.replace(/-/g, "");
+ return `#${headerSlugMap[pageId] ?? headerSlugMap[cleanId] ?? cleanId}`;
+ }}
/>
);
}
diff --git a/src/app/styles/notion.css b/src/app/styles/notion.css
index eacf721..f4b655c 100644
--- a/src/app/styles/notion.css
+++ b/src/app/styles/notion.css
@@ -10,22 +10,21 @@
@reference "./global.css";
-/* Light mode colors */
-.notion-table-of-contents-item {
- color: #5d5d5f !important;
-}
-
-/* Dark mode colors */
.notion {
@apply !bg-[hsl(var(--b1))] !text-[hsl(var(--bc))];
}
-.notion-table-of-contents-item {
- @apply dark:!text-[#b0b0b0];
+.notion-table-of-contents-item,
+.notion-table-of-contents-item-body,
+.notion-table-of-contents a {
+ color: #5d5d5f !important;
+ opacity: 1 !important;
}
-.notion-table-of-contents a {
- @apply dark:text-[hsl(var(--bc))] dark:opacity-70;
+:is([data-theme="dark"], .dark) .notion-table-of-contents-item,
+:is([data-theme="dark"], .dark) .notion-table-of-contents-item-body,
+:is([data-theme="dark"], .dark) .notion-table-of-contents a {
+ color: #b0b0b0 !important;
}
.notion-asset-caption {