mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
Add image support to footer component with size control via HTML attributes
This commit is contained in:
@@ -285,6 +285,29 @@ function getTextContent(children: React.ReactNode): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Anchor link component for headings
|
||||
function HeadingAnchor({ id }: { id: string }) {
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
// Copy URL to clipboard, but allow default scroll behavior
|
||||
const url = `${window.location.origin}${window.location.pathname}#${id}`;
|
||||
navigator.clipboard.writeText(url).catch(() => {
|
||||
// Silently fail if clipboard API is not available
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`#${id}`}
|
||||
className="heading-anchor"
|
||||
onClick={handleClick}
|
||||
aria-label="Copy link to heading"
|
||||
title="Copy link to heading"
|
||||
>
|
||||
#
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BlogPost({ content }: BlogPostProps) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
@@ -397,6 +420,7 @@ export default function BlogPost({ content }: BlogPostProps) {
|
||||
const id = generateSlug(getTextContent(children));
|
||||
return (
|
||||
<h1 id={id} className="blog-h1">
|
||||
<HeadingAnchor id={id} />
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
@@ -405,6 +429,7 @@ export default function BlogPost({ content }: BlogPostProps) {
|
||||
const id = generateSlug(getTextContent(children));
|
||||
return (
|
||||
<h2 id={id} className="blog-h2">
|
||||
<HeadingAnchor id={id} />
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
@@ -413,6 +438,7 @@ export default function BlogPost({ content }: BlogPostProps) {
|
||||
const id = generateSlug(getTextContent(children));
|
||||
return (
|
||||
<h3 id={id} className="blog-h3">
|
||||
<HeadingAnchor id={id} />
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
@@ -421,6 +447,7 @@ export default function BlogPost({ content }: BlogPostProps) {
|
||||
const id = generateSlug(getTextContent(children));
|
||||
return (
|
||||
<h4 id={id} className="blog-h4">
|
||||
<HeadingAnchor id={id} />
|
||||
{children}
|
||||
</h4>
|
||||
);
|
||||
@@ -429,6 +456,7 @@ export default function BlogPost({ content }: BlogPostProps) {
|
||||
const id = generateSlug(getTextContent(children));
|
||||
return (
|
||||
<h5 id={id} className="blog-h5">
|
||||
<HeadingAnchor id={id} />
|
||||
{children}
|
||||
</h5>
|
||||
);
|
||||
@@ -437,6 +465,7 @@ export default function BlogPost({ content }: BlogPostProps) {
|
||||
const id = generateSlug(getTextContent(children));
|
||||
return (
|
||||
<h6 id={id} className="blog-h6">
|
||||
<HeadingAnchor id={id} />
|
||||
{children}
|
||||
</h6>
|
||||
);
|
||||
|
||||
91
src/components/Footer.tsx
Normal file
91
src/components/Footer.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
||||
import siteConfig from "../config/siteConfig";
|
||||
|
||||
// Sanitize schema for footer markdown (allows links, paragraphs, line breaks, images)
|
||||
// style attribute is sanitized by rehypeSanitize to remove dangerous CSS
|
||||
const footerSanitizeSchema = {
|
||||
...defaultSchema,
|
||||
tagNames: [...(defaultSchema.tagNames || []), "br", "img"],
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
img: ["src", "alt", "loading", "width", "height", "style", "class"],
|
||||
},
|
||||
};
|
||||
|
||||
// Footer component
|
||||
// Renders markdown content from frontmatter footer field
|
||||
// Falls back to siteConfig.footer.defaultContent if no frontmatter footer provided
|
||||
// Visibility controlled by siteConfig.footer settings and frontmatter showFooter field
|
||||
interface FooterProps {
|
||||
content?: string; // Markdown content from frontmatter
|
||||
}
|
||||
|
||||
export default function Footer({ content }: FooterProps) {
|
||||
const { footer } = siteConfig;
|
||||
|
||||
// Don't render if footer is globally disabled
|
||||
if (!footer.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use frontmatter content if provided, otherwise fall back to siteConfig default
|
||||
const footerContent = content || footer.defaultContent;
|
||||
|
||||
// Don't render if no content available
|
||||
if (!footerContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="site-footer">
|
||||
<div className="site-footer-content">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, footerSanitizeSchema]]}
|
||||
components={{
|
||||
p({ children }) {
|
||||
return <p className="site-footer-text">{children}</p>;
|
||||
},
|
||||
img({ src, alt, width, height, style, className }) {
|
||||
return (
|
||||
<span className="site-footer-image-wrapper">
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || ""}
|
||||
className={className || "site-footer-image"}
|
||||
loading="lazy"
|
||||
width={width}
|
||||
height={height}
|
||||
style={style}
|
||||
/>
|
||||
{alt && (
|
||||
<span className="site-footer-image-caption">{alt}</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
a({ href, children }) {
|
||||
const isExternal = href?.startsWith("http");
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||
className="site-footer-link"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{footerContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,10 @@
|
||||
import CopyPageDropdown from "./CopyPageDropdown";
|
||||
|
||||
interface RightSidebarProps {
|
||||
title: string;
|
||||
content: string;
|
||||
url: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
date?: string;
|
||||
tags?: string[];
|
||||
readTime?: string;
|
||||
}
|
||||
|
||||
export default function RightSidebar(props: RightSidebarProps) {
|
||||
// Right sidebar component - maintains layout spacing when sidebars are enabled
|
||||
// CopyPageDropdown is now rendered in the main content area instead
|
||||
export default function RightSidebar() {
|
||||
return (
|
||||
<aside className="post-sidebar-right">
|
||||
<div className="right-sidebar-content">
|
||||
<CopyPageDropdown {...props} />
|
||||
{/* Empty - CopyPageDropdown moved to main content area */}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -87,6 +87,17 @@ export interface RightSidebarConfig {
|
||||
minWidth?: number; // Minimum viewport width to show sidebar (default: 1135)
|
||||
}
|
||||
|
||||
// Footer configuration
|
||||
// Footer content can be set in frontmatter (footer field) or use defaultContent here
|
||||
// Footer can be enabled/disabled globally and per-page via frontmatter showFooter field
|
||||
export interface FooterConfig {
|
||||
enabled: boolean; // Global toggle for footer
|
||||
showOnHomepage: boolean; // Show footer on homepage
|
||||
showOnPosts: boolean; // Default: show footer on blog posts
|
||||
showOnPages: boolean; // Default: show footer on static pages
|
||||
defaultContent?: string; // Default markdown content if no frontmatter footer field provided
|
||||
}
|
||||
|
||||
// Site configuration interface
|
||||
export interface SiteConfig {
|
||||
// Basic site info
|
||||
@@ -136,6 +147,9 @@ export interface SiteConfig {
|
||||
|
||||
// Right sidebar configuration
|
||||
rightSidebar: RightSidebarConfig;
|
||||
|
||||
// Footer configuration
|
||||
footer: FooterConfig;
|
||||
}
|
||||
|
||||
// Default site configuration
|
||||
@@ -289,6 +303,20 @@ export const siteConfig: SiteConfig = {
|
||||
enabled: true, // Set to false to disable right sidebar globally
|
||||
minWidth: 1135, // Minimum viewport width in pixels to show sidebar
|
||||
},
|
||||
|
||||
// Footer configuration
|
||||
// Footer content can be set in frontmatter (footer field) or use defaultContent here
|
||||
// Use showFooter: false in frontmatter to hide footer on specific posts/pages
|
||||
footer: {
|
||||
enabled: true, // Global toggle for footer
|
||||
showOnHomepage: true, // Show footer on homepage
|
||||
showOnPosts: true, // Default: show footer on blog posts (override with frontmatter)
|
||||
showOnPages: true, // Default: show footer on static pages (override with frontmatter)
|
||||
// Default footer markdown (used when frontmatter footer field is not provided)
|
||||
defaultContent: `Built with [Convex](https://convex.dev) for real-time sync and deployed on [Netlify](https://netlify.com). Read the [project on GitHub](https://github.com/waynesutton/markdown-site) to fork and deploy your own. View [real-time site stats](/stats).
|
||||
|
||||
Created by [Wayne](https://x.com/waynesutton) with Convex, Cursor, and Claude Opus 4.5. Follow on [Twitter/X](https://x.com/waynesutton), [LinkedIn](https://www.linkedin.com/in/waynesutton/), and [GitHub](https://github.com/waynesutton). This project is licensed under the MIT [License](https://github.com/waynesutton/markdown-site?tab=MIT-1-ov-file).`,
|
||||
},
|
||||
};
|
||||
|
||||
// Export the config as default for easy importing
|
||||
|
||||
@@ -6,6 +6,7 @@ import PostList from "../components/PostList";
|
||||
import FeaturedCards from "../components/FeaturedCards";
|
||||
import LogoMarquee from "../components/LogoMarquee";
|
||||
import GitHubContributions from "../components/GitHubContributions";
|
||||
import Footer from "../components/Footer";
|
||||
import siteConfig from "../config/siteConfig";
|
||||
|
||||
// Local storage key for view mode preference
|
||||
@@ -223,82 +224,9 @@ export default function Home() {
|
||||
{renderLogoGallery("above-footer")}
|
||||
|
||||
{/* Footer section */}
|
||||
<section className="home-footer">
|
||||
<p className="home-footer-text">
|
||||
Built with{" "}
|
||||
<a
|
||||
href={siteConfig.links.convex}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Convex
|
||||
</a>{" "}
|
||||
for real-time sync and deployed on{" "}
|
||||
<a
|
||||
href={siteConfig.links.netlify}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Netlify
|
||||
</a>
|
||||
. Read the{" "}
|
||||
<a
|
||||
href="https://github.com/waynesutton/markdown-site"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
project on GitHub
|
||||
</a>{" "}
|
||||
to fork and deploy your own. View{" "}
|
||||
<Link to="/stats" className="home-text-link">
|
||||
real-time site stats
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p></p>
|
||||
<br></br>
|
||||
<p className="home-footer-text">
|
||||
Created by{" "}
|
||||
<a
|
||||
href="https://x.com/waynesutton"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Wayne
|
||||
</a>{" "}
|
||||
with Convex, Cursor, and Claude Opus 4.5. Follow on{" "}
|
||||
<a
|
||||
href="https://x.com/waynesutton"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Twitter/X
|
||||
</a>
|
||||
,{" "}
|
||||
<a
|
||||
href="https://www.linkedin.com/in/waynesutton/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
LinkedIn
|
||||
</a>
|
||||
, and{" "}
|
||||
<a
|
||||
href="https://github.com/waynesutton"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
. This project is licensed under the MIT{" "}
|
||||
<a
|
||||
href="https://github.com/waynesutton/markdown-site?tab=MIT-1-ov-file"
|
||||
className="home-text-link"
|
||||
>
|
||||
License.
|
||||
</a>{" "}
|
||||
</p>
|
||||
</section>
|
||||
{siteConfig.footer.enabled && siteConfig.footer.showOnHomepage && (
|
||||
<Footer content={siteConfig.footer.defaultContent} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import BlogPost from "../components/BlogPost";
|
||||
import CopyPageDropdown from "../components/CopyPageDropdown";
|
||||
import PageSidebar from "../components/PageSidebar";
|
||||
import RightSidebar from "../components/RightSidebar";
|
||||
import Footer from "../components/Footer";
|
||||
import { extractHeadings } from "../utils/extractHeadings";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { format, parseISO } from "date-fns";
|
||||
@@ -195,12 +196,15 @@ export default function Post() {
|
||||
return (
|
||||
<div className={`post-page ${hasAnySidebar ? "post-page-with-sidebar" : ""}`}>
|
||||
<nav className={`post-nav ${hasAnySidebar ? "post-nav-with-sidebar" : ""}`}>
|
||||
<button onClick={() => navigate("/")} className="back-button">
|
||||
<ArrowLeft size={16} />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
{/* Only show CopyPageDropdown in nav if right sidebar is disabled */}
|
||||
{!hasRightSidebar && (
|
||||
{/* Hide back-button when sidebars are enabled */}
|
||||
{!hasAnySidebar && (
|
||||
<button onClick={() => navigate("/")} className="back-button">
|
||||
<ArrowLeft size={16} />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
)}
|
||||
{/* Only show CopyPageDropdown in nav if no sidebars are enabled */}
|
||||
{!hasAnySidebar && (
|
||||
<CopyPageDropdown
|
||||
title={page.title}
|
||||
content={page.content}
|
||||
@@ -222,7 +226,21 @@ export default function Post() {
|
||||
{/* Main content */}
|
||||
<article className={`post-article ${hasAnySidebar ? "post-article-with-sidebar" : ""}`}>
|
||||
<header className="post-header">
|
||||
<h1 className="post-title">{page.title}</h1>
|
||||
<div className="post-title-row">
|
||||
<h1 className="post-title">{page.title}</h1>
|
||||
{/* Show CopyPageDropdown aligned with title when sidebars are enabled */}
|
||||
{hasAnySidebar && (
|
||||
<div className="post-header-actions">
|
||||
<CopyPageDropdown
|
||||
title={page.title}
|
||||
content={page.content}
|
||||
url={window.location.href}
|
||||
slug={page.slug}
|
||||
description={page.excerpt}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Author avatar and name for pages (optional) */}
|
||||
{(page.authorImage || page.authorName) && (
|
||||
<div className="post-meta-header">
|
||||
@@ -243,18 +261,16 @@ export default function Post() {
|
||||
</header>
|
||||
|
||||
<BlogPost content={page.content} />
|
||||
|
||||
{/* Footer - shown inside article at bottom for pages */}
|
||||
{siteConfig.footer.enabled &&
|
||||
(page.showFooter !== undefined ? page.showFooter : siteConfig.footer.showOnPages) && (
|
||||
<Footer content={page.footer} />
|
||||
)}
|
||||
</article>
|
||||
|
||||
{/* Right sidebar - CopyPageDropdown */}
|
||||
{hasRightSidebar && (
|
||||
<RightSidebar
|
||||
title={page.title}
|
||||
content={page.content}
|
||||
url={window.location.href}
|
||||
slug={page.slug}
|
||||
description={page.excerpt}
|
||||
/>
|
||||
)}
|
||||
{/* Right sidebar - empty when sidebars are enabled, CopyPageDropdown moved to main content */}
|
||||
{hasRightSidebar && <RightSidebar />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -303,12 +319,15 @@ export default function Post() {
|
||||
return (
|
||||
<div className={`post-page ${hasAnySidebar ? "post-page-with-sidebar" : ""}`}>
|
||||
<nav className={`post-nav ${hasAnySidebar ? "post-nav-with-sidebar" : ""}`}>
|
||||
<button onClick={() => navigate("/")} className="back-button">
|
||||
<ArrowLeft size={16} />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
{/* Only show CopyPageDropdown in nav if right sidebar is disabled */}
|
||||
{!hasRightSidebar && (
|
||||
{/* Hide back-button when sidebars are enabled */}
|
||||
{!hasAnySidebar && (
|
||||
<button onClick={() => navigate("/")} className="back-button">
|
||||
<ArrowLeft size={16} />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
)}
|
||||
{/* Only show CopyPageDropdown in nav if no sidebars are enabled */}
|
||||
{!hasAnySidebar && (
|
||||
<CopyPageDropdown
|
||||
title={post.title}
|
||||
content={post.content}
|
||||
@@ -332,7 +351,24 @@ export default function Post() {
|
||||
|
||||
<article className={`post-article ${hasAnySidebar ? "post-article-with-sidebar" : ""}`}>
|
||||
<header className="post-header">
|
||||
<h1 className="post-title">{post.title}</h1>
|
||||
<div className="post-title-row">
|
||||
<h1 className="post-title">{post.title}</h1>
|
||||
{/* Show CopyPageDropdown aligned with title when sidebars are enabled */}
|
||||
{hasAnySidebar && (
|
||||
<div className="post-header-actions">
|
||||
<CopyPageDropdown
|
||||
title={post.title}
|
||||
content={post.content}
|
||||
url={window.location.href}
|
||||
slug={post.slug}
|
||||
description={post.description}
|
||||
date={post.date}
|
||||
tags={post.tags}
|
||||
readTime={post.readTime}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="post-meta-header">
|
||||
{/* Author avatar and name (optional) */}
|
||||
{(post.authorImage || post.authorName) && (
|
||||
@@ -431,21 +467,16 @@ export default function Post() {
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
|
||||
{/* Footer - shown inside article at bottom for posts */}
|
||||
{siteConfig.footer.enabled &&
|
||||
(post.showFooter !== undefined ? post.showFooter : siteConfig.footer.showOnPosts) && (
|
||||
<Footer content={post.footer} />
|
||||
)}
|
||||
</article>
|
||||
|
||||
{/* Right sidebar - CopyPageDropdown */}
|
||||
{hasRightSidebar && (
|
||||
<RightSidebar
|
||||
title={post.title}
|
||||
content={post.content}
|
||||
url={window.location.href}
|
||||
slug={post.slug}
|
||||
description={post.description}
|
||||
date={post.date}
|
||||
tags={post.tags}
|
||||
readTime={post.readTime}
|
||||
/>
|
||||
)}
|
||||
{/* Right sidebar - empty when sidebars are enabled, CopyPageDropdown moved to main content */}
|
||||
{hasRightSidebar && <RightSidebar />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -40,6 +40,8 @@ const POST_FIELDS = [
|
||||
{ name: "authorImage", required: false, example: '"/images/authors/jane.png"' },
|
||||
{ name: "layout", required: false, example: '"sidebar"' },
|
||||
{ name: "rightSidebar", required: false, example: "true" },
|
||||
{ name: "showFooter", required: false, example: "true" },
|
||||
{ name: "footer", required: false, example: '"Built with [Convex](https://convex.dev)."' },
|
||||
];
|
||||
|
||||
// Frontmatter field definitions for pages
|
||||
@@ -57,6 +59,8 @@ const PAGE_FIELDS = [
|
||||
{ name: "authorImage", required: false, example: '"/images/authors/jane.png"' },
|
||||
{ name: "layout", required: false, example: '"sidebar"' },
|
||||
{ name: "rightSidebar", required: false, example: "true" },
|
||||
{ name: "showFooter", required: false, example: "true" },
|
||||
{ name: "footer", required: false, example: '"Built with [Convex](https://convex.dev)."' },
|
||||
];
|
||||
|
||||
// Generate frontmatter template based on content type
|
||||
|
||||
@@ -314,7 +314,7 @@ body {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
/* padding: 40px 24px; */
|
||||
}
|
||||
|
||||
/* Wide content layout for pages that need more space (stats, etc.) */
|
||||
@@ -648,7 +648,7 @@ body {
|
||||
|
||||
/* Post page styles */
|
||||
.post-page {
|
||||
padding-top: 20px;
|
||||
padding-top: 55px;
|
||||
}
|
||||
|
||||
/* Full-width sidebar layout - breaks out of .main-content constraints */
|
||||
@@ -656,7 +656,7 @@ body {
|
||||
padding-top: 20px;
|
||||
/* Break out of the 680px max-width container */
|
||||
width: calc(100vw - 48px);
|
||||
max-width: 1400px;
|
||||
max-width: none;
|
||||
margin-left: calc(-1 * (min(100vw - 48px, 1400px) - 680px) / 2);
|
||||
position: relative;
|
||||
/* Add left padding to align content with sidebar edge
|
||||
@@ -862,6 +862,22 @@ body {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
/* Title row container - flex layout for title and CopyPageDropdown */
|
||||
.post-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Header actions container for CopyPageDropdown when sidebars are enabled */
|
||||
.post-header-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* Sidebar layout for pages - two-column grid with docs-style sidebar */
|
||||
.post-content-with-sidebar {
|
||||
display: grid;
|
||||
@@ -888,7 +904,7 @@ body {
|
||||
.post-sidebar-wrapper {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
align-self: flex-start;
|
||||
align-self: start;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
/* Hide scrollbar while keeping scroll functionality */
|
||||
@@ -902,7 +918,7 @@ body {
|
||||
padding-bottom: 24px;
|
||||
margin-top: -24px;
|
||||
border-radius: 6px;
|
||||
/* Extend background to fill height */
|
||||
/* Extend background to fill height - ensures sidebars flush to bottom */
|
||||
min-height: calc(100vh - 80px);
|
||||
/* Top border using CSS variable for theme consistency */
|
||||
border-top: 1px solid var(--border-sidebar);
|
||||
@@ -923,7 +939,7 @@ body {
|
||||
.post-sidebar-right {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
align-self: flex-start;
|
||||
align-self: start;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
/* Hide scrollbar while keeping scroll functionality */
|
||||
@@ -937,7 +953,7 @@ body {
|
||||
padding-bottom: 24px;
|
||||
margin-top: -24px;
|
||||
border-radius: 6px;
|
||||
/* Extend background to fill height */
|
||||
/* Extend background to fill height - ensures sidebars flush to bottom */
|
||||
min-height: calc(100vh - 80px);
|
||||
/* Top border using CSS variable for theme consistency */
|
||||
border-top: 1px solid var(--border-sidebar);
|
||||
@@ -1177,8 +1193,10 @@ body {
|
||||
font-size: var(--font-size-post-title);
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.2;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.post-meta-header {
|
||||
@@ -1238,6 +1256,7 @@ body {
|
||||
margin: 48px 0 24px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.3;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blog-h2 {
|
||||
@@ -1246,6 +1265,7 @@ body {
|
||||
margin: 40px 0 20px;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.3;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blog-h3 {
|
||||
@@ -1253,6 +1273,7 @@ body {
|
||||
font-weight: 300;
|
||||
margin: 32px 0 16px;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blog-h4 {
|
||||
@@ -1260,6 +1281,7 @@ body {
|
||||
font-weight: 300;
|
||||
margin: 24px 0 12px;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blog-h5 {
|
||||
@@ -1267,6 +1289,7 @@ body {
|
||||
font-weight: 300;
|
||||
margin: 20px 0 10px;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blog-h6 {
|
||||
@@ -1274,6 +1297,33 @@ body {
|
||||
font-weight: 300;
|
||||
margin: 16px 0 8px;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Anchor links for headings */
|
||||
.heading-anchor {
|
||||
position: absolute;
|
||||
left: -1.5em;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
font-size: 0.9em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.blog-h1:hover .heading-anchor,
|
||||
.blog-h2:hover .heading-anchor,
|
||||
.blog-h3:hover .heading-anchor,
|
||||
.blog-h4:hover .heading-anchor,
|
||||
.blog-h5:hover .heading-anchor,
|
||||
.blog-h6:hover .heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.heading-anchor:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.blog-link {
|
||||
@@ -1716,6 +1766,72 @@ body {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Site footer styles (used by Footer component) */
|
||||
.site-footer {
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.site-footer-content {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-footer-text);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.site-footer-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-footer-text);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.site-footer-text:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.site-footer-text a {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-footer-text a:hover {
|
||||
color: var(--link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.site-footer-link {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-footer-link:hover {
|
||||
color: var(--link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Site footer image styles */
|
||||
.site-footer-image-wrapper {
|
||||
/* display: block;*/
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.site-footer-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.site-footer-image-caption {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Blog Page Styles
|
||||
Dedicated /blog page that shows all posts
|
||||
@@ -2009,6 +2125,12 @@ body {
|
||||
font-size: var(--font-size-blog-h6);
|
||||
}
|
||||
|
||||
/* Adjust anchor link position on mobile */
|
||||
.heading-anchor {
|
||||
left: -1.2em;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Table mobile styles */
|
||||
.blog-table {
|
||||
font-size: var(--font-size-table);
|
||||
|
||||
Reference in New Issue
Block a user