diff --git a/.gitignore b/.gitignore index 2b0c3e1..c7a07f0 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ fork-config.json # PRD files prds/metadataforsubs.md +prds/addchattoapp.md diff --git a/TASK.md b/TASK.md index 21815b1..bd3ab74 100644 --- a/TASK.md +++ b/TASK.md @@ -8,10 +8,35 @@ ## Current Status -v1.30.2 ready. Right sidebar now opt-in only via frontmatter. +v1.31.1 ready. Footer component now supports images with size control via HTML attributes. ## Completed +- [x] Image support in footer component with size control + - [x] Footer sanitize schema updated to allow width, height, style, class attributes on images + - [x] Footer image component handler updated to pass through size attributes + - [x] CSS styles added for footer images (.site-footer-image-wrapper, .site-footer-image, .site-footer-image-caption) + - [x] Images support lazy loading and optional captions from alt text + - [x] Security verified: rehypeSanitize sanitizes style attributes to remove dangerous CSS + - [x] Updated files.md, changelog.md with image support documentation + +- [x] Customizable footer component with markdown support + - [x] Footer component created (src/components/Footer.tsx) with ReactMarkdown rendering + - [x] Footer configuration added to siteConfig.ts (FooterConfig interface with defaultContent) + - [x] Footer content can be set in frontmatter footer field (markdown) or siteConfig.defaultContent + - [x] Footer can be enabled/disabled globally and per-page type + - [x] showFooter and footer frontmatter fields added for posts and pages + - [x] Footer renders inside article tag at bottom for posts/pages + - [x] Footer maintains current position on homepage + - [x] Updated Home.tsx to use Footer component with defaultContent + - [x] Updated Post.tsx to render Footer inside article based on showFooter + - [x] Added CSS styles for site-footer (.site-footer, .site-footer-content, .site-footer-text, .site-footer-link) + - [x] Updated schema.ts, posts.ts, pages.ts with showFooter and footer fields + - [x] Updated sync-posts.ts to parse showFooter and footer frontmatter + - [x] Updated Write.tsx to include showFooter and footer in frontmatter reference + - [x] Sidebars flush to bottom when footer is enabled (min-height ensures proper extension) + - [x] Updated files.md, changelog.md with footer feature documentation + - [x] Fixed right sidebar default behavior: now requires explicit `rightSidebar: true` in frontmatter - [x] Pages/posts without rightSidebar frontmatter render normally with CopyPageDropdown in nav - [x] Fixed TypeScript errors: Added rightSidebar to syncPosts and syncPostsPublic args validators diff --git a/changelog.md b/changelog.md index 0fc4dec..e62fdf4 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [1.31.1] - 2025-12-25 + +### Added + +- Image support in footer component with size control + - Footer markdown now supports images using standard markdown syntax or HTML + - Images can be sized using `width`, `height`, `style`, or `class` HTML attributes + - Image attributes are sanitized by rehypeSanitize for security (removes dangerous CSS) + - Footer images support lazy loading and optional captions from alt text + - CSS styles added for footer images (`.site-footer-image-wrapper`, `.site-footer-image`, `.site-footer-image-caption`) + +### Changed + +- Footer sanitize schema updated to allow `width`, `height`, `style`, and `class` attributes on images +- Footer image component handler updated to pass through size attributes from HTML + +## [1.31.0] - 2025-12-25 + +### Added + +- Customizable footer component with markdown support + - New `Footer` component (`src/components/Footer.tsx`) that renders markdown content + - Footer content can be set in frontmatter `footer` field (markdown) or use `siteConfig.footer.defaultContent` + - Footer can be enabled/disabled globally via `siteConfig.footer.enabled` + - Footer visibility controlled per-page type via `siteConfig.footer.showOnHomepage`, `showOnPosts`, `showOnPages` + - New `showFooter` frontmatter field for posts and pages to override siteConfig defaults + - New `footer` frontmatter field for posts and pages to provide custom markdown content + - Footer renders inside article at bottom for posts/pages, maintains current position on homepage + - Footer supports markdown formatting (links, paragraphs, line breaks) + - Sidebars flush to bottom when footer is enabled (using min-height) + +### Changed + +- Homepage footer section now uses the new `Footer` component instead of hardcoded HTML +- Post and page views now render footer inside article tag (before closing ``) +- Footer component simplified to accept markdown content instead of structured link arrays +- Footer configuration in `siteConfig.ts` now uses `defaultContent` (markdown string) instead of `builtWith`/`createdBy` objects + ## [1.30.2] - 2025-12-25 ### Fixed diff --git a/content/pages/changelog-page.md b/content/pages/changelog-page.md index a9083a7..6ff0ac9 100644 --- a/content/pages/changelog-page.md +++ b/content/pages/changelog-page.md @@ -9,6 +9,40 @@ layout: "sidebar" All notable changes to this project. ![](https://img.shields.io/badge/License-MIT-yellow.svg) +## v1.31.1 + +Released December 25, 2025 + +**Image support in footer component** + +- Footer markdown now supports images using standard markdown syntax or HTML +- Images can be sized using `width`, `height`, `style`, or `class` HTML attributes +- Image attributes are sanitized by rehypeSanitize for security (removes dangerous CSS) +- Footer images support lazy loading and optional captions from alt text +- CSS styles added for footer images + +Updated files: `src/components/Footer.tsx`, `src/styles/global.css` + +## v1.31.0 + +Released December 25, 2025 + +**Customizable footer component with markdown support** + +- New `Footer` component that renders markdown content +- Footer content can be set in frontmatter `footer` field (markdown) or use `siteConfig.footer.defaultContent` +- Footer can be enabled/disabled globally via `siteConfig.footer.enabled` +- Footer visibility controlled per-page type via `siteConfig.footer.showOnHomepage`, `showOnPosts`, `showOnPages` +- New `showFooter` frontmatter field for posts and pages to override siteConfig defaults +- New `footer` frontmatter field for posts and pages to provide custom markdown content +- Footer renders inside article at bottom for posts/pages, maintains current position on homepage +- Footer supports markdown formatting (links, paragraphs, line breaks) +- Sidebars flush to bottom when footer is enabled + +Updated files: `src/components/Footer.tsx`, `src/pages/Home.tsx`, `src/pages/Post.tsx`, `src/config/siteConfig.ts`, `src/styles/global.css`, `convex/schema.ts`, `convex/posts.ts`, `convex/pages.ts`, `scripts/sync-posts.ts`, `src/pages/Write.tsx` + +Documentation updated: `files.md`, `changelog.md` + ## v1.30.2 Released December 25, 2025 diff --git a/content/pages/docs.md b/content/pages/docs.md index 158682b..3bb9a67 100644 --- a/content/pages/docs.md +++ b/content/pages/docs.md @@ -5,6 +5,7 @@ published: true order: 0 layout: "sidebar" rightSidebar: true +footer: true --- Reference documentation for setting up, customizing, and deploying this markdown framework. diff --git a/convex/pages.ts b/convex/pages.ts index 278e468..fbca627 100644 --- a/convex/pages.ts +++ b/convex/pages.ts @@ -20,6 +20,8 @@ export const getAllPages = query({ authorImage: v.optional(v.string()), layout: v.optional(v.string()), rightSidebar: v.optional(v.boolean()), + showFooter: v.optional(v.boolean()), + footer: v.optional(v.string()), }), ), handler: async (ctx) => { @@ -57,6 +59,7 @@ export const getAllPages = query({ authorImage: page.authorImage, layout: page.layout, rightSidebar: page.rightSidebar, + showFooter: page.showFooter, })); }, }); @@ -122,6 +125,8 @@ export const getPageBySlug = query({ authorImage: v.optional(v.string()), layout: v.optional(v.string()), rightSidebar: v.optional(v.boolean()), + showFooter: v.optional(v.boolean()), + footer: v.optional(v.string()), }), v.null(), ), @@ -151,6 +156,8 @@ export const getPageBySlug = query({ authorImage: page.authorImage, layout: page.layout, rightSidebar: page.rightSidebar, + showFooter: page.showFooter, + footer: page.footer, }; }, }); @@ -174,6 +181,8 @@ export const syncPagesPublic = mutation({ authorImage: v.optional(v.string()), layout: v.optional(v.string()), rightSidebar: v.optional(v.boolean()), + showFooter: v.optional(v.boolean()), + footer: v.optional(v.string()), }), ), }, @@ -214,6 +223,8 @@ export const syncPagesPublic = mutation({ authorImage: page.authorImage, layout: page.layout, rightSidebar: page.rightSidebar, + showFooter: page.showFooter, + footer: page.footer, lastSyncedAt: now, }); updated++; diff --git a/convex/posts.ts b/convex/posts.ts index 43faf52..e54ae66 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -23,6 +23,8 @@ export const getAllPosts = query({ authorImage: v.optional(v.string()), layout: v.optional(v.string()), rightSidebar: v.optional(v.boolean()), + showFooter: v.optional(v.boolean()), + footer: v.optional(v.string()), }), ), handler: async (ctx) => { @@ -55,6 +57,7 @@ export const getAllPosts = query({ authorImage: post.authorImage, layout: post.layout, rightSidebar: post.rightSidebar, + showFooter: post.showFooter, })); }, }); @@ -125,6 +128,8 @@ export const getPostBySlug = query({ authorImage: v.optional(v.string()), layout: v.optional(v.string()), rightSidebar: v.optional(v.boolean()), + showFooter: v.optional(v.boolean()), + footer: v.optional(v.string()), }), v.null(), ), @@ -157,6 +162,8 @@ export const getPostBySlug = query({ authorImage: post.authorImage, layout: post.layout, rightSidebar: post.rightSidebar, + showFooter: post.showFooter, + footer: post.footer, }; }, }); @@ -182,6 +189,8 @@ export const syncPosts = internalMutation({ authorImage: v.optional(v.string()), layout: v.optional(v.string()), rightSidebar: v.optional(v.boolean()), + showFooter: v.optional(v.boolean()), + footer: v.optional(v.string()), }), ), }, @@ -224,6 +233,8 @@ export const syncPosts = internalMutation({ authorImage: post.authorImage, layout: post.layout, rightSidebar: post.rightSidebar, + showFooter: post.showFooter, + footer: post.footer, lastSyncedAt: now, }); updated++; @@ -270,6 +281,8 @@ export const syncPostsPublic = mutation({ authorImage: v.optional(v.string()), layout: v.optional(v.string()), rightSidebar: v.optional(v.boolean()), + showFooter: v.optional(v.boolean()), + footer: v.optional(v.string()), }), ), }, @@ -312,6 +325,8 @@ export const syncPostsPublic = mutation({ authorImage: post.authorImage, layout: post.layout, rightSidebar: post.rightSidebar, + showFooter: post.showFooter, + footer: post.footer, lastSyncedAt: now, }); updated++; diff --git a/convex/schema.ts b/convex/schema.ts index eb6ee4d..a0819de 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -20,6 +20,8 @@ export default defineSchema({ authorImage: v.optional(v.string()), // Author avatar image URL (round) layout: v.optional(v.string()), // Layout type: "sidebar" for docs-style layout rightSidebar: v.optional(v.boolean()), // Enable right sidebar with CopyPageDropdown + showFooter: v.optional(v.boolean()), // Show footer on this post (overrides siteConfig default) + footer: v.optional(v.string()), // Footer markdown content (overrides siteConfig defaultContent) lastSyncedAt: v.number(), }) .index("by_slug", ["slug"]) @@ -51,6 +53,8 @@ export default defineSchema({ authorImage: v.optional(v.string()), // Author avatar image URL (round) layout: v.optional(v.string()), // Layout type: "sidebar" for docs-style layout rightSidebar: v.optional(v.boolean()), // Enable right sidebar with CopyPageDropdown + showFooter: v.optional(v.boolean()), // Show footer on this page (overrides siteConfig default) + footer: v.optional(v.string()), // Footer markdown content (overrides siteConfig defaultContent) lastSyncedAt: v.number(), }) .index("by_slug", ["slug"]) diff --git a/files.md b/files.md index 58cf0e2..9ffacef 100644 --- a/files.md +++ b/files.md @@ -33,7 +33,7 @@ A brief description of each file in the codebase. | File | Description | | --------------- | --------------------------------------------------------------------------------------------------------- | -| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs, font family configuration, right sidebar configuration) | +| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs, font family configuration, right sidebar configuration, footer configuration) | ### Pages (`src/pages/`) @@ -55,6 +55,7 @@ A brief description of each file in the codebase. | `PostList.tsx` | Year-grouped blog post list or card grid (supports list/cards view modes) | | `BlogPost.tsx` | Markdown renderer with syntax highlighting, collapsible sections (details/summary), and text wrapping for plain text code blocks | | `CopyPageDropdown.tsx` | Share dropdown with Copy page (markdown to clipboard), View as Markdown (opens raw .md file), Download as SKILL.md (Anthropic Agent Skills format), and Open in AI links (ChatGPT, Claude, Perplexity) using GitHub raw URLs with universal prompt | +| `Footer.tsx` | Footer component that renders markdown content from frontmatter footer field or siteConfig.defaultContent. Can be enabled/disabled globally and per-page via frontmatter showFooter field. Renders inside article at bottom for posts/pages, and in current position on homepage. Supports images with size control via HTML attributes (width, height, style, class) | | `SearchModal.tsx` | Full text search modal with keyboard navigation | | `FeaturedCards.tsx` | Card grid for featured posts/pages with excerpts | | `LogoMarquee.tsx` | Scrolling logo gallery with clickable links | @@ -89,7 +90,7 @@ A brief description of each file in the codebase. | File | Description | | ------------ | ------------------------------------------------------------------------------------ | -| `global.css` | Global CSS with theme variables, centralized font-size CSS variables for all themes, sidebar styling with alternate background colors, hidden scrollbar, and consistent borders using box-shadow for docs-style layout. Left sidebar (`.post-sidebar-wrapper`) and right sidebar (`.post-sidebar-right`) have separate, independent styles | +| `global.css` | Global CSS with theme variables, centralized font-size CSS variables for all themes, sidebar styling with alternate background colors, hidden scrollbar, and consistent borders using box-shadow for docs-style layout. Left sidebar (`.post-sidebar-wrapper`) and right sidebar (`.post-sidebar-right`) have separate, independent styles. Footer image styles (`.site-footer-image-wrapper`, `.site-footer-image`, `.site-footer-image-caption`) for responsive image display | ## Convex Backend (`convex/`) @@ -141,6 +142,9 @@ Markdown files with frontmatter for blog posts. Each file becomes a blog post. | `featuredOrder` | Order in featured section (optional) | | `authorName` | Author display name (optional) | | `authorImage` | Round author avatar image URL (optional) | +| `rightSidebar` | Enable right sidebar with CopyPageDropdown (optional) | +| `showFooter` | Show footer on this post (optional, overrides siteConfig default) | +| `footer` | Footer markdown content (optional, overrides siteConfig.defaultContent) | ## Static Pages (`content/pages/`) @@ -159,6 +163,8 @@ Markdown files for static pages like About, Projects, Contact, Changelog. | `authorName` | Author display name (optional) | | `authorImage` | Round author avatar image URL (optional) | | `rightSidebar` | Enable right sidebar with CopyPageDropdown (optional) | +| `showFooter` | Show footer on this page (optional, overrides siteConfig default) | +| `footer` | Footer markdown content (optional, overrides siteConfig.defaultContent) | ## Scripts (`scripts/`) diff --git a/public/images/logos/agentmail.svg b/public/images/logos/agentmail.svg new file mode 100644 index 0000000..470039d --- /dev/null +++ b/public/images/logos/agentmail.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/raw/changelog.md b/public/raw/changelog.md index 6a37681..8333bab 100644 --- a/public/raw/changelog.md +++ b/public/raw/changelog.md @@ -8,6 +8,91 @@ Date: 2025-12-26 All notable changes to this project. ![](https://img.shields.io/badge/License-MIT-yellow.svg) +## v1.31.1 + +Released December 25, 2025 + +**Image support in footer component** + +- Footer markdown now supports images using standard markdown syntax or HTML +- Images can be sized using `width`, `height`, `style`, or `class` HTML attributes +- Image attributes are sanitized by rehypeSanitize for security (removes dangerous CSS) +- Footer images support lazy loading and optional captions from alt text +- CSS styles added for footer images + +Updated files: `src/components/Footer.tsx`, `src/styles/global.css` + +## v1.31.0 + +Released December 25, 2025 + +**Customizable footer component with markdown support** + +- New `Footer` component that renders markdown content +- Footer content can be set in frontmatter `footer` field (markdown) or use `siteConfig.footer.defaultContent` +- Footer can be enabled/disabled globally via `siteConfig.footer.enabled` +- Footer visibility controlled per-page type via `siteConfig.footer.showOnHomepage`, `showOnPosts`, `showOnPages` +- New `showFooter` frontmatter field for posts and pages to override siteConfig defaults +- New `footer` frontmatter field for posts and pages to provide custom markdown content +- Footer renders inside article at bottom for posts/pages, maintains current position on homepage +- Footer supports markdown formatting (links, paragraphs, line breaks) +- Sidebars flush to bottom when footer is enabled + +Updated files: `src/components/Footer.tsx`, `src/pages/Home.tsx`, `src/pages/Post.tsx`, `src/config/siteConfig.ts`, `src/styles/global.css`, `convex/schema.ts`, `convex/posts.ts`, `convex/pages.ts`, `scripts/sync-posts.ts`, `src/pages/Write.tsx` + +Documentation updated: `files.md`, `changelog.md` + +## v1.30.2 + +Released December 25, 2025 + +**Right sidebar default behavior fix** + +- Right sidebar no longer appears on pages/posts without explicit `rightSidebar: true` in frontmatter + - Changed default behavior: right sidebar is now opt-in only + - Pages like About and Contact now render without the right sidebar as expected + - `CopyPageDropdown` correctly appears in nav bar when right sidebar is disabled +- Logic in `Post.tsx` changed from `(page.rightSidebar ?? true)` to `page.rightSidebar === true` + +Updated files: `src/pages/Post.tsx` + +## v1.30.1 + +Released December 25, 2025 + +**TypeScript error fix** + +- TypeScript error in `convex/posts.ts` where `rightSidebar` was used in mutation handlers but missing from args validators + - Added `rightSidebar: v.optional(v.boolean())` to `syncPosts` args validator + - Added `rightSidebar: v.optional(v.boolean())` to `syncPostsPublic` args validator + +Updated files: `convex/posts.ts` + +## v1.30.0 + +Released December 25, 2025 + +**Right sidebar feature for posts and pages** + +- New `RightSidebar` component that displays `CopyPageDropdown` in a right sidebar + - Appears at 1135px+ viewport width when enabled + - Controlled by `siteConfig.rightSidebar.enabled` (global toggle) + - Per-post/page control via `rightSidebar: true` frontmatter field (opt-in only) + - Three-column layout support: left sidebar (TOC), main content, right sidebar (CopyPageDropdown) + - CopyPageDropdown automatically moves from nav to right sidebar when enabled + - Responsive: right sidebar hidden below 1135px, CopyPageDropdown returns to nav +- Right sidebar configuration in siteConfig + - `rightSidebar.enabled`: Global toggle for right sidebar feature + - `rightSidebar.minWidth`: Minimum viewport width to show sidebar (default: 1135px) +- `rightSidebar` frontmatter field + - Available for both blog posts and pages + - Optional boolean field to enable/disable right sidebar per post/page + - Added to Write page frontmatter reference with copy button + +Updated files: `src/components/RightSidebar.tsx`, `src/pages/Post.tsx`, `src/config/siteConfig.ts`, `src/styles/global.css`, `convex/schema.ts`, `convex/posts.ts`, `convex/pages.ts`, `scripts/sync-posts.ts`, `src/pages/Write.tsx` + +Documentation updated: `content/blog/setup-guide.md`, `content/pages/docs.md`, `files.md`, `changelog.md` + ## v1.29.0 Released December 25, 2025 diff --git a/scripts/sync-posts.ts b/scripts/sync-posts.ts index 14f81ee..2d59920 100644 --- a/scripts/sync-posts.ts +++ b/scripts/sync-posts.ts @@ -57,6 +57,8 @@ interface ParsedPost { authorImage?: string; // Author avatar image URL (round) layout?: string; // Layout type: "sidebar" for docs-style layout rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled) + showFooter?: boolean; // Show footer on this post (overrides siteConfig default) + footer?: string; // Footer markdown content (overrides siteConfig defaultContent) } // Page frontmatter (for static pages like About, Projects, Contact) @@ -74,6 +76,7 @@ interface PageFrontmatter { authorImage?: string; // Author avatar image URL (round) layout?: string; // Layout type: "sidebar" for docs-style layout rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled) + showFooter?: boolean; // Show footer on this page (overrides siteConfig default) } interface ParsedPage { @@ -91,6 +94,7 @@ interface ParsedPage { authorImage?: string; // Author avatar image URL (round) layout?: string; // Layout type: "sidebar" for docs-style layout rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled) + showFooter?: boolean; // Show footer on this page (overrides siteConfig default) } // Calculate reading time based on word count @@ -132,6 +136,8 @@ function parseMarkdownFile(filePath: string): ParsedPost | null { authorImage: frontmatter.authorImage, // Author avatar image URL layout: frontmatter.layout, // Layout type: "sidebar" for docs-style layout rightSidebar: frontmatter.rightSidebar, // Enable right sidebar with CopyPageDropdown + showFooter: frontmatter.showFooter, // Show footer on this post + footer: frontmatter.footer, // Footer markdown content }; } catch (error) { console.error(`Error parsing ${filePath}:`, error); @@ -184,6 +190,7 @@ function parsePageFile(filePath: string): ParsedPage | null { authorImage: frontmatter.authorImage, // Author avatar image URL layout: frontmatter.layout, // Layout type: "sidebar" for docs-style layout rightSidebar: frontmatter.rightSidebar, // Enable right sidebar with CopyPageDropdown + showFooter: frontmatter.showFooter, // Show footer on this page }; } catch (error) { console.error(`Error parsing page ${filePath}:`, error); diff --git a/src/components/BlogPost.tsx b/src/components/BlogPost.tsx index 760c7a8..0a5d4c9 100644 --- a/src/components/BlogPost.tsx +++ b/src/components/BlogPost.tsx @@ -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) => { + // 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 ( + + # + + ); +} + 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 (

