diff --git a/components/app-shell.tsx b/components/app-shell.tsx
index 6dd8117..9eb8308 100644
--- a/components/app-shell.tsx
+++ b/components/app-shell.tsx
@@ -6,7 +6,7 @@ import { usePathname } from "next/navigation";
import { AnnouncementBar } from "poyraz-ui/organisms";
import { SiteNavbar } from "@/components/site-navbar";
import { NekoFollower } from "@/components/neko-follower";
-import { ANNOUNCEMENT_ITEMS } from "@/data/site-settings";
+import { ANNOUNCEMENT_ITEMS, ENABLE_NEKO_FOLLOWER } from "@/data/site-settings";
type AppShellProps = {
children: React.ReactNode;
@@ -15,7 +15,8 @@ type AppShellProps = {
export function AppShell({ children }: AppShellProps) {
const pathname = usePathname();
const announcement = ANNOUNCEMENT_ITEMS[0];
- const isStandaloneLinksPage = pathname === "/links" || pathname.startsWith("/links/");
+ const isStandaloneLinksPage =
+ pathname === "/links" || pathname.startsWith("/links/");
if (isStandaloneLinksPage) {
return children;
@@ -23,7 +24,7 @@ export function AppShell({ children }: AppShellProps) {
return (
<>
-
+ {ENABLE_NEKO_FOLLOWER ? : null}
{announcement ? (
diff --git a/components/blog-detail-content.tsx b/components/blog-detail-content.tsx
index 14704a9..94697c5 100644
--- a/components/blog-detail-content.tsx
+++ b/components/blog-detail-content.tsx
@@ -1,12 +1,11 @@
"use client";
+import Image from "next/image";
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { useCallback, useEffect, useRef, useState } from "react";
import { Icon } from "@iconify/react";
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
import type { BlogDetail } from "@/data/blog-detail";
import { BlogToc } from "@/components/blog-toc";
@@ -16,6 +15,59 @@ type BlogDetailContentProps = {
post: BlogDetail;
};
+function isHttpUrl(value: string) {
+ return value.startsWith("http://") || value.startsWith("https://");
+}
+
+function MarkdownImage({ src, alt }: { src?: string; alt?: string }) {
+ if (!src) return null;
+
+ const caption = alt || "";
+ const isLocalAsset = src.startsWith("/");
+ const isExternalAsset = isHttpUrl(src);
+
+ if (!isLocalAsset && !isExternalAsset) return null;
+
+ return (
+
+ {isLocalAsset ? (
+
+ ) : (
+
+ )}
+
+ {caption ? (
+
+
+ {caption}
+
+
+ ) : null}
+
+ );
+}
+
function slugify(text: string) {
return text
.toLowerCase()
@@ -31,7 +83,10 @@ function extractText(children: React.ReactNode): string {
if (typeof children === "string") return children;
if (Array.isArray(children)) return children.map(extractText).join("");
if (children && typeof children === "object" && "props" in children) {
- return extractText((children as React.ReactElement<{ children?: React.ReactNode }>).props.children);
+ return extractText(
+ (children as React.ReactElement<{ children?: React.ReactNode }>).props
+ .children,
+ );
}
return "";
}
@@ -47,7 +102,11 @@ function MermaidBlock({ chart }: { chart: string }) {
const renderChart = async () => {
try {
const mermaid = (await import("mermaid")).default;
- mermaid.initialize({ startOnLoad: false, theme: "neutral", securityLevel: "loose" });
+ mermaid.initialize({
+ startOnLoad: false,
+ theme: "neutral",
+ securityLevel: "loose",
+ });
const { svg: rendered } = await mermaid.render(idRef.current, chart);
if (mounted) {
@@ -96,7 +155,8 @@ function MermaidBlock({ chart }: { chart: string }) {
const GISCUS_REPO = process.env.NEXT_PUBLIC_GISCUS_REPO || "";
const GISCUS_REPO_ID = process.env.NEXT_PUBLIC_GISCUS_REPO_ID || "";
-const GISCUS_CATEGORY = process.env.NEXT_PUBLIC_GISCUS_CATEGORY || "Announcements";
+const GISCUS_CATEGORY =
+ process.env.NEXT_PUBLIC_GISCUS_CATEGORY || "Announcements";
const GISCUS_CATEGORY_ID = process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID || "";
export function BlogDetailContent({ post }: BlogDetailContentProps) {
@@ -109,7 +169,9 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
const update = () => {
const bar = progressBarRef.current;
if (!bar) return;
- const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
+ const docHeight =
+ document.documentElement.scrollHeight -
+ document.documentElement.clientHeight;
const pct = docHeight <= 0 ? 100 : (window.scrollY / docHeight) * 100;
bar.style.width = `${Math.min(100, Math.max(0, pct))}%`;
};
@@ -140,7 +202,10 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
{post.category}
-
+
{post.author} · {post.date} · {post.readTime}
@@ -153,7 +218,11 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
const text = extractText(children);
const id = slugify(text);
return (
-
+
{children}
);
@@ -162,7 +231,11 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
const text = extractText(children);
const id = slugify(text);
return (
-
+
{children}
);
@@ -171,7 +244,11 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
const text = extractText(children);
const id = slugify(text);
return (
-
+
{children}
);
@@ -180,22 +257,35 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
const text = extractText(children);
const id = slugify(text);
return (
-
+
{children}
);
},
p: ({ node, children, ...props }) => {
- const hasImg = node?.children?.some((c: any) => c.tagName === "img");
+ const hasImg = node?.children?.some(
+ (c: any) => c.tagName === "img",
+ );
if (hasImg) {
return (
-
),
- hr: () => (
-
- ),
+ hr: () => ,
table: ({ children }) => (
),
- thead: ({ children }) => {children},
- tr: ({ children }) => {children}
,
+ thead: ({ children }) => (
+ {children}
+ ),
+ tr: ({ children }) => (
+ {children}
+ ),
th: ({ children }) => (
{children}
|
),
td: ({ children }) => (
- {children} |
+
+ {children}
+ |
),
img: ({ src, alt }) => (
-
-
- {alt && (
-
-
- {alt}
-
-
- )}
-
+
),
code: ({ className, children }) => {
const match = /language-(\w+)/.exec(className ?? "");
@@ -279,19 +375,14 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
}
return (
-
- {codeText}
-
+
+
+ {language || "code"}
+
+
+ {codeText}
+
+
);
},
}}
@@ -330,7 +421,12 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
className="fixed right-4 bottom-6 z-40 flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-border bg-background shadow-lg transition-transform hover:scale-105 active:scale-95 lg:hidden"
aria-label="İçindekiler"
>
-
+
{/* Mobil İçindekiler Drawer */}
diff --git a/components/blog-toc.tsx b/components/blog-toc.tsx
index 9dd9485..8ae8337 100644
--- a/components/blog-toc.tsx
+++ b/components/blog-toc.tsx
@@ -44,49 +44,66 @@ export function BlogToc({ markdown, onNavigate }: BlogTocProps) {
const [activeId, setActiveId] = useState("");
const rafRef = useRef(0);
- const handleClick = useCallback((id: string) => {
- const target = document.getElementById(id);
- if (!target) return;
+ const handleClick = useCallback(
+ (id: string) => {
+ const target = document.getElementById(id);
+ if (!target) return;
- target.scrollIntoView({ behavior: "smooth", block: "start" });
- setActiveId(id);
- onNavigate?.();
- }, [onNavigate]);
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
+ setActiveId(id);
+ onNavigate?.();
+ },
+ [onNavigate],
+ );
useEffect(() => {
if (headings.length === 0) return;
- const OFFSET = 120;
+ const headingElements = headings
+ .map((heading) => ({
+ id: heading.id,
+ element: document.getElementById(heading.id),
+ }))
+ .filter((item): item is { id: string; element: HTMLElement } =>
+ Boolean(item.element),
+ );
+
+ if (headingElements.length === 0) return;
const updateActive = () => {
- let current = "";
+ let current = headingElements[0].id;
- for (const heading of headings) {
- const el = document.getElementById(heading.id);
- if (!el) continue;
-
- const top = el.getBoundingClientRect().top;
- if (top <= OFFSET) {
- current = heading.id;
+ for (const item of headingElements) {
+ if (item.element.getBoundingClientRect().top <= 120) {
+ current = item.id;
+ continue;
}
+ break;
}
- if (current) {
- setActiveId(current);
- }
+ setActiveId(current);
};
- const onScroll = () => {
- cancelAnimationFrame(rafRef.current);
- rafRef.current = requestAnimationFrame(updateActive);
- };
+ const observer = new IntersectionObserver(
+ () => {
+ cancelAnimationFrame(rafRef.current);
+ rafRef.current = requestAnimationFrame(updateActive);
+ },
+ {
+ root: null,
+ rootMargin: "-120px 0px -65% 0px",
+ threshold: [0, 1],
+ },
+ );
- window.addEventListener("scroll", onScroll, { passive: true });
+ for (const item of headingElements) {
+ observer.observe(item.element);
+ }
- const timer = setTimeout(updateActive, 300);
+ const timer = setTimeout(updateActive, 200);
return () => {
- window.removeEventListener("scroll", onScroll);
+ observer.disconnect();
cancelAnimationFrame(rafRef.current);
clearTimeout(timer);
};
@@ -96,7 +113,10 @@ export function BlogToc({ markdown, onNavigate }: BlogTocProps) {
return (