diff --git a/src/components/PostNavigation.astro b/src/components/PostNavigation.astro new file mode 100644 index 0000000..45522d7 --- /dev/null +++ b/src/components/PostNavigation.astro @@ -0,0 +1,139 @@ +--- +import { Image } from 'astro:assets'; +import type { ImageMetadata } from 'astro'; + +interface NavPost { + title: string; + href: string; + heroImage?: ImageMetadata; +} + +interface Props { + prevPost?: NavPost; + nextPost?: NavPost; +} + +const { prevPost, nextPost } = Astro.props; +--- + +{(prevPost || nextPost) && ( + +)} + diff --git a/src/components/ReadingProgress.astro b/src/components/ReadingProgress.astro new file mode 100644 index 0000000..63802a6 --- /dev/null +++ b/src/components/ReadingProgress.astro @@ -0,0 +1,50 @@ +--- +// Reading progress bar that tracks scroll position +--- + +
+
+
+ + + diff --git a/src/components/RelatedPosts.astro b/src/components/RelatedPosts.astro new file mode 100644 index 0000000..85c18b5 --- /dev/null +++ b/src/components/RelatedPosts.astro @@ -0,0 +1,48 @@ +--- +import BlogCard from './BlogCard.astro'; +import type { ImageMetadata } from 'astro'; + +interface RelatedPost { + title: string; + description: string; + pubDate: Date; + heroImage?: ImageMetadata; + category?: string; + tags?: string[]; + href: string; +} + +interface Props { + posts: RelatedPost[]; + class?: string; +} + +const { posts, class: className = '' } = Astro.props; +--- + +{posts.length > 0 && ( +
+
+ + /// RELATED ARTICLES + + +
+ +
+ {posts.slice(0, 3).map((post) => ( + + ))} +
+
+)} + diff --git a/src/components/TableOfContents.astro b/src/components/TableOfContents.astro new file mode 100644 index 0000000..c01f633 --- /dev/null +++ b/src/components/TableOfContents.astro @@ -0,0 +1,121 @@ +--- +interface Props { + headings: Array<{ + depth: number; + slug: string; + text: string; + }>; + class?: string; +} + +const { headings, class: className = '' } = Astro.props; + +// Filter to only H2 and H3 headings +const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3); +--- + +{tocHeadings.length > 0 && ( + +)} + + + + diff --git a/src/layouts/BlogPost.astro b/src/layouts/BlogPost.astro index 3254670..a852dbd 100644 --- a/src/layouts/BlogPost.astro +++ b/src/layouts/BlogPost.astro @@ -1,51 +1,230 @@ --- import type { CollectionEntry } from 'astro:content'; +import type { ImageMetadata } from 'astro'; import BaseLayout from './BaseLayout.astro'; import FormattedDate from '../components/FormattedDate.astro'; +import ReadingProgress from '../components/ReadingProgress.astro'; +import TableOfContents from '../components/TableOfContents.astro'; +import PostNavigation from '../components/PostNavigation.astro'; +import RelatedPosts from '../components/RelatedPosts.astro'; import { Image } from 'astro:assets'; -type Props = CollectionEntry<'blog'>['data']; +interface NavPost { + title: string; + href: string; + heroImage?: ImageMetadata; +} -const { title, description, pubDate, updatedDate, heroImage } = Astro.props; +interface RelatedPost { + title: string; + description: string; + pubDate: Date; + heroImage?: ImageMetadata; + category?: string; + tags?: string[]; + href: string; +} + +interface Props { + title: string; + description: string; + pubDate: Date; + updatedDate?: Date; + heroImage?: ImageMetadata; + category?: string; + tags?: string[]; + headings?: Array<{ depth: number; slug: string; text: string }>; + prevPost?: NavPost; + nextPost?: NavPost; + relatedPosts?: RelatedPost[]; +} + +const { + title, + description, + pubDate, + updatedDate, + heroImage, + category, + tags, + headings = [], + prevPost, + nextPost, + relatedPosts = [], +} = Astro.props; + +// Estimated read time (rough calculation) +const readTime = '5 min read'; --- - -
- - - - Back to Blog - + + + +
+ +
+
+ +
+ + -
-
- {heroImage && } -
-
-
- - { - updatedDate && ( - - (Updated: ) - - ) - } + +
+
+ +
+ +
+
+
+ SYS.ARTICLE +
+ + + + {readTime} +
+ + {category && ( +
+ + {category} + +
+ )} + +

+ {title} +

+ +

+ {description} +