+ {children}

); @@ -405,6 +429,7 @@ export default function BlogPost({ content }: BlogPostProps) { const id = generateSlug(getTextContent(children)); return (

+ {children}

); @@ -413,6 +438,7 @@ export default function BlogPost({ content }: BlogPostProps) { const id = generateSlug(getTextContent(children)); return (

+ {children}

); @@ -421,6 +447,7 @@ export default function BlogPost({ content }: BlogPostProps) { const id = generateSlug(getTextContent(children)); return (

+ {children}

); @@ -429,6 +456,7 @@ export default function BlogPost({ content }: BlogPostProps) { const id = generateSlug(getTextContent(children)); return (
+ {children}
); @@ -437,6 +465,7 @@ export default function BlogPost({ content }: BlogPostProps) { const id = generateSlug(getTextContent(children)); return (
+ {children}
); diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..3d8e8e7 --- /dev/null +++ b/src/components/Footer.tsx @@ -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 ( +
+
+ {children}

; + }, + img({ src, alt, width, height, style, className }) { + return ( + + {alt + {alt && ( + {alt} + )} + + ); + }, + a({ href, children }) { + const isExternal = href?.startsWith("http"); + return ( + + {children} + + ); + }, + }} + > + {footerContent} +
+
+
+ ); +} diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index a8b51d9..6a5ca99 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -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 ( ); diff --git a/src/config/siteConfig.ts b/src/config/siteConfig.ts index cd96311..4a7b3e0 100644 --- a/src/config/siteConfig.ts +++ b/src/config/siteConfig.ts @@ -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 diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 985a25d..66e6cce 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -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 */} -
-

- Built with{" "} - - Convex - {" "} - for real-time sync and deployed on{" "} - - Netlify - - . Read the{" "} - - project on GitHub - {" "} - to fork and deploy your own. View{" "} - - real-time site stats - - . -

-

-

-

- Created by{" "} - - Wayne - {" "} - with Convex, Cursor, and Claude Opus 4.5. Follow on{" "} - - Twitter/X - - ,{" "} - - LinkedIn - - , and{" "} - - GitHub - - . This project is licensed under the MIT{" "} - - License. - {" "} -

-
+ {siteConfig.footer.enabled && siteConfig.footer.showOnHomepage && ( + + + {/* Footer - shown inside article at bottom for posts */} + {siteConfig.footer.enabled && + (post.showFooter !== undefined ? post.showFooter : siteConfig.footer.showOnPosts) && ( +