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>;
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 {};
}
};
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
Swap outer wrappers to inline spans: Styled with
blockorinline-block, elements satisfy HTML phrasing content rules while keeping exact layout styling intact.Mount the lightbox via
createPortal: Teleporting the fullscreen zoom/pan modal directly todocument.bodykeeps 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
)}
</>
);
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>
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:
Unified Schema Graph (
#person): Instead of repeating fragmented author fields across subpages, sub-routes (/experience,/projects,/blogs) link theirmainEntityorauthorback to a permanent URI anchor (https://prantikghosh.vercel.app/#person). This prevents duplicate entity warnings in Google Search Console./llms.txt: Added an/llms.txtmanifest 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) andcreatePortalto prevent invalid block-element hydration mismatches inside<p>tags.Guard Dynamic Loaders: Always wrap frontmatter and YAML parsers in null-checks and
try/catchblocks 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.txtalongside linked JSON-LD schemas so both search crawlers and AI answer engines index your information cleanly.
Top comments (1)
What result are you using to evaluate the
/llms.txtchange: model answers before and after deployment, or crawler fetches?