399 lines
13 KiB
Plaintext
399 lines
13 KiB
Plaintext
---
|
|
import Layout from './Layout.astro';
|
|
import FormattedDate from '../components/FormattedDate.astro';
|
|
import ShareButtons from '../components/ShareButtons.astro';
|
|
import TagList from '../components/TagList.astro';
|
|
import './styles/markdown.css';
|
|
|
|
import directus from '../../lib/directus';
|
|
import { readItems } from '@directus/sdk';
|
|
|
|
export async function getStaticPaths() {
|
|
const posts = await directus.request(
|
|
readItems('posts', {
|
|
fields: ['*'],
|
|
})
|
|
);
|
|
return posts.map((post) => ({ params: { slug: post.slug }, props: post }));
|
|
}
|
|
|
|
const post = Astro.props;
|
|
const published_date: string = post.published_date.toLocaleString();
|
|
|
|
let canonicalURL;
|
|
try {
|
|
canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL);
|
|
} catch (error) {
|
|
console.error('Error creating canonical URL:', error);
|
|
canonicalURL = new URL('https://www.example.com');
|
|
}
|
|
---
|
|
|
|
<Layout title={post.title} description={post.description}>
|
|
<article class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl">
|
|
<div class="mb-12">
|
|
<h1
|
|
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100"
|
|
>
|
|
{post.title}
|
|
</h1>
|
|
|
|
<div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400">
|
|
<FormattedDate date={published_date} />
|
|
</div>
|
|
|
|
<TagList tags={post.tags} class="mt-2" />
|
|
</div>
|
|
|
|
<!-- Hero image -->
|
|
{
|
|
post.image && (
|
|
<div class="relative mb-8 overflow-hidden rounded-xl shadow-lg sm:mb-12">
|
|
<div class="aspect-[16/9] w-full">
|
|
<img
|
|
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
|
|
alt={post.image_alt}
|
|
class="h-full w-full object-cover"
|
|
loading="eager"
|
|
/>
|
|
</div>
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
<div class="markdown-content">
|
|
<slot />
|
|
</div>
|
|
|
|
<!-- Add the like button after the content -->
|
|
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
|
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
|
<ShareButtons url={canonicalURL.toString()} title={post.title} />
|
|
<!-- Convert URL to string -->
|
|
</div>
|
|
</div>
|
|
|
|
{
|
|
post.updated_date && (
|
|
<div class="mt-8 text-sm text-zinc-500 italic dark:text-zinc-400">
|
|
Last updated on <FormattedDate date={post.updated_date} />
|
|
</div>
|
|
)
|
|
}
|
|
</article>
|
|
|
|
<slot name="after-article" />
|
|
</Layout>
|
|
|
|
<script>
|
|
// Blog post SPA transitions
|
|
function setupBlogPostTransitions() {
|
|
// Animate article entrance
|
|
const article = document.querySelector('article');
|
|
if (article) {
|
|
article.classList.add('article-entering');
|
|
|
|
// Remove class after animation completes
|
|
setTimeout(() => {
|
|
article.classList.remove('article-entering');
|
|
}, 1000);
|
|
}
|
|
|
|
// Ensure consistent code block styling
|
|
function updateCodeBlockStyles() {
|
|
document.querySelectorAll('pre').forEach((pre) => {
|
|
// Force the background color with !important for both light and dark mode
|
|
pre.setAttribute('style', 'background-color: #1e293b !important');
|
|
|
|
// Also apply to any nested code elements
|
|
const codeElements = pre.querySelectorAll('code');
|
|
codeElements.forEach((code) => {
|
|
code.setAttribute(
|
|
'style',
|
|
'background-color: transparent !important; color: #e5e7eb !important;'
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Initial application
|
|
updateCodeBlockStyles();
|
|
|
|
// Watch for theme changes
|
|
const observer = new MutationObserver(() => {
|
|
updateCodeBlockStyles();
|
|
});
|
|
|
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
|
|
// Also run on any content changes that might add new code blocks
|
|
const contentObserver = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
if (mutation.addedNodes.length) {
|
|
updateCodeBlockStyles();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
contentObserver.observe(document.body, { childList: true, subtree: true });
|
|
|
|
// Clean up observers when navigating away
|
|
document.addEventListener('spa-navigation-start', () => {
|
|
observer.disconnect();
|
|
contentObserver.disconnect();
|
|
});
|
|
|
|
// Remove the parallax effect for hero image
|
|
|
|
// Handle prev/next navigation links
|
|
const navLinks = document.querySelectorAll('.blog-nav-link');
|
|
navLinks.forEach((link) => {
|
|
if (!link.hasAttribute('data-spa-handled')) {
|
|
link.setAttribute('data-spa-handled', 'true');
|
|
|
|
link.addEventListener('mouseenter', () => {
|
|
link.classList.add('nav-link-hover');
|
|
});
|
|
|
|
link.addEventListener('mouseleave', () => {
|
|
link.classList.remove('nav-link-hover');
|
|
});
|
|
}
|
|
});
|
|
|
|
// Animate headings when they enter the viewport
|
|
const animateHeadings = () => {
|
|
const headings = document.querySelectorAll('article h2, article h3');
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('heading-visible');
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{
|
|
threshold: 0.2,
|
|
rootMargin: '0px 0px -100px 0px',
|
|
}
|
|
);
|
|
|
|
headings.forEach((heading) => {
|
|
heading.classList.add('heading-animated');
|
|
observer.observe(heading);
|
|
});
|
|
|
|
return observer;
|
|
};
|
|
|
|
// Initialize heading animations
|
|
const headingObserver = animateHeadings();
|
|
|
|
// Enhance code blocks with syntax highlighting and copy button
|
|
function enhanceCodeBlocks() {
|
|
const codeBlocks = document.querySelectorAll('pre code');
|
|
|
|
codeBlocks.forEach((codeBlock) => {
|
|
// Skip if already processed
|
|
if (codeBlock.parentElement.classList.contains('enhanced')) return;
|
|
|
|
// Mark as enhanced
|
|
codeBlock.parentElement.classList.add('enhanced');
|
|
|
|
// Create copy button
|
|
const copyButton = document.createElement('button');
|
|
copyButton.className = 'copy-code-button';
|
|
copyButton.innerHTML = `
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
</svg>
|
|
`;
|
|
|
|
// Add copy functionality
|
|
copyButton.addEventListener('click', () => {
|
|
const code = codeBlock.textContent;
|
|
navigator.clipboard.writeText(code);
|
|
|
|
// Show copied feedback
|
|
copyButton.innerHTML = `
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
</svg>
|
|
`;
|
|
|
|
setTimeout(() => {
|
|
copyButton.innerHTML = `
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
</svg>
|
|
`;
|
|
}, 2000);
|
|
});
|
|
|
|
// Add copy button to pre element
|
|
codeBlock.parentElement.appendChild(copyButton);
|
|
|
|
// Fix line numbers implementation
|
|
const codeText = codeBlock.textContent;
|
|
const lines = codeText.split('\n');
|
|
|
|
const lineNumbers = document.createElement('div');
|
|
lineNumbers.className = 'line-numbers';
|
|
|
|
// Always include all lines, including empty ones
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const lineNumber = document.createElement('span');
|
|
lineNumber.textContent = i + 1;
|
|
lineNumbers.appendChild(lineNumber);
|
|
}
|
|
|
|
codeBlock.parentElement.classList.add('with-line-numbers');
|
|
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
|
|
|
|
// Fix language label detection and display
|
|
const className = codeBlock.className;
|
|
const languageMatch = className.match(/language-(\w+)/);
|
|
|
|
if (languageMatch && languageMatch[1]) {
|
|
const language = languageMatch[1];
|
|
|
|
// Add language label at top right
|
|
const languageLabel = document.createElement('div');
|
|
languageLabel.className = 'language-label';
|
|
languageLabel.textContent = language;
|
|
codeBlock.parentElement.appendChild(languageLabel);
|
|
|
|
// Add language badge at bottom right with markdown syntax
|
|
const languageBadge = document.createElement('div');
|
|
languageBadge.className = 'language-badge';
|
|
languageBadge.textContent = `\`\`\`${language}`;
|
|
languageBadge.style.position = 'absolute';
|
|
languageBadge.style.bottom = '0.5rem';
|
|
languageBadge.style.right = '0.5rem';
|
|
languageBadge.style.fontSize = '0.7rem';
|
|
languageBadge.style.padding = '0.1rem 0.3rem';
|
|
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
|
|
languageBadge.style.color = '#e5e7eb';
|
|
languageBadge.style.borderRadius = '0.25rem';
|
|
languageBadge.style.fontFamily =
|
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
languageBadge.style.zIndex = '10';
|
|
codeBlock.parentElement.appendChild(languageBadge);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Enhance tables with better styling
|
|
function enhanceTables() {
|
|
const tables = document.querySelectorAll('.markdown-content table');
|
|
|
|
tables.forEach((table) => {
|
|
if (table.classList.contains('enhanced-table')) return;
|
|
|
|
table.classList.add('enhanced-table');
|
|
|
|
// Wrap table in responsive container
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'table-container';
|
|
table.parentNode.insertBefore(wrapper, table);
|
|
wrapper.appendChild(table);
|
|
|
|
// Add zebra striping to rows
|
|
const rows = table.querySelectorAll('tbody tr');
|
|
rows.forEach((row, index) => {
|
|
if (index % 2 === 0) {
|
|
row.classList.add('even-row');
|
|
} else {
|
|
row.classList.add('odd-row');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Enhance blockquotes with icons
|
|
function enhanceBlockquotes() {
|
|
const blockquotes = document.querySelectorAll('.markdown-content blockquote');
|
|
|
|
blockquotes.forEach((blockquote) => {
|
|
if (blockquote.classList.contains('enhanced-quote')) return;
|
|
|
|
blockquote.classList.add('enhanced-quote');
|
|
|
|
// Add quote icon
|
|
const icon = document.createElement('div');
|
|
icon.className = 'quote-icon';
|
|
icon.innerHTML = `
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
|
</svg>
|
|
`;
|
|
|
|
blockquote.insertBefore(icon, blockquote.firstChild);
|
|
});
|
|
}
|
|
|
|
// Run all enhancements
|
|
enhanceCodeBlocks();
|
|
enhanceTables();
|
|
enhanceBlockquotes();
|
|
|
|
// Clean up observers when navigating away
|
|
document.addEventListener('spa-navigation-start', () => {
|
|
if (headingObserver) {
|
|
headingObserver.disconnect();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize on first load
|
|
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
|
|
|
|
// Re-initialize when content changes via Astro's view transitions
|
|
document.addEventListener('astro:page-load', setupBlogPostTransitions);
|
|
|
|
// For compatibility with custom transition system
|
|
document.addEventListener('page-transition-complete', setupBlogPostTransitions);
|
|
|
|
// Also initialize when SPA navigation completes
|
|
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
|
|
</script>
|
|
|
|
<style>
|
|
/* Enhanced hero image styling */
|
|
article img:first-of-type {
|
|
border-radius: 1rem;
|
|
box-shadow:
|
|
0 10px 25px -5px rgba(0, 0, 0, 0.1),
|
|
0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
article img:first-of-type:hover {
|
|
transform: scale(1.01);
|
|
}
|
|
|
|
/* Article entrance animation */
|
|
.article-entering {
|
|
animation: article-fade-in 0.8s ease-out forwards;
|
|
}
|
|
|
|
@keyframes article-fade-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* Rest of the styles remain unchanged... */
|
|
</style>
|