Files
kennethreitz 75694af609 Bold shell prompt $ in code blocks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 14:57:20 -04:00

1271 lines
54 KiB
HTML

<!doctype html>
<html lang="en" class="loaded">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>{% block title %}{{ title }} - Kenneth Reitz{% endblock %}</title>
<!-- SEO and Meta -->
<meta name="description" content="{% block description %}Kenneth Reitz - Creator of Requests and Certifi, trusted by millions of developers worldwide. Thoughts on technology, philosophy, and building software for humans.{% endblock %}" />
<meta name="author" content="Kenneth Reitz" />
<link rel="canonical" href="{% block canonical_url %}https://kennethreitz.org{{ request.path if request.path != '/' else '' }}{% endblock %}" />
<!-- Mobile & App Meta -->
<meta name="theme-color" content="#fffff8" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0d1117" media="(prefers-color-scheme: dark)" />
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png" />
<link rel="icon" href="data:," />
<!-- OpenGraph -->
<meta property="og:title" content="{% block og_title %}{{ title }} - Kenneth Reitz{% endblock %}" />
<meta property="og:description" content="{% block og_description %}Creator of Requests and Certifi - libraries trusted by millions of developers worldwide{% endblock %}" />
<meta property="og:type" content="{% block og_type %}website{% endblock %}" />
<meta property="og:url" content="{% block og_url %}https://kennethreitz.org{% endblock %}" />
<meta property="og:image" content="{% block og_image %}https://kennethreitz.org/static/images/social-card.jpg{% endblock %}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="{% block og_image_alt %}Kenneth Reitz - Software developer and open source creator{% endblock %}" />
<meta property="og:site_name" content="Kenneth Reitz" />
<meta property="og:locale" content="en_US" />
<!-- Twitter Card -->
<meta name="twitter:card" content="{% block twitter_card %}summary_large_image{% endblock %}" />
<meta name="twitter:creator" content="@kennethreitz42" />
<meta name="twitter:title" content="{% block twitter_title %}{{ title }} - Kenneth Reitz{% endblock %}" />
<meta name="twitter:description" content="{% block twitter_description %}Creator of Requests and Certifi - libraries trusted by millions of developers worldwide{% endblock %}" />
<meta name="twitter:image" content="{% block twitter_image %}https://kennethreitz.org/static/images/social-card.jpg{% endblock %}" />
<!-- RSS Feed -->
<link rel="alternate" type="application/rss+xml" title="Kenneth Reitz - Essays &amp; AI Writings" href="/feed.xml" />
<!-- Preload critical resources -->
<link rel="preload" href="/static/tufte/tufte.css" as="style" />
<link rel="preload" href="/static/custom.css" as="style" />
<!-- Tufte CSS -->
<link rel="stylesheet" href="/static/tufte/tufte.css" />
<!-- Custom Site CSS -->
<link rel="stylesheet" href="/static/custom.css" />
<style>
/* Prevent FOUC by hiding content until stylesheets load */
html {
visibility: hidden;
opacity: 0;
}
html.loaded {
visibility: visible;
opacity: 1;
transition: opacity 0.1s ease-in;
}
/* Only page-specific or dynamic styles that can't be in external CSS */
/* Legend dots for content guide */
.legend-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0.3rem;
vertical-align: middle;
}
/* Reading progress indicator */
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 0%;
height: 2px;
background: #333;
z-index: 1000;
transition: width 0.1s ease-out;
}
/* Header and nav z-index for dropdown stacking */
header {
position: relative;
z-index: 9999;
}
nav {
position: relative;
z-index: 9999;
}
nav > a, nav > .nav-dropdown {
margin-right: 1rem;
}
.nav-dropdown {
position: relative;
display: inline-block;
}
.nav-dropdown-trigger {
cursor: pointer;
color: inherit;
text-decoration: underline;
padding-bottom: 1rem;
display: inline-block;
}
.nav-dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 250px;
max-width: 400px;
max-height: 500px;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
padding: 0.75rem;
}
.nav-dropdown:hover .nav-dropdown-content,
.nav-dropdown-content:hover {
display: block;
}
.tree-item {
padding: 0.35rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tree-item-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
}
.tree-item a {
color: #333;
text-decoration: none;
flex: 1;
}
.tree-item a:hover {
text-decoration: underline;
}
.tree-folder a {
font-weight: 500;
}
body.dark-mode .nav-dropdown-content {
background: #1a1a1a;
border-color: #333;
}
body.dark-mode .tree-item a {
color: #ccc;
}
@media (prefers-color-scheme: dark) {
.nav-dropdown-content {
background: #1a1a1a;
border-color: #333;
}
.tree-item a {
color: #ccc;
}
}
.theme-toggle {
position: fixed;
bottom: 2rem;
right: 2rem;
background: none;
border: none;
cursor: pointer;
font-size: 1.5rem;
padding: 0.5rem;
margin: 0;
opacity: 0.15;
transition: opacity 0.3s ease;
line-height: 1;
z-index: 9999;
}
.theme-toggle:hover {
opacity: 0.8;
}
.sun-icon,
.moon-icon {
display: none;
}
body.light-mode .sun-icon {
display: inline;
}
body.dark-mode .moon-icon {
display: inline;
}
/* Dark mode styles using body class */
body.dark-mode {
background-color: #0d1117;
color: #c9d1d9;
}
/* Light mode styles to override media query */
body.light-mode {
background-color: #fffff8;
color: #111;
}
@media (max-width: 760px) {
.theme-toggle {
font-size: 1.3rem;
bottom: 1rem;
right: 1rem;
padding: 0.4rem;
}
}
main {
position: relative;
z-index: 1;
}
article {
position: relative;
z-index: 1;
}
/* Dropdown navigation styles */
.nav-dropdown {
position: relative;
display: inline-block;
padding: 0.5rem;
margin: -0.5rem;
z-index: 9999;
}
.nav-dropdown .dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
z-index: 999999;
min-width: 280px;
padding: 0.5rem 0;
margin-top: -1px;
}
.nav-dropdown:hover .dropdown-content {
display: block;
}
.nav-dropdown .dropdown-content a {
display: block;
padding: 0.5rem 1rem;
color: #333;
text-decoration: none;
border-bottom: none;
font-size: 0.9rem;
line-height: 1.4;
}
.nav-dropdown .dropdown-content a:hover {
background-color: #f5f5f5;
}
.nav-dropdown .dropdown-content a .index-description {
display: block;
font-size: 0.75rem;
color: #666;
margin-top: 0.1rem;
}
.nav-dropdown > a::after {
content: "\00a0▼";
font-size: 0.7rem;
color: #999;
}
</style>
<!-- Structured Data (JSON-LD) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "{% block schema_type %}WebSite{% endblock %}",
"name": "{% block schema_name %}Kenneth Reitz{% endblock %}",
"url": "https://kennethreitz.org{% block schema_url %}/{% endblock %}",
"description": "{% block schema_description %}Kenneth Reitz - Creator of Requests and Certifi, trusted by millions of developers worldwide. Thoughts on technology, philosophy, and building software for humans.{% endblock %}",
"author": {
"@type": "Person",
"name": "Kenneth Reitz",
"url": "https://kennethreitz.org",
"sameAs": [
"https://github.com/kennethreitz",
"https://twitter.com/kennethreitz42"
],
"jobTitle": "Python Developer",
"worksFor": {
"@type": "Organization",
"name": "Independent"
},
"knowsAbout": [
"Python Programming",
"API Design",
"Software Architecture",
"Open Source Development",
"Artificial Intelligence",
"Mental Health Advocacy"
]
},
"publisher": {
"@type": "Person",
"name": "Kenneth Reitz",
"url": "https://kennethreitz.org"
}{% block schema_extra %}{% endblock %}
}
</script>
{% block extra_head %}{% endblock %}
<!-- Breadcrumb Structured Data -->
{% if current_path and current_path != '/' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://kennethreitz.org/"
}{% if breadcrumbs %}{% for crumb in breadcrumbs %},
{
"@type": "ListItem",
"position": {{ loop.index + 1 }},
"name": "{{ crumb.name }}",
"item": "https://kennethreitz.org{{ crumb.url }}"
}{% endfor %}{% elif current_path %}{% set path_parts = current_path.strip('/').split('/') %}{% for part in path_parts if part %},
{
"@type": "ListItem",
"position": {{ loop.index + 1 }},
"name": "{{ part | replace('-', ' ') | replace('_', ' ') | title }}",
"item": "https://kennethreitz.org/{{ path_parts[:loop.index]|join('/') }}"
}{% endfor %}{% endif %}
]
}
</script>
{% endif %}
<!-- Analytics -->
{% if not (config.get('DISABLE_ANALYTICS') or request.environ.get('DISABLE_ANALYTICS')) %}
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-RB9QHYEG2X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-RB9QHYEG2X');
</script>
<script type="text/javascript">
var _gauges = _gauges || [];
(function() {
var t = document.createElement('script');
t.type = 'text/javascript';
t.async = true;
t.id = 'gauges-tracker';
t.setAttribute('data-site-id', '65529a9abd1a3b3101979d52');
t.setAttribute('data-track-path', 'https://track.gaug.es/track.gif');
t.src = 'https://d2fuc4clr7gvcn.cloudfront.net/track.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(t, s);
})();
</script>
{% endif %}
<!-- Minimal syntax styling: comments only, keeps code literary -->
</head>
<body>
<!-- Reading progress bar -->
<div class="reading-progress" id="reading-progress"></div>
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode">
<span class="sun-icon">☀️</span>
<span class="moon-icon">🌙</span>
</button>
<article>
<header>
<nav>
<a href="/">Home</a>
<a href="/archive">Archive</a>
<a href="/themes">Themes</a>
<div class="nav-dropdown">
<span class="nav-dropdown-trigger">Browse</span>
<div class="nav-dropdown-content" id="browse-dropdown">
<div style="text-align: center; padding: 1rem; color: #999;">Loading...</div>
</div>
</div>
</nav>
{% if current_path and current_path != '/' %}
{% set path_parts = current_path.strip('/').split('/') %}
{% if path_parts|length > 1 %}
<div class="breadcrumbs">
<a href="/">~</a>
{% for part in path_parts[:-1] %}
<span class="breadcrumb-separator">/</span>
{% set partial_path = '/' + path_parts[:loop.index]|join('/') %}
<a href="{{ partial_path }}">{{ part|replace('-', ' ')|replace('_', ' ')|title }}</a>
{% endfor %}
</div>
{% endif %}
{% endif %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<div class="footer-content">
<div class="footer-note">
<p>&copy; {{ "now"|strftime("%Y") }} Kenneth Reitz. <a href="https://kjvstudy.org/topics/Love">Made with love</a>.</p>
</div>
</div>
</footer>
</article>
{% block extra_scripts %}{% endblock %}
<!-- Theme Toggle -->
<script>
(function() {
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;
const body = document.body;
// Check for saved theme preference or default to system preference
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Set initial theme
if (savedTheme) {
body.classList.add(savedTheme);
} else if (prefersDark) {
body.classList.add('dark-mode');
} else {
body.classList.add('light-mode');
}
// Toggle theme
themeToggle.addEventListener('click', function() {
if (body.classList.contains('light-mode')) {
body.classList.remove('light-mode');
body.classList.add('dark-mode');
localStorage.setItem('theme', 'dark-mode');
} else {
body.classList.remove('dark-mode');
body.classList.add('light-mode');
localStorage.setItem('theme', 'light-mode');
}
});
})();
</script>
<!-- Color System -->
<script>
(function() {
const lightColorSchemes = [
'scheme-ocean',
'scheme-forest',
'scheme-sunset',
'scheme-lavender',
'scheme-rose',
'scheme-sage',
'scheme-amber'
];
// Get or generate a scheme based on the current page
let scheme = localStorage.getItem('current-color-scheme');
// Change scheme occasionally (20% chance on page load)
if (!scheme || Math.random() < 0.2) {
scheme = lightColorSchemes[Math.floor(Math.random() * lightColorSchemes.length)];
localStorage.setItem('current-color-scheme', scheme);
}
// Apply the scheme
document.body.className = (document.body.className + ' ' + scheme).trim();
})();
</script>
<script>
// Render items as tree structure
function renderItems(items) {
if (!items || items.length === 0) {
return '<div style="padding: 1rem; color: #999;">No content found</div>';
}
let html = '';
for (const item of items) {
const cssClass = item.is_dir ? 'tree-folder' : 'tree-file';
html += `<div class="tree-item ${cssClass}">`;
// Add icon if present
if (item.icon) {
html += `<img src="${item.icon}" class="tree-item-icon" alt="">`;
}
html += `<a href="${item.path}">${item.name}</a>`;
html += `</div>`;
}
return html;
}
// Load and render directory listing
async function loadDirectoryTree() {
try {
const response = await fetch('/api/directory-tree');
const data = await response.json();
const dropdown = document.getElementById('browse-dropdown');
if (data.items && data.items.length > 0) {
dropdown.innerHTML = renderItems(data.items);
} else {
dropdown.innerHTML = '<div style="padding: 1rem; color: #999;">No content found</div>';
}
} catch (error) {
console.error('Error loading directory tree:', error);
document.getElementById('browse-dropdown').innerHTML =
'<div style="padding: 1rem; color: #999;">Error loading content</div>';
}
}
// Main DOMContentLoaded handler
document.addEventListener('DOMContentLoaded', function() {
// First priority: Add loaded class to reveal content
document.documentElement.classList.add('loaded');
// Load directory tree for Browse dropdown
loadDirectoryTree();
// Find all pre elements (code blocks)
const codeBlocks = document.querySelectorAll('pre');
// Function to add subtle comment highlighting
function highlightComments(pre) {
const code = pre.querySelector('code') || pre;
let html = code.innerHTML;
// Python/Shell comments (# comment) - both line start and inline
html = html.replace(/(^|\s+)(#.*)$/gm, '$1<span style="color: #888; font-style: italic;">$2</span>');
// Python docstrings (""" or ''') — italic but readable, not grayed out
html = html.replace(/("""[\s\S]*?""")/g, '<span style="font-style: italic;">$1</span>');
html = html.replace(/('''[\s\S]*?''')/g, '<span style="font-style: italic;">$1</span>');
// Python class names (class ClassName)
html = html.replace(/(\bclass\s+)([A-Za-z_][A-Za-z0-9_]*)/g, '$1<span style="font-weight: bold;">$2</span>');
// Variable assignment targets (name = ...)
html = html.replace(/^(\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s)/gm, '$1<span style="font-weight: bold;">$2</span>$3');
// Shell prompt ($ command) - bold the $
html = html.replace(/^(\$ )/gm, '<span style="font-weight: bold;">$1</span>');
// JavaScript/C++/Java comments (// comment) - only at line start, not in URLs
html = html.replace(/^(\s*\/\/.*)$/gm, '<span style="color: #888; font-style: italic;">$1</span>');
// CSS/C/Java block comments (/* comment */)
html = html.replace(/(\/\*[\s\S]*?\*\/)/g, '<span style="color: #888; font-style: italic;">$1</span>');
// HTML comments (<!-- comment -->)
html = html.replace(/(&lt;!--[\s\S]*?--&gt;)/g, '<span style="color: #888; font-style: italic;">$1</span>');
code.innerHTML = html;
}
codeBlocks.forEach(function(pre) {
// Add subtle comment highlighting first
highlightComments(pre);
// Skip if already wrapped
if (pre.parentElement.classList.contains('code-block-wrapper')) {
return;
}
// Create wrapper div
const wrapper = document.createElement('div');
wrapper.className = 'code-block-wrapper';
// Create copy button
const copyButton = document.createElement('button');
copyButton.className = 'copy-button';
copyButton.textContent = 'Copy';
copyButton.setAttribute('aria-label', 'Copy code to clipboard');
// Add click handler
copyButton.addEventListener('click', async function() {
try {
// Get the text content of the pre element
const codeText = pre.textContent || pre.innerText;
// Use modern clipboard API if available
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(codeText);
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = codeText;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
textArea.remove();
}
// Show feedback
const originalText = copyButton.textContent;
copyButton.textContent = 'Copied!';
copyButton.classList.add('copied');
setTimeout(function() {
copyButton.textContent = originalText;
copyButton.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Failed to copy code: ', err);
// Show error feedback
const originalText = copyButton.textContent;
copyButton.textContent = 'Failed';
setTimeout(function() {
copyButton.textContent = originalText;
}, 2000);
}
});
// Wrap the pre element and add the button
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
wrapper.appendChild(copyButton);
});
// Initialize reading progress and article icons
const currentPath = window.location.pathname;
const isIndexPage = currentPath === '/' ||
currentPath === '/archive' ||
currentPath.endsWith('/index') ||
currentPath.match(/^\/(archive|search|sidenotes|outlines|connections|quotes|terms|graph|random)\/?$/);
if (!isIndexPage) {
// Load article icons
loadArticleIcons();
}
// Check if this is a content page (essay/article) rather than an index
const isContentPage = document.querySelector('main').textContent.length > 2000;
if (isContentPage) {
window.addEventListener('scroll', updateReadingProgress);
window.addEventListener('resize', updateReadingProgress);
updateReadingProgress(); // Initial call
}
// Lazy loading for images
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
imageObserver.unobserve(img);
}
}
});
}, {
rootMargin: '50px 0px'
});
// Apply lazy loading to all images with data-src
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
}
});
// Reading progress indicator
function updateReadingProgress() {
const progressBar = document.getElementById('reading-progress');
if (!progressBar) return;
const mainContent = document.querySelector('main');
if (!mainContent) return;
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight;
const winHeight = window.innerHeight;
const scrollPercent = scrollTop / (docHeight - winHeight);
const scrollPercentRounded = Math.round(scrollPercent * 100);
progressBar.style.width = scrollPercentRounded + '%';
// Only show progress bar if there's meaningful content to scroll
if (docHeight > winHeight * 1.5) {
progressBar.style.opacity = '1';
} else {
progressBar.style.opacity = '0';
}
}
// Create a simple fallback icon based on text
function createFallbackIcon(text) {
// Simple hash function for consistent colors
let hash = 0;
for (let i = 0; i < text.length; i++) {
hash = ((hash << 5) - hash + text.charCodeAt(i)) & 0xffffffff;
}
// Generate colors based on hash
const hue = Math.abs(hash) % 360;
const saturation = 60 + (Math.abs(hash >> 8) % 30); // 60-90%
const lightness = 45 + (Math.abs(hash >> 16) % 20); // 45-65%
// Get first letter of text for simple icon
const letter = text.charAt(0).toUpperCase();
return `<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="8" fill="hsl(${hue}, ${saturation}%, ${lightness}%)" opacity="0.8"/>
<text x="10" y="14" text-anchor="middle" fill="white" font-family="serif" font-size="11" font-weight="bold">${letter}</text>
</svg>`;
}
// Load icons for article links at the beginning of paragraphs
function loadArticleIcons() {
// Find all paragraphs that start with a link
const paragraphs = document.querySelectorAll('p');
paragraphs.forEach(paragraph => {
// Skip paragraphs that are sidenotes or contain sidenote elements
if (paragraph.classList.contains('sidenote-entry') ||
paragraph.querySelector('.sidenote') ||
paragraph.querySelector('.marginnote') ||
paragraph.querySelector('.margin-toggle')) {
return;
}
// Skip paragraphs inside directory listings or item lists
if (paragraph.closest('.items-list') || paragraph.closest('.directory-listing') || paragraph.closest('.item')) {
return;
}
// Check if the paragraph starts with a link (no text before it)
const firstNode = paragraph.firstChild;
let firstElement = null;
// Skip any whitespace/text nodes at the beginning
for (let node = firstNode; node; node = node.nextSibling) {
if (node.nodeType === Node.ELEMENT_NODE) {
firstElement = node;
break;
} else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
// If there's non-whitespace text before any element, don't add icon
return;
}
}
// Check if first element is a link, or if it's a strong/b tag containing a link
let linkElement = null;
if (firstElement && firstElement.tagName === 'A') {
linkElement = firstElement;
} else if (firstElement && (firstElement.tagName === 'STRONG' || firstElement.tagName === 'B')) {
// Check if the strong/b tag contains a link as its first child
const strongFirstChild = firstElement.firstElementChild;
if (strongFirstChild && strongFirstChild.tagName === 'A') {
linkElement = strongFirstChild;
}
}
if (linkElement) {
const href = linkElement.getAttribute('href');
// Check if it's an internal link
if (href && href.startsWith('/') && !href.startsWith('//')) {
// Skip if icon already exists
if (paragraph.querySelector('.article-link-icon')) return;
// Fetch icon with simple error handling
fetch(`/api/icon${href}`)
.then(response => response.ok ? response.json() : null)
.then(data => {
if (data && data.success && data.icon) {
// Validate the data URL format
if (!data.icon.startsWith('data:image/svg+xml;base64,')) {
return;
}
try {
// Decode the SVG to insert as inline SVG instead of data URL
const base64Part = data.icon.split(',')[1];
const svgContent = atob(base64Part);
// Create a container div for the SVG
const iconContainer = document.createElement('div');
iconContainer.className = 'article-link-icon';
iconContainer.style.cssText = `
display: inline-block;
width: 20px;
height: 20px;
margin-right: 0.75rem;
margin-left: -2.25rem;
margin-top: -0.25em;
vertical-align: middle;
flex-shrink: 0;
`;
// Add CSS to hide on mobile and tablet - only add once
if (!window.articleIconStylesAdded) {
const style = document.createElement('style');
style.textContent = `
@media (max-width: 760px) {
.article-link-icon,
.article-link-icon.fallback {
display: none !important;
visibility: hidden !important;
}
}
`;
document.head.appendChild(style);
window.articleIconStylesAdded = true;
}
// Insert the SVG content directly
iconContainer.innerHTML = svgContent;
// Insert icon before the first element (which contains the link)
paragraph.insertBefore(iconContainer, firstElement);
// Add some styling to the paragraph
paragraph.style.position = 'relative';
} catch (e) {
// If decoding fails, skip silently
}
}
})
.catch(() => {
// If API call fails, try to generate a fallback icon based on link text
const linkText = linkElement.textContent.trim();
if (linkText) {
try {
// Create a simple fallback icon using the link text
const fallbackIcon = createFallbackIcon(linkText);
const iconContainer = document.createElement('div');
iconContainer.className = 'article-link-icon fallback';
iconContainer.style.cssText = `
display: inline-block;
width: 20px;
height: 20px;
margin-right: 0.75rem;
margin-left: -2.25rem;
margin-top: -0.25em;
vertical-align: middle;
flex-shrink: 0;
opacity: 0.6;
`;
iconContainer.innerHTML = fallbackIcon;
paragraph.insertBefore(iconContainer, firstElement);
paragraph.style.position = 'relative';
} catch (e) {
// If even fallback fails, just skip
}
}
});
}
}
});
}
</script>
<!-- Link Preview Tooltip -->
<div id="link-preview-tooltip" class="link-preview-tooltip">
<div id="preview-content"></div>
</div>
<style>
.link-preview-tooltip {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 10000;
width: 400px;
max-width: calc(100vw - 4rem);
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 1.25rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
pointer-events: auto;
cursor: pointer;
font-family: et-book, Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;
opacity: 0;
transform: translateY(100%);
transition: opacity 0.08s ease, transform 0.08s ease;
}
.link-preview-tooltip:hover {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
}
.link-preview-tooltip.visible {
opacity: 1;
transform: translateY(0);
}
a.link-preview-active {
background: rgba(243, 156, 18, 0.15);
box-shadow: 0 0 8px rgba(243, 156, 18, 0.4);
border-radius: 3px;
padding: 0.1rem 0.2rem;
margin: -0.1rem -0.2rem;
}
.preview-header {
display: flex;
align-items: flex-start;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.preview-icon {
width: 36px;
height: 36px;
flex-shrink: 0;
border-radius: 4px;
}
.preview-title {
font-size: 1.05rem;
font-weight: 600;
color: #222;
line-height: 1.4;
margin-bottom: 0.25rem;
}
.preview-meta {
font-size: 0.8rem;
color: #888;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.preview-meta-item {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.preview-excerpt {
font-size: 0.875rem;
color: #555;
line-height: 1.6;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
body.dark-mode .link-preview-tooltip {
background: rgba(26, 26, 26, 0.98);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
body.dark-mode .preview-title {
color: #e5e5e5;
}
body.dark-mode .preview-meta {
color: #999;
}
body.dark-mode .preview-excerpt {
color: #bbb;
border-top-color: rgba(255, 255, 255, 0.1);
}
@media (prefers-color-scheme: dark) {
.link-preview-tooltip {
background: rgba(26, 26, 26, 0.98);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
.preview-title {
color: #e5e5e5;
}
.preview-meta {
color: #999;
}
.preview-excerpt {
color: #bbb;
border-top-color: rgba(255, 255, 255, 0.1);
}
}
@media (max-width: 768px) {
.link-preview-tooltip {
bottom: 1rem;
right: 1rem;
width: calc(100vw - 2rem);
padding: 1rem;
}
}
</style>
<script>
(function() {
const tooltip = document.getElementById('link-preview-tooltip');
const previewContent = document.getElementById('preview-content');
let currentLink = null;
let hideTimeout = null;
const cache = {};
// Find all links in article content (internal and external)
document.addEventListener('DOMContentLoaded', function() {
// Disable tooltips on index/archive pages
const path = window.location.pathname;
if (path.match(/\/(sidenotes|outlines|connections|quotes|terms|archive)/)) {
return; // Don't attach tooltip listeners on these pages
}
const articleLinks = document.querySelectorAll('article a[href]');
articleLinks.forEach(link => {
const href = link.getAttribute('href');
// Skip links in nav, breadcrumbs, etc.
if (link.closest('nav') || link.closest('.breadcrumbs')) return;
// Skip links in special index pages (sidenotes, outlines, etc.)
if (link.closest('.sidenotes-list') || link.closest('.outline-list') || link.closest('.sidenote-entry')) return;
// Skip sidenote links (links with # anchors to same page)
if (link.classList.contains('sidenote-link') || link.classList.contains('outline-link')) return;
// Skip anchor links and mailto/tel links
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return;
// Skip any link that contains a hash (likely anchor link)
if (href.includes('#')) return;
// Skip alternate format links (.md, .pdf)
if (href.endsWith('.md') || href.endsWith('.pdf')) return;
// Support internal links and external http/https links
if (href.startsWith('/') || href.startsWith('http://') || href.startsWith('https://')) {
link.addEventListener('mouseenter', handleLinkHover);
link.addEventListener('mouseleave', handleLinkLeave);
}
});
});
function handleLinkHover(e) {
const link = e.currentTarget;
const href = link.getAttribute('href');
clearTimeout(hideTimeout);
currentLink = link;
// Show tooltip after slight delay
setTimeout(() => {
if (currentLink === link) {
showPreview(link, href);
}
}, 500);
}
function handleLinkLeave() {
currentLink = null;
tooltip.classList.remove('visible');
hideTimeout = setTimeout(() => {
tooltip.style.display = 'none';
}, 200);
}
async function showPreview(link, href) {
// Check cache first
if (cache[href]) {
displayPreview(link, cache[href]);
return;
}
// Handle external links differently (can't fetch due to CORS)
const isExternal = href.startsWith('http://') || href.startsWith('https://');
if (isExternal) {
try {
const url = new URL(href);
const data = {
title: link.textContent || url.hostname,
excerpt: `External link: ${url.hostname}`,
url: href,
isExternal: true
};
cache[href] = data;
displayPreview(link, data);
} catch (e) {
console.error('Invalid URL:', e);
}
return;
}
try {
// Fetch internal page
const response = await fetch(href);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract metadata
const title = doc.querySelector('h1')?.textContent ||
doc.querySelector('title')?.textContent ||
'Untitled';
// Try to find reading time badge
const readingTimeBadge = doc.querySelector('.reading-time-badge');
const readingTime = readingTimeBadge?.textContent || null;
// Try to find post date
const dateElement = doc.querySelector('.post-subtitle');
const dateMatch = dateElement?.textContent.match(/[A-Z][a-z]+ \d{1,2}, \d{4}/);
const date = dateMatch ? dateMatch[0] : null;
// Get first meaningful paragraph
const paragraphs = doc.querySelectorAll('article section p:not(.subtitle):not(.sidenote):not(.marginnote)');
let excerpt = '';
for (const p of paragraphs) {
const text = p.textContent.trim();
if (text.length > 50) {
excerpt = text;
break;
}
}
if (excerpt.length > 200) {
excerpt = excerpt.substring(0, 200).trim() + '...';
}
// Try to get icon
const icon = doc.querySelector('.post-title-icon')?.src ||
doc.querySelector('.archive-post-icon')?.src ||
doc.querySelector('.article-icon')?.src ||
null;
// Calculate word count
const articleText = doc.querySelector('article section')?.textContent || '';
const wordCount = Math.round(articleText.trim().split(/\s+/).filter(w => w.length > 0).length);
const data = {
title,
readingTime,
date,
excerpt,
icon,
wordCount
};
cache[href] = data;
displayPreview(link, data);
} catch (error) {
console.error('Failed to fetch preview:', error);
}
}
function displayPreview(link, data) {
let html = '<div class="preview-header">';
if (data.icon) {
html += `<img src="${data.icon}" class="preview-icon" alt="">`;
}
html += `<div>`;
html += `<div class="preview-title">${escapeHtml(data.title || 'Untitled')}</div>`;
const metaParts = [];
if (data.readingTime) metaParts.push(data.readingTime);
else if (data.wordCount && data.wordCount > 0) {
const estReadingTime = Math.ceil(data.wordCount / 200);
metaParts.push(`~${estReadingTime} min read`);
}
if (data.wordCount && data.wordCount > 0) metaParts.push(`${data.wordCount} words`);
if (data.date) metaParts.push(data.date);
if (metaParts.length > 0) {
html += `<div class="preview-meta">${metaParts.join(' • ')}</div>`;
}
html += `</div></div>`;
if (data.excerpt && data.excerpt.length > 0) {
html += `<div class="preview-excerpt">${escapeHtml(data.excerpt)}</div>`;
}
previewContent.innerHTML = html;
// Show tooltip in fixed position
tooltip.style.display = 'block';
// Trigger animation
setTimeout(() => {
tooltip.classList.add('visible');
}, 10);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})();
</script>
</body>
</html>