DEV Community

Cover image for Revamping My Portfolio - Markdown Engine, DOM Hydration Fixes, and LLM Discovery
Prantik Ghosh
Prantik Ghosh

Posted on

Revamping My Portfolio - Markdown Engine, DOM Hydration Fixes, and LLM Discovery

Why the Redesign?

My previous portfolio had become outdated and didn't represent the quality or complexity of the systems I build today[cite: 3]. Beyond giving the UI a cleaner visual hierarchy and tighter typography[cite: 4], this rebuild was about making the platform functional: adding a dedicated technical blog, ensuring dark mode works across all dynamic media, and making the site discoverable by both search engines and AI models.

Here are the key engineering updates and solutions implemented during the rebuild:

flowchart TD
    subgraph Discovery ["Discovery Layer"]
        A["Googlebot / Crawlers"] -->|"Parses Graph"| B["JSON-LD Schema @id: #person"]
        C["AI Engines (Perplexity / ChatGPT)"] -->|"Fetches Summary"| D["/llms.txt Spec"]
    end

    subgraph ContentEngine ["Blog & Rendering Pipeline"]
        E["Vite glob (.md files)"] --> F["Custom blogLoader.ts (YAML Frontmatter)"]
        F --> G["Streamdown Engine"]
        G --> H["ZoomPanImage (createPortal Lightbox)"]
        G --> I["Mermaid Diagrams (Dark Mode Reactive)"]
    end

    B --> ContentEngine
    D --> ContentEngine

1. Custom Markdown Blog Engine with Resilient Error Handling

Instead of relying on an external CMS, the blog runs directly off local Markdown files parsed at build/runtime using Vite's import.meta.glob:

// blogLoader.ts
const rawFiles = import.meta.glob('/src/content/blogs/*.md', {
    query: '?raw',
    import: 'default',
    eager: true,
}) as Record<string, string>;
Enter fullscreen mode Exit fullscreen mode

Preventing Empty File Crashes

During development, creating a blank .md draft or omitting frontmatter dividers caused js-yaml to crash with YAMLException: expected a document, but the input is empty. We hardened the loader with guarded string checks and fallback meta defaults so draft files fail silently instead of taking down the entire route:

const parseYamlMeta = (block: string): Record<string, any> => {
    const trimmed = block.trim();
    if (!trimmed) return {};

    try {
        const parsed = load(trimmed);
        return parsed && typeof parsed === 'object' ? parsed : {};
    }
    catch {
        return {};
    }
};
Enter fullscreen mode Exit fullscreen mode

2. Solving React DOM Nesting & Hydration Bugs in Markdown

When adding interactive image zoom (ZoomPanImage) to Markdown posts, React began throwing hydration and nesting errors:

  • In HTML, <figure> cannot be a descendant of <p>.
  • In HTML, <div> cannot be a descendant of <p>.

Because CommonMark parsers automatically wrap standalone images in <p> tags, rendering block-level wrappers (<figure>, <div>) causes browser HTML parsers to split the DOM, breaking React's hydration tree.

The Fix

  1. Swap outer wrappers to inline spans: Styled with block or inline-block, elements satisfy HTML phrasing content rules while keeping exact layout styling intact.

  2. Mount the lightbox via createPortal: Teleporting the fullscreen zoom/pan modal directly to document.body keeps it completely outside the article's <p> hierarchy and eliminates z-index fights with sticky headers:

// Inside ZoomPanImage.tsx
return (
    <>
        <span className='my-6 block w-full text-center'>
            <span className='inline-block cursor-zoom-in' onClick={() => setIsOpen(true)}>
                <img src={src} alt={alt} className='rounded-xl mx-auto' />
                {alt && <span className='mt-2 block text-xs font-mono text-slate-400'>Fig. {alt}</span>}
            </span>
        </span>

        {isOpen && createPortal(
            <div className='fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 backdrop-blur-md'>
                {/* Fullscreen Zoom & Pan Canvas */}
            </div>,
            document.body
        )}
    </>
);
Enter fullscreen mode Exit fullscreen mode

3. Dark-Mode Compatible Mermaid Diagrams

Standard Mermaid diagrams output SVG elements calculated with dark fills and strokes (#333333) by default, making text unreadable against dark backgrounds.

Because Mermaid compiles SVGs via its own JavaScript engine rather than CSS utility classes, we synchronized diagram rendering with our theme store by dynamically instantiating the plugin and keying the container on theme change:

const mermaidPlugin = useMemo(() => {
    return createMermaidPlugin({
        config: {
            theme: isDarkMode ? 'dark' : 'default',
        },
    });
}, [isDarkMode]);

<Streamdown 'dark' 'light'} : ? cjk, code, key="{isDarkMode" math, mermaid: mermaidPlugin plugins="{{" }}>
    {post.rawContent}
</Streamdown>
Enter fullscreen mode Exit fullscreen mode

Passing key={isDarkMode ? 'dark' : 'light'} forces the container to invalidate the cached light SVG and freshly render crisp, readable dark-mode diagrams on toggle.

4. Modern Discovery: Linked JSON-LD & llms.txt

For discoverability, the site uses a linked two-tier architecture:

  1. Unified Schema Graph (#person): Instead of repeating fragmented author fields across subpages, sub-routes (/experience, /projects, /blogs) link their mainEntity or author back to a permanent URI anchor (https://prantikghosh.vercel.app/#person). This prevents duplicate entity warnings in Google Search Console.

  2. /llms.txt: Added an /llms.txt manifest at the site root to give modern LLMs (Perplexity, ChatGPT, Gemini) clean, factual context about my skill set, background, and production work without relying on unpredictable web scraping.

Wrapping Up

A portfolio redesign is a great opportunity to audit practical edge cases: handling invalid DOM nesting in Markdown content, building robust YAML loaders, and keeping SVG diagram renderers reactive to dark mode. The result is a faster, cleaner, and more resilient platform.

Key Takeaways:

  • Watch Markdown DOM Nesting: Use inline elements (span) and createPortal to prevent invalid block-element hydration mismatches inside <p> tags.

  • Guard Dynamic Loaders: Always wrap frontmatter and YAML parsers in null-checks and try/catch blocks to prevent blank draft files from breaking routes.

  • Sync Canvas & SVG Themes: Rerender SVG generators like Mermaid with explicit keys so they track your app's theme state properly.

  • Structure for AI Discovery: Use llms.txt alongside linked JSON-LD schemas so both search crawlers and AI answer engines index your information cleanly.

Top comments (1)

Collapse
 
kyisaiah47 profile image
kyisaiah47 •

What result are you using to evaluate the /llms.txt change: model answers before and after deployment, or crawler fetches?