+ + + {tags && tags.length > 0 && ( +
+ {tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ + + {heroImage && ( +
+
+ + +
+
+
+
+ )} +
+
+ + +
+ +
+ + +
+
+
+

+ /// END TRANSMISSION +

+

+ Published + {updatedDate && ( + ยท Last updated + )} +

+
+ + +
+ Share +
+ + + + + + + + + + + + + +
+
+
+
+ + + + + + + + +
-

{title}

-

{description}

+ + +
-
- -
- - -
diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index 0754ea7..94aef83 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -4,17 +4,99 @@ import BlogPost from '../../layouts/BlogPost.astro'; export async function getStaticPaths() { const posts = await getCollection('blog'); - return posts.map((post) => ({ - params: { slug: post.id }, - props: post, - })); + + // Sort posts by date (newest first) + const sortedPosts = posts.sort( + (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf() + ); + + return sortedPosts.map((post, index) => { + // Get previous and next posts + const prevPost = index < sortedPosts.length - 1 ? sortedPosts[index + 1] : undefined; + const nextPost = index > 0 ? sortedPosts[index - 1] : undefined; + + // Find related posts (same category or shared tags) + const relatedPosts = sortedPosts + .filter((p) => p.id !== post.id) + .filter((p) => { + // Match by category + if (post.data.category && p.data.category === post.data.category) { + return true; + } + // Match by shared tags + if (post.data.tags && p.data.tags) { + const sharedTags = post.data.tags.filter((tag) => p.data.tags?.includes(tag)); + return sharedTags.length > 0; + } + return false; + }) + .slice(0, 3); + + return { + params: { slug: post.id }, + props: { + post, + prevPost: prevPost + ? { + title: prevPost.data.title, + href: `/blog/${prevPost.id}/`, + heroImage: prevPost.data.heroImage, + } + : undefined, + nextPost: nextPost + ? { + title: nextPost.data.title, + href: `/blog/${nextPost.id}/`, + heroImage: nextPost.data.heroImage, + } + : undefined, + relatedPosts: relatedPosts.map((p) => ({ + title: p.data.title, + description: p.data.description, + pubDate: p.data.pubDate, + heroImage: p.data.heroImage, + category: p.data.category, + tags: p.data.tags, + href: `/blog/${p.id}/`, + })), + }, + }; + }); } -type Props = CollectionEntry<'blog'>; -const post = Astro.props; -const { Content } = await render(post); +interface Props { + post: CollectionEntry<'blog'>; + prevPost?: { + title: string; + href: string; + heroImage?: any; + }; + nextPost?: { + title: string; + href: string; + heroImage?: any; + }; + relatedPosts: Array<{ + title: string; + description: string; + pubDate: Date; + heroImage?: any; + category?: string; + tags?: string[]; + href: string; + }>; +} + +const { post, prevPost, nextPost, relatedPosts } = Astro.props; +const { Content, headings } = await render(post); --- - + diff --git a/src/styles/global.css b/src/styles/global.css index 7e118e1..486e192 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -318,6 +318,7 @@ a { .prose-custom { color: #94A3B8; line-height: 1.8; + font-size: 1.0625rem; } .prose-custom h2 { @@ -326,10 +327,20 @@ a { font-weight: 700; text-transform: uppercase; letter-spacing: -0.025em; - margin-top: 3rem; + margin-top: 3.5rem; margin-bottom: 1.25rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1); + position: relative; + scroll-margin-top: 6rem; +} + +.prose-custom h2::before { + content: "//"; + color: var(--color-brand-accent); + margin-right: 0.5rem; + font-family: var(--font-mono); + font-size: 0.9em; } .prose-custom h3 { @@ -338,8 +349,9 @@ a { font-weight: 600; text-transform: uppercase; letter-spacing: -0.015em; - margin-top: 2rem; + margin-top: 2.5rem; margin-bottom: 1rem; + scroll-margin-top: 6rem; } .prose-custom h4 { @@ -348,6 +360,7 @@ a { font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.75rem; + scroll-margin-top: 6rem; } .prose-custom p { @@ -358,11 +371,12 @@ a { color: var(--color-brand-accent); text-decoration: none; transition: color 0.3s ease; + border-bottom: 1px solid transparent; } .prose-custom a:hover { color: #ffffff; - text-decoration: underline; + border-bottom-color: var(--color-brand-accent); } .prose-custom strong { @@ -383,7 +397,7 @@ a { .prose-custom ul li { position: relative; - padding-left: 1.5rem; + padding-left: 1.75rem; margin-bottom: 0.75rem; } @@ -392,69 +406,223 @@ a { position: absolute; left: 0; color: var(--color-brand-accent); + font-size: 0.85em; } .prose-custom ol { - list-style: decimal; - padding-left: 1.5rem; + list-style: none; + padding-left: 0; margin-bottom: 1.5rem; + counter-reset: ol-counter; } .prose-custom ol li { margin-bottom: 0.75rem; - padding-left: 0.5rem; + padding-left: 2.5rem; + position: relative; + counter-increment: ol-counter; } -.prose-custom ol li::marker { +.prose-custom ol li::before { + content: counter(ol-counter, decimal-leading-zero); + position: absolute; + left: 0; color: var(--color-brand-accent); - font-weight: 600; + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + width: 1.75rem; } +/* Enhanced Blockquotes - Terminal/Industrial Style */ .prose-custom blockquote { + position: relative; border-left: 3px solid var(--color-brand-accent); - padding-left: 1.5rem; - margin: 2rem 0; + background: linear-gradient(135deg, rgba(255, 77, 0, 0.05), rgba(21, 25, 33, 0.8)); + padding: 1.5rem 1.5rem 1.5rem 2rem; + margin: 2.5rem 0; font-style: italic; color: #CBD5E1; + border-right: 1px solid rgba(255, 255, 255, 0.05); + border-top: 1px solid rgba(255, 255, 255, 0.05); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); } +.prose-custom blockquote::before { + content: "///"; + position: absolute; + top: -0.75rem; + left: 1rem; + background: var(--color-brand-dark); + padding: 0 0.5rem; + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.1em; + color: var(--color-brand-accent); + font-style: normal; +} + +.prose-custom blockquote p { + margin-bottom: 0; +} + +.prose-custom blockquote p:last-child { + margin-bottom: 0; +} + +/* Enhanced Code - Inline */ .prose-custom code { color: var(--color-brand-accent); background-color: rgba(255, 77, 0, 0.1); - padding: 0.2rem 0.4rem; - border-radius: 2px; + padding: 0.2rem 0.5rem; + border-radius: 0; font-family: var(--font-mono); - font-size: 0.9em; + font-size: 0.85em; + border: 1px solid rgba(255, 77, 0, 0.2); } +/* Enhanced Code Blocks - Terminal Style */ .prose-custom pre { + position: relative; background-color: var(--color-brand-panel); border: 1px solid rgba(255, 255, 255, 0.1); - padding: 1.5rem; - margin: 2rem 0; - overflow-x: auto; + padding: 0; + margin: 2.5rem 0; + overflow: hidden; +} + +.prose-custom pre::before { + content: "TERMINAL"; + display: block; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding: 0.75rem 1rem; + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.15em; + color: #64748B; + text-transform: uppercase; } .prose-custom pre code { + display: block; background: none; - padding: 0; + padding: 1.5rem; color: #CBD5E1; + border: none; + overflow-x: auto; } +/* Enhanced Horizontal Rules - Section Dividers */ .prose-custom hr { border: none; - height: 1px; - background: linear-gradient( - to right, - transparent, - rgba(255, 255, 255, 0.2) 20%, - rgba(255, 255, 255, 0.2) 80%, - transparent - ); - margin: 3rem 0; + height: auto; + margin: 4rem 0; + position: relative; + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; } +.prose-custom hr::before { + content: ""; + flex: 1; + height: 1px; + background: linear-gradient(to right, transparent, rgba(255, 77, 0, 0.3)); +} + +.prose-custom hr::after { + content: ""; + flex: 1; + height: 1px; + background: linear-gradient(to left, transparent, rgba(255, 77, 0, 0.3)); +} + +/* Enhanced Images */ .prose-custom img { border: 1px solid rgba(255, 255, 255, 0.1); - margin: 2rem 0; + margin: 2.5rem 0; + transition: border-color 0.3s ease; +} + +.prose-custom img:hover { + border-color: rgba(255, 77, 0, 0.3); +} + +/* Image Captions (for figures) */ +.prose-custom figure { + margin: 2.5rem 0; +} + +.prose-custom figure img { + margin: 0; +} + +.prose-custom figcaption { + font-family: var(--font-mono); + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #64748B; + margin-top: 0.75rem; + padding-left: 0.5rem; + border-left: 2px solid var(--color-brand-accent); +} + +/* Video containers */ +.prose-custom .video-container { + margin: 2.5rem 0; + position: relative; +} + +.prose-custom .video-container video { + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.prose-custom .video-container p { + font-family: var(--font-mono); + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #64748B; + margin-top: 0.75rem; + margin-bottom: 0; +} + +/* Tables */ +.prose-custom table { + width: 100%; + margin: 2.5rem 0; + border-collapse: collapse; + font-size: 0.9375rem; +} + +.prose-custom thead { + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.prose-custom th { + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #64748B; + padding: 1rem; + text-align: left; +} + +.prose-custom td { + padding: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + color: #94A3B8; +} + +.prose-custom tr:hover td { + background: rgba(255, 255, 255, 0.02); }