mirror of
https://github.com/kennethreitz/kennethreitz.org.git
synced 2026-06-05 06:46:13 +00:00
8267a8e646
- Add Starlette GZipMiddleware for response compression (~60% smaller HTML) - Extract 290 lines of inline CSS to /static/site.css (browser-cacheable) - Bump HTML Cache-Control from 5min to 1hr - Reduce Granian workers from 4 to 2 (matches shared-cpu-2x) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
697 lines
34 KiB
HTML
697 lines
34 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 & 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" />
|
|
|
|
<!-- Site CSS (layout, nav, dark mode, etc.) -->
|
|
<link rel="stylesheet" href="/static/site.css" />
|
|
|
|
|
|
<!-- 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>
|
|
|
|
{% set github_base = 'https://github.com/kennethreitz/kennethreitz.org' %}
|
|
{% if github_file %}
|
|
<a href="{{ github_base }}/blob/main/{{ github_file }}" class="github-corner" aria-label="View source on GitHub" target="_blank">
|
|
{% else %}
|
|
<a href="{{ github_base }}" class="github-corner" aria-label="View source on GitHub" target="_blank">
|
|
{% endif %}
|
|
<img src="/static/images/github-banner.svg" alt="View on GitHub" class="github-corner-img">
|
|
</a>
|
|
|
|
<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="/search">Search</a>
|
|
<div class="nav-dropdown">
|
|
<span class="nav-dropdown-trigger">Themes</span>
|
|
<div class="nav-dropdown-content" id="themes-dropdown">
|
|
<div style="text-align: center; padding: 1rem; color: #999;">Loading...</div>
|
|
</div>
|
|
</div>
|
|
<div class="nav-dropdown">
|
|
<span class="nav-dropdown-trigger" style="font-style: italic;">etc.</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>© {{ "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');
|
|
}
|
|
// Sync giscus theme if present
|
|
var giscusFrame = document.querySelector('iframe.giscus-frame');
|
|
if (giscusFrame) {
|
|
var theme = body.classList.contains('dark-mode') ? 'dark' : 'light';
|
|
giscusFrame.contentWindow.postMessage(
|
|
{ giscus: { setConfig: { theme: theme } } }, 'https://giscus.app'
|
|
);
|
|
}
|
|
});
|
|
})();
|
|
</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>';
|
|
}
|
|
}
|
|
|
|
|
|
// Load and render themes listing
|
|
async function loadThemes() {
|
|
try {
|
|
const response = await fetch('/api/themes');
|
|
const data = await response.json();
|
|
const dropdown = document.getElementById('themes-dropdown');
|
|
|
|
if (data.themes && data.themes.length > 0) {
|
|
let html = '';
|
|
for (const t of data.themes) {
|
|
html += `<div class="tree-item tree-file">`;
|
|
if (t.icon) {
|
|
html += `<img src="${t.icon}" class="tree-item-icon" alt="">`;
|
|
}
|
|
html += `<a href="${t.path}">${t.name}</a>`;
|
|
html += `</div>`;
|
|
}
|
|
html += `<div class="tree-item tree-folder" style="border-top: 1px solid rgba(128,128,128,0.2); margin-top: 0.25rem; padding-top: 0.25rem;">`;
|
|
html += `<a href="/themes">All Themes →</a>`;
|
|
html += `</div>`;
|
|
dropdown.innerHTML = html;
|
|
} else {
|
|
dropdown.innerHTML = '<div style="padding: 1rem; color: #999;">No themes found</div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading themes:', error);
|
|
document.getElementById('themes-dropdown').innerHTML =
|
|
'<div class="tree-item tree-folder"><a href="/themes">View All Themes</a></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();
|
|
loadThemes();
|
|
|
|
// 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(/(<!--[\s\S]*?-->)/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';
|
|
}
|
|
}
|
|
|
|
// 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(() => {});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
<!-- Client-side oEmbed -->
|
|
<script>
|
|
(function() {
|
|
document.querySelectorAll('.oembed-placeholder').forEach(function(el) {
|
|
var url = el.getAttribute('data-oembed-url');
|
|
if (!url) return;
|
|
fetch('/api/oembed?url=' + encodeURIComponent(url))
|
|
.then(function(r) { return r.ok ? r.json() : Promise.reject(); })
|
|
.then(function(data) {
|
|
if (data.html) {
|
|
el.innerHTML = data.html;
|
|
}
|
|
})
|
|
.catch(function() {
|
|
// Fallback link is already in place — do nothing.
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|