fix: use frontmatter image for OG meta instead of default

This commit is contained in:
Wayne Sutton
2025-12-20 16:34:48 -08:00
parent 98a43b86a2
commit 4b187cff53
27 changed files with 836 additions and 204 deletions

View File

@@ -16,7 +16,6 @@ alwaysApply: true
- always create type-safe code
- **!IMPORTANT**: **DO NOT** externalize or document your work, usage guidelines, or benchmarks (e.g. `README.md`, `CONTRIBUTING.md`, `SUMMARY.md`, `USAGE_GUIDELINES.md` after completing the task, unless explicitly instructed to do so. You may include a brief summary of your work, but do not create separate documentation files for it.
- When creating Convex mutations, always patch directly without reading first, use indexed queries for ownership checks instead of `ctx.db.get()`, make mutations idempotent with early returns, use timestamp-based ordering for new items, and use `Promise.all()` for parallel independent operations to avoid write conflicts.
- - When a task touches changelog.md, the changelog page, or files.md, run git log --date=short (or check commit history) and set each release date to match the real commit timeline—no placeholders or future months.
-Do you understand, what Im asking? Never assume anything on your own, if anything isnt clear, please ask questions and clarify your doubts.
@@ -53,9 +52,11 @@ alwaysApply: true
- Treat me as an new developer
- Be accurate and thorough
- Keep a list of the codebase files, provide a brief description of what each file one does called files.md.
- you keep a developer friendly changelog.md of new features added based on https://keepachangelog.com/en/1.0.0/
- you keep a developer friendly changelog.md of new features added based on https://keepachangelog.com/en/1.0.0/ and keep changelog-page.md updated also.
- keep a track of changes completed in task.md
- When a task touches changelog.md, the changelog page changelog-page.md, or files.md, run git log --date=short (or check commit history) and set each release date to match the real commit timeline—no placeholders or future months.
- prd files always end with .MD and not .prd
- prd files are located in the prds folder except forchangelog.M , files.MD, README.md and TASK.MDwhich can stay in the root folder
- prd files are located in the prds folder except for changelog.MD, files.MD, README.md and TASK.MD which can stay in the root folder
- create type-safe code always, if the prds folder does not exist create one
- Give the answer immediately. Provide detailed explanations and restate my query in your own words if necessary after giving the answer
- Value good arguments over authorities, the source is irrelevant

3
.gitignore vendored
View File

@@ -30,3 +30,6 @@ dist-ssr
# Cursor rules
.cursor/rules/write.mdc
# PRD files
prds/metadataforsubs.md

View File

@@ -361,13 +361,20 @@ See `prds/howtoavoidwriteconflicts.md` for full details.
## Configuration
Site config lives in `src/pages/Home.tsx`:
Site config lives in `src/config/siteConfig.ts`:
```typescript
const siteConfig = {
export default {
name: "Site Name",
title: "Tagline",
logo: "/images/logo.svg", // null to hide
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
featuredViewMode: "list", // 'list' or 'cards'
showViewToggle: true,
logoGallery: {

View File

@@ -28,6 +28,7 @@ npm run sync:prod # production
- Featured section with list/card view toggle
- Logo gallery with continuous marquee scroll
- Static raw markdown files at `/raw/{slug}.md`
- Dedicated blog page with configurable navigation order
### SEO and Discovery
@@ -61,7 +62,7 @@ When you fork this project, update these files with your site information:
| File | What to update |
| ----------------------------------- | ----------------------------------------------------------- |
| `src/pages/Home.tsx` | Site name, title, intro, bio, featured config, logo gallery |
| `src/config/siteConfig.ts` | Site name, title, intro, bio, blog page, logo gallery |
| `convex/http.ts` | `SITE_URL`, `SITE_NAME` (API responses, sitemap) |
| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` (RSS feeds) |
| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME`, `DEFAULT_OG_IMAGE` (OG tags) |
@@ -222,6 +223,36 @@ In card view, the `image` field displays as a square thumbnail above the title.
Square thumbnails: 400x400px minimum (800x800px for retina). The same image can serve as both the OG image for social sharing and the featured card thumbnail.
## Blog Page
The site supports a dedicated blog page at `/blog`. Configure in `src/config/siteConfig.ts`:
```typescript
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
```
| Option | Description |
| ------ | ----------- |
| `enabled` | Enable the `/blog` route |
| `showInNav` | Show Blog link in navigation |
| `title` | Text for nav link and page heading |
| `order` | Position in navigation (lower = first) |
| `displayOnHomepage` | Show post list on homepage |
**Display options:**
- Homepage only: `displayOnHomepage: true`, `blogPage.enabled: false`
- Blog page only: `displayOnHomepage: false`, `blogPage.enabled: true`
- Both: `displayOnHomepage: true`, `blogPage.enabled: true`
**Navigation order:** The Blog link merges with page links and sorts by order. Pages use the `order` field in frontmatter. Set `blogPage.order: 5` to position Blog after pages with order 0-4.
## Logo Gallery
The homepage includes a scrolling logo gallery with sample logos. Configure in `siteConfig`:

20
TASK.md
View File

@@ -2,21 +2,23 @@
## To Do
- [ ] add componet fork fix for stats
- [ ] Add blog page list and config
- [ ] add github code block
- [ ] add home to mobile menu
- [ ] Add markdown write page with copy option
- [ ] add github code block
- [ ] create a ui site config page
- [ ] create a prompt formator or skill or agent to change everything at once after forking
- [ ] Add app background image option
## Current Status
v1.10.0 ready for deployment. Build passes. TypeScript verified. Documentation updated.
v1.12.1 deployed. OG images now use post/page image from frontmatter instead of always defaulting.
## Completed
- [x] Open Graph image fix for posts and pages with frontmatter images
- [x] Dedicated blog page with configurable display options
- [x] Blog page navigation order via siteConfig.blogPage.order
- [x] Centralized siteConfig.ts for site configuration
- [x] Posts display toggle for homepage and/or blog page
- [x] move home to the top of the mobile menu
- [x] Fork configuration documentation in docs.md and setup-guide.md
- [x] "Files to Update When Forking" section with all 9 configuration files
- [x] Backend configuration examples for Convex files
@@ -71,6 +73,12 @@ v1.10.0 ready for deployment. Build passes. TypeScript verified. Documentation u
- [x] Perplexity added to AI service options
- [x] Featured image support with square thumbnails in card view
- [x] Improved markdown table CSS styling
- [x] Aggregate component integration for efficient stats counting (O(log n) vs O(n))
- [x] Three aggregate components: pageViewsByPath, totalPageViews, uniqueVisitors
- [x] Chunked backfilling mutation for existing page view data
- [x] Aggregate component registration in convex.config.ts
- [x] Stats query updated to use aggregate counts
- [x] Aggregate component documentation in prds/howstatsworks.md
## Deployment Steps

View File

@@ -4,6 +4,62 @@ 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.12.1] - 2025-12-20
### Fixed
- Open Graph images now use post/page `image` field from frontmatter
- Posts with images in frontmatter display their specific OG image
- Posts without images fall back to `og-default.svg`
- Pages now supported with appropriate `og:type` set to "website"
- Relative image paths resolved to absolute URLs
### Changed
- Renamed `generatePostMetaHtml` to `generateMetaHtml` in `convex/http.ts`
- `/meta/post` endpoint now checks for pages if no post found
- Meta HTML generation accepts optional `image` and `type` parameters
### Technical
- Updated `convex/http.ts` with image resolution logic
- Handles both absolute URLs and relative paths for images
- Deployed to production Convex
## [1.12.0] - 2025-12-20
### Added
- Dedicated blog page at `/blog` with configurable display
- Enable/disable via `siteConfig.blogPage.enabled`
- Show/hide from navigation via `siteConfig.blogPage.showInNav`
- Custom page title via `siteConfig.blogPage.title`
- Navigation order via `siteConfig.blogPage.order` (lower = first)
- Centralized site configuration in `src/config/siteConfig.ts`
- Moved all site settings from `Home.tsx` to dedicated config file
- Easier to customize when forking
- Flexible post display options
- `displayOnHomepage`: Show posts on the homepage
- `blogPage.enabled`: Show posts on dedicated `/blog` page
- Both can be enabled for dual display
### Changed
- Navigation now combines Blog link with pages and sorts by order
- Blog link position controlled by `siteConfig.blogPage.order`
- Pages sorted by frontmatter `order` field (lower = first)
- Items without order default to 999 (appear last, alphabetically)
- `Home.tsx` imports siteConfig instead of defining inline
- `Layout.tsx` uses unified nav item sorting for desktop and mobile
### Technical
- New file: `src/config/siteConfig.ts`
- New page: `src/pages/Blog.tsx`
- Updated: `src/App.tsx` (conditional blog route)
- Updated: `src/components/Layout.tsx` (nav item ordering)
- Updated: `src/styles/global.css` (blog page styles)
## [1.11.1] - 2025-12-20
### Fixed

View File

@@ -400,7 +400,7 @@ When you fork this project, update these files with your site information:
| File | What to update |
| ----------------------------------- | ----------------------------------------------------------- |
| `src/pages/Home.tsx` | Site name, title, intro, bio, featured config, logo gallery |
| `src/config/siteConfig.ts` | Site name, title, intro, bio, blog page, logo gallery |
| `convex/http.ts` | `SITE_URL`, `SITE_NAME` (API responses, sitemap) |
| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` (RSS feeds) |
| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME`, `DEFAULT_OG_IMAGE` (OG tags) |
@@ -493,23 +493,27 @@ const DEFAULT_OG_IMAGE = "/images/og-default.svg";
### Update Site Configuration
Edit `src/pages/Home.tsx` to customize:
Edit `src/config/siteConfig.ts` to customize:
```typescript
const siteConfig = {
export default {
name: "Your Name",
title: "Your Title",
intro: "Your introduction...",
bio: "Your bio...",
// Blog page configuration
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
// Featured section options
featuredViewMode: "list", // 'list' or 'cards'
showViewToggle: true, // Let users switch between views
featuredItems: [
{ slug: "post-slug", type: "post" },
{ slug: "page-slug", type: "page" },
],
featuredEssays: [{ title: "Post Title", slug: "post-slug" }],
// Logo gallery (marquee scroll with clickable links)
logoGallery: {
@@ -621,6 +625,36 @@ Delete the sample files from `public/images/logos/` and clear the images array,
The gallery uses CSS animations for smooth infinite scrolling. Logos display in grayscale and colorize on hover.
### Blog page
The site supports a dedicated blog page at `/blog`. Configure in `src/config/siteConfig.ts`:
```typescript
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
```
| Option | Description |
| ------ | ----------- |
| `enabled` | Enable the `/blog` route |
| `showInNav` | Show Blog link in navigation |
| `title` | Text for nav link and page heading |
| `order` | Position in navigation (lower = first) |
| `displayOnHomepage` | Show post list on homepage |
**Display options:**
- Homepage only: `displayOnHomepage: true`, `blogPage.enabled: false`
- Blog page only: `displayOnHomepage: false`, `blogPage.enabled: true`
- Both: `displayOnHomepage: true`, `blogPage.enabled: true`
**Navigation order:** The Blog link merges with page links and sorts by order. Pages use the `order` field in frontmatter. Set `blogPage.order: 5` to position Blog after pages with order 0-4.
### Scroll-to-top button
A scroll-to-top button appears after scrolling down on posts and pages. Configure it in `src/components/Layout.tsx`:

View File

@@ -2,7 +2,7 @@
title: "About"
slug: "about"
published: true
order: 1
order: 2
excerpt: "An open-source markdown sync site for developers and AI agents."
---
@@ -55,6 +55,7 @@ It's a hybrid: developer workflow for publishing + real-time delivery like a dyn
- Full text search with Command+K shortcut
- Featured section with list/card view toggle and excerpts
- Logo gallery with clickable links and marquee scroll
- Dedicated blog page with configurable navigation order
- Real-time analytics at `/stats`
- RSS feeds and sitemap for SEO
- Static raw markdown files at `/raw/{slug}.md`

View File

@@ -7,6 +7,48 @@ order: 5
All notable changes to this project.
## v1.12.1
Released December 20, 2025
**Open Graph image fix**
- Posts with `image` in frontmatter now display their specific OG image when shared
- Posts without images fall back to `og-default.svg`
- Pages now supported with `og:type` set to "website" instead of "article"
- Relative image paths (like `/images/v17.png`) resolve to absolute URLs
The `/meta/post` endpoint in `convex/http.ts` now passes the `image` field from posts and pages to the meta HTML generator. If no post matches the slug, it checks for a page with that slug.
## v1.12.0
Released December 20, 2025
**Dedicated blog page with configurable navigation**
- New `/blog` page for dedicated post listing
- Enable/disable via `siteConfig.blogPage.enabled`
- Control navigation position with `siteConfig.blogPage.order`
- Centralized site configuration in `src/config/siteConfig.ts`
- Flexible post display: homepage only, blog page only, or both
Configuration options:
```typescript
// src/config/siteConfig.ts
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
```
The Blog link now integrates with page navigation ordering. Set `order: 5` to place it after pages with order 0-4, or `order: 0` to keep it first.
New files: `src/config/siteConfig.ts`, `src/pages/Blog.tsx`
## v1.11.1
Released December 20, 2025

View File

@@ -2,7 +2,7 @@
title: "Contact"
slug: "contact"
published: true
order: 3
order: 4
---
You found the contact page. Nice

View File

@@ -154,7 +154,7 @@ When you fork this project, update these files with your site information:
| File | What to update |
|------|----------------|
| `src/pages/Home.tsx` | Site name, title, intro, bio, featured config, logo gallery |
| `src/config/siteConfig.ts` | Site name, title, intro, bio, blog page, logo gallery |
| `convex/http.ts` | `SITE_URL`, `SITE_NAME` (API responses, sitemap) |
| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` (RSS feeds) |
| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME`, `DEFAULT_OG_IMAGE` (OG tags) |
@@ -203,23 +203,30 @@ const DEFAULT_OG_IMAGE = "/images/og-default.svg";
These constants affect RSS feeds, API responses, sitemaps, and social sharing metadata.
### Homepage settings
### Site settings
Edit `src/pages/Home.tsx`:
Edit `src/config/siteConfig.ts`:
```typescript
const siteConfig = {
export default {
name: "Site Name",
title: "Tagline",
logo: "/images/logo.svg", // null to hide
intro: "Introduction text...",
bio: "Bio text...",
// Blog page configuration
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
// Featured section
featuredViewMode: "list", // 'list' or 'cards'
showViewToggle: true,
featuredItems: [{ slug: "post-slug", type: "post" }],
featuredEssays: [{ title: "Post Title", slug: "post-slug" }],
// Logo gallery (with clickable links)
logoGallery: {
@@ -312,6 +319,36 @@ logoGallery: {
**To remove samples:** Delete files from `public/images/logos/` or clear the images array.
### Blog page
The site supports a dedicated blog page at `/blog`. Configure in `src/config/siteConfig.ts`:
```typescript
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
```
| Option | Description |
| ------------------- | -------------------------------------- |
| `enabled` | Enable the `/blog` route |
| `showInNav` | Show Blog link in navigation |
| `title` | Text for nav link and page heading |
| `order` | Position in navigation (lower = first) |
| `displayOnHomepage` | Show post list on homepage |
**Display options:**
- Homepage only: `displayOnHomepage: true`, `blogPage.enabled: false`
- Blog page only: `displayOnHomepage: false`, `blogPage.enabled: true`
- Both: `displayOnHomepage: true`, `blogPage.enabled: true`
**Navigation order:** The Blog link merges with page links and sorts by order. Pages use the `order` field in frontmatter. Set `blogPage.order: 5` to position Blog after pages with order 0-4.
### Scroll-to-top button
A scroll-to-top button appears after scrolling down. Configure in `src/components/Layout.tsx`:

View File

@@ -2,7 +2,7 @@
title: "Projects"
slug: "projects"
published: true
order: 2
order: 3
---
This markdown site is open source and built to be extended. Here is what ships out of the box.

View File

@@ -220,21 +220,34 @@ function escapeHtml(text: string): string {
.replace(/'/g, "'");
}
// Generate Open Graph HTML for a post
function generatePostMetaHtml(post: {
// Generate Open Graph HTML for a post or page
function generateMetaHtml(content: {
title: string;
description: string;
slug: string;
date: string;
date?: string;
readTime?: string;
image?: string;
type?: "post" | "page";
}): string {
const siteUrl = process.env.SITE_URL || "https://markdowncms.netlify.app";
const siteName = "markdown sync site";
const defaultImage = `${siteUrl}/og-image.png`;
const canonicalUrl = `${siteUrl}/${post.slug}`;
const defaultImage = `${siteUrl}/images/og-default.svg`;
const canonicalUrl = `${siteUrl}/${content.slug}`;
const safeTitle = escapeHtml(post.title);
const safeDescription = escapeHtml(post.description);
// Resolve image URL: use post image if available, otherwise default
let ogImage = defaultImage;
if (content.image) {
// Handle both absolute URLs and relative paths
ogImage = content.image.startsWith("http")
? content.image
: `${siteUrl}${content.image}`;
}
const safeTitle = escapeHtml(content.title);
const safeDescription = escapeHtml(content.description);
const contentType = content.type || "post";
const ogType = contentType === "post" ? "article" : "website";
return `<!DOCTYPE html>
<html lang="en">
@@ -250,17 +263,17 @@ function generatePostMetaHtml(post: {
<!-- Open Graph -->
<meta property="og:title" content="${safeTitle}">
<meta property="og:description" content="${safeDescription}">
<meta property="og:image" content="${defaultImage}">
<meta property="og:image" content="${ogImage}">
<meta property="og:url" content="${canonicalUrl}">
<meta property="og:type" content="article">
<meta property="og:site_name" content="${siteName}">
<meta property="article:published_time" content="${post.date}">
<meta property="og:type" content="${ogType}">
<meta property="og:site_name" content="${siteName}">${content.date ? `
<meta property="article:published_time" content="${content.date}">` : ""}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${safeTitle}">
<meta name="twitter:description" content="${safeDescription}">
<meta name="twitter:image" content="${defaultImage}">
<meta name="twitter:image" content="${ogImage}">
<!-- Redirect to actual page after a brief delay for crawlers -->
<script>
@@ -271,14 +284,14 @@ function generatePostMetaHtml(post: {
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 680px; margin: 50px auto; padding: 20px; color: #111;">
<h1 style="font-size: 32px; margin-bottom: 16px;">${safeTitle}</h1>
<p style="color: #666; margin-bottom: 24px;">${safeDescription}</p>
<p style="font-size: 14px; color: #999;">${post.date}${post.readTime ? ` · ${post.readTime}` : ""}</p>
<p style="margin-top: 24px;"><small>Redirecting to full article...</small></p>
<p style="color: #666; margin-bottom: 24px;">${safeDescription}</p>${content.date ? `
<p style="font-size: 14px; color: #999;">${content.date}${content.readTime ? ` · ${content.readTime}` : ""}</p>` : ""}
<p style="margin-top: 24px;"><small>Redirecting to full ${contentType}...</small></p>
</body>
</html>`;
}
// HTTP endpoint for Open Graph metadata
// HTTP endpoint for Open Graph metadata (supports both posts and pages)
http.route({
path: "/meta/post",
method: "GET",
@@ -291,27 +304,52 @@ http.route({
}
try {
// First try to find a post
const post = await ctx.runQuery(api.posts.getPostBySlug, { slug });
if (!post) {
return new Response("Post not found", { status: 404 });
if (post) {
const html = generateMetaHtml({
title: post.title,
description: post.description,
slug: post.slug,
date: post.date,
readTime: post.readTime,
image: post.image,
type: "post",
});
return new Response(html, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control":
"public, max-age=60, s-maxage=300, stale-while-revalidate=600",
},
});
}
const html = generatePostMetaHtml({
title: post.title,
description: post.description,
slug: post.slug,
date: post.date,
readTime: post.readTime,
});
// If no post found, try to find a page
const page = await ctx.runQuery(api.pages.getPageBySlug, { slug });
return new Response(html, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control":
"public, max-age=60, s-maxage=300, stale-while-revalidate=600",
},
});
if (page) {
const html = generateMetaHtml({
title: page.title,
description: page.excerpt || `${page.title} - ${SITE_NAME}`,
slug: page.slug,
image: page.image,
type: "page",
});
return new Response(html, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control":
"public, max-age=60, s-maxage=300, stale-while-revalidate=600",
},
});
}
// Neither post nor page found
return new Response("Content not found", { status: 404 });
} catch {
return new Response("Internal server error", { status: 500 });
}

View File

@@ -27,11 +27,18 @@ A brief description of each file in the codebase.
| `App.tsx` | Main app component with routing |
| `vite-env.d.ts` | Vite environment type definitions |
### Config (`src/config/`)
| File | Description |
| --------------- | -------------------------------------------------------------------------------- |
| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display, nav order) |
### Pages (`src/pages/`)
| File | Description |
| ----------- | ----------------------------------------------------------------- |
| `Home.tsx` | Landing page with siteConfig (update name/title/bio when forking) |
| `Home.tsx` | Landing page with featured content and optional post list |
| `Blog.tsx` | Dedicated blog page with post list (configurable via siteConfig.blogPage) |
| `Post.tsx` | Individual blog post view (update SITE_URL/SITE_NAME when forking) |
| `Stats.tsx` | Real-time analytics dashboard with visitor stats |

View File

@@ -2,7 +2,7 @@
---
Type: page
Date: 2025-12-20
Date: 2025-12-21
---
An open-source markdown sync site for developers and AI agents. Publish from the terminal with `npm run sync`. Write locally, sync instantly with real-time updates. Powered by Convex and Netlify.
@@ -54,6 +54,7 @@ It's a hybrid: developer workflow for publishing + real-time delivery like a dyn
- Full text search with Command+K shortcut
- Featured section with list/card view toggle and excerpts
- Logo gallery with clickable links and marquee scroll
- Dedicated blog page with configurable navigation order
- Real-time analytics at `/stats`
- RSS feeds and sitemap for SEO
- Static raw markdown files at `/raw/{slug}.md`

View File

@@ -2,11 +2,65 @@
---
Type: page
Date: 2025-12-20
Date: 2025-12-21
---
All notable changes to this project.
## v1.12.1
Released December 20, 2025
**Open Graph image fix**
- Posts with `image` in frontmatter now display their specific OG image when shared
- Posts without images fall back to `og-default.svg`
- Pages now supported with `og:type` set to "website" instead of "article"
- Relative image paths (like `/images/v17.png`) resolve to absolute URLs
The `/meta/post` endpoint in `convex/http.ts` now passes the `image` field from posts and pages to the meta HTML generator. If no post matches the slug, it checks for a page with that slug.
## v1.12.0
Released December 20, 2025
**Dedicated blog page with configurable navigation**
- New `/blog` page for dedicated post listing
- Enable/disable via `siteConfig.blogPage.enabled`
- Control navigation position with `siteConfig.blogPage.order`
- Centralized site configuration in `src/config/siteConfig.ts`
- Flexible post display: homepage only, blog page only, or both
Configuration options:
```typescript
// src/config/siteConfig.ts
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
```
The Blog link now integrates with page navigation ordering. Set `order: 5` to place it after pages with order 0-4, or `order: 0` to keep it first.
New files: `src/config/siteConfig.ts`, `src/pages/Blog.tsx`
## v1.11.1
Released December 20, 2025
**Fix historical stats display and chunked backfilling**
- Stats page now shows all historical page views correctly
- Changed `getStats` to use direct counting until aggregates are fully backfilled
- Backfill mutation now processes 500 records at a time (chunked)
- Prevents memory limit issues with large datasets (16MB Convex limit)
- Schedules itself to continue processing until complete
## v1.11.0
Released December 20, 2025

View File

@@ -2,7 +2,7 @@
---
Type: page
Date: 2025-12-20
Date: 2025-12-21
---
You found the contact page. Nice

View File

@@ -2,7 +2,7 @@
---
Type: page
Date: 2025-12-20
Date: 2025-12-21
---
Reference documentation for setting up, customizing, and deploying this markdown site.
@@ -154,7 +154,7 @@ When you fork this project, update these files with your site information:
| File | What to update |
|------|----------------|
| `src/pages/Home.tsx` | Site name, title, intro, bio, featured config, logo gallery |
| `src/config/siteConfig.ts` | Site name, title, intro, bio, blog page, logo gallery |
| `convex/http.ts` | `SITE_URL`, `SITE_NAME` (API responses, sitemap) |
| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` (RSS feeds) |
| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME`, `DEFAULT_OG_IMAGE` (OG tags) |
@@ -203,23 +203,30 @@ const DEFAULT_OG_IMAGE = "/images/og-default.svg";
These constants affect RSS feeds, API responses, sitemaps, and social sharing metadata.
### Homepage settings
### Site settings
Edit `src/pages/Home.tsx`:
Edit `src/config/siteConfig.ts`:
```typescript
const siteConfig = {
export default {
name: "Site Name",
title: "Tagline",
logo: "/images/logo.svg", // null to hide
intro: "Introduction text...",
bio: "Bio text...",
// Blog page configuration
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
// Featured section
featuredViewMode: "list", // 'list' or 'cards'
showViewToggle: true,
featuredItems: [{ slug: "post-slug", type: "post" }],
featuredEssays: [{ title: "Post Title", slug: "post-slug" }],
// Logo gallery (with clickable links)
logoGallery: {
@@ -312,6 +319,36 @@ logoGallery: {
**To remove samples:** Delete files from `public/images/logos/` or clear the images array.
### Blog page
The site supports a dedicated blog page at `/blog`. Configure in `src/config/siteConfig.ts`:
```typescript
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
```
| Option | Description |
| ------------------- | -------------------------------------- |
| `enabled` | Enable the `/blog` route |
| `showInNav` | Show Blog link in navigation |
| `title` | Text for nav link and page heading |
| `order` | Position in navigation (lower = first) |
| `displayOnHomepage` | Show post list on homepage |
**Display options:**
- Homepage only: `displayOnHomepage: true`, `blogPage.enabled: false`
- Blog page only: `displayOnHomepage: false`, `blogPage.enabled: true`
- Both: `displayOnHomepage: true`, `blogPage.enabled: true`
**Navigation order:** The Blog link merges with page links and sorts by order. Pages use the `order` field in frontmatter. Set `blogPage.order: 5` to position Blog after pages with order 0-4.
### Scroll-to-top button
A scroll-to-top button appears after scrolling down. Configure in `src/components/Layout.tsx`:

View File

@@ -2,7 +2,7 @@
---
Type: page
Date: 2025-12-20
Date: 2025-12-21
---
This markdown site is open source and built to be extended. Here is what ships out of the box.

View File

@@ -397,7 +397,7 @@ When you fork this project, update these files with your site information:
| File | What to update |
| ----------------------------------- | ----------------------------------------------------------- |
| `src/pages/Home.tsx` | Site name, title, intro, bio, featured config, logo gallery |
| `src/config/siteConfig.ts` | Site name, title, intro, bio, blog page, logo gallery |
| `convex/http.ts` | `SITE_URL`, `SITE_NAME` (API responses, sitemap) |
| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` (RSS feeds) |
| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME`, `DEFAULT_OG_IMAGE` (OG tags) |
@@ -490,23 +490,27 @@ const DEFAULT_OG_IMAGE = "/images/og-default.svg";
### Update Site Configuration
Edit `src/pages/Home.tsx` to customize:
Edit `src/config/siteConfig.ts` to customize:
```typescript
const siteConfig = {
export default {
name: "Your Name",
title: "Your Title",
intro: "Your introduction...",
bio: "Your bio...",
// Blog page configuration
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
// Featured section options
featuredViewMode: "list", // 'list' or 'cards'
showViewToggle: true, // Let users switch between views
featuredItems: [
{ slug: "post-slug", type: "post" },
{ slug: "page-slug", type: "page" },
],
featuredEssays: [{ title: "Post Title", slug: "post-slug" }],
// Logo gallery (marquee scroll with clickable links)
logoGallery: {
@@ -618,6 +622,36 @@ Delete the sample files from `public/images/logos/` and clear the images array,
The gallery uses CSS animations for smooth infinite scrolling. Logos display in grayscale and colorize on hover.
### Blog page
The site supports a dedicated blog page at `/blog`. Configure in `src/config/siteConfig.ts`:
```typescript
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
```
| Option | Description |
| ------ | ----------- |
| `enabled` | Enable the `/blog` route |
| `showInNav` | Show Blog link in navigation |
| `title` | Text for nav link and page heading |
| `order` | Position in navigation (lower = first) |
| `displayOnHomepage` | Show post list on homepage |
**Display options:**
- Homepage only: `displayOnHomepage: true`, `blogPage.enabled: false`
- Blog page only: `displayOnHomepage: false`, `blogPage.enabled: true`
- Both: `displayOnHomepage: true`, `blogPage.enabled: true`
**Navigation order:** The Blog link merges with page links and sorts by order. Pages use the `order` field in frontmatter. Set `blogPage.order: 5` to position Blog after pages with order 0-4.
### Scroll-to-top button
A scroll-to-top button appears after scrolling down on posts and pages. Configure it in `src/components/Layout.tsx`:

View File

@@ -2,8 +2,10 @@ import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Post from "./pages/Post";
import Stats from "./pages/Stats";
import Blog from "./pages/Blog";
import Layout from "./components/Layout";
import { usePageTracking } from "./hooks/usePageTracking";
import siteConfig from "./config/siteConfig";
function App() {
// Track page views and active sessions
@@ -14,6 +16,11 @@ function App() {
<Routes>
<Route path="/" element={<Home />} />
<Route path="/stats" element={<Stats />} />
{/* Blog page route - only enabled when blogPage.enabled is true */}
{siteConfig.blogPage.enabled && (
<Route path="/blog" element={<Blog />} />
)}
{/* Catch-all for post/page slugs - must be last */}
<Route path="/:slug" element={<Post />} />
</Routes>
</Layout>
@@ -21,4 +28,3 @@ function App() {
}
export default App;

View File

@@ -7,6 +7,7 @@ import ThemeToggle from "./ThemeToggle";
import SearchModal from "./SearchModal";
import MobileMenu, { HamburgerButton } from "./MobileMenu";
import ScrollToTop, { ScrollToTopConfig } from "./ScrollToTop";
import siteConfig from "../config/siteConfig";
// Scroll-to-top configuration - enabled by default
// Customize threshold (pixels) to control when button appears
@@ -69,6 +70,48 @@ export default function Layout({ children }: LayoutProps) {
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isSearchOpen]);
// Check if Blog link should be shown in nav
const showBlogInNav =
siteConfig.blogPage.enabled && siteConfig.blogPage.showInNav;
// Combine Blog link with pages and sort by order
// This allows Blog to be positioned anywhere in the nav via siteConfig.blogPage.order
type NavItem = {
slug: string;
title: string;
order: number;
isBlog?: boolean;
};
const navItems: NavItem[] = [];
// Add Blog link if enabled
if (showBlogInNav) {
navItems.push({
slug: "blog",
title: siteConfig.blogPage.title,
order: siteConfig.blogPage.order ?? 0,
isBlog: true,
});
}
// Add pages from Convex
if (pages && pages.length > 0) {
pages.forEach((page) => {
navItems.push({
slug: page.slug,
title: page.title,
order: page.order ?? 999,
});
});
}
// Sort by order (lower numbers first), then alphabetically by title
navItems.sort((a, b) => {
if (a.order !== b.order) return a.order - b.order;
return a.title.localeCompare(b.title);
});
return (
<div className="layout">
{/* Top navigation bar with page links, search, and theme toggle */}
@@ -82,19 +125,19 @@ export default function Layout({ children }: LayoutProps) {
</div>
{/* Page navigation links (visible on desktop only) */}
{pages && pages.length > 0 && (
<nav className="page-nav desktop-only">
{pages.map((page) => (
<Link
key={page.slug}
to={`/${page.slug}`}
className="page-nav-link"
>
{page.title}
</Link>
))}
</nav>
)}
<nav className="page-nav desktop-only">
{/* Nav links sorted by order (Blog + pages combined) */}
{navItems.map((item) => (
<Link
key={item.slug}
to={`/${item.slug}`}
className="page-nav-link"
>
{item.title}
</Link>
))}
</nav>
{/* Search button with icon */}
<button
onClick={openSearch}
@@ -112,21 +155,19 @@ export default function Layout({ children }: LayoutProps) {
{/* Mobile menu drawer */}
<MobileMenu isOpen={isMobileMenuOpen} onClose={closeMobileMenu}>
{/* Page navigation links in mobile menu */}
{pages && pages.length > 0 && (
<nav className="mobile-nav-links">
{pages.map((page) => (
<Link
key={page.slug}
to={`/${page.slug}`}
className="mobile-nav-link"
onClick={closeMobileMenu}
>
{page.title}
</Link>
))}
</nav>
)}
{/* Page navigation links in mobile menu (same order as desktop) */}
<nav className="mobile-nav-links">
{navItems.map((item) => (
<Link
key={item.slug}
to={`/${item.slug}`}
className="mobile-nav-link"
onClick={closeMobileMenu}
>
{item.title}
</Link>
))}
</nav>
</MobileMenu>
<main className="main-content">{children}</main>

View File

@@ -94,15 +94,15 @@ export default function MobileMenu({
</svg>
</button>
{/* Menu content */}
<div className="mobile-menu-content">{children}</div>
{/* Home link at bottom */}
<div className="mobile-menu-footer">
{/* Home link at top */}
<div className="mobile-menu-header">
<Link to="/" className="mobile-menu-home-link" onClick={onClose}>
Home
</Link>
</div>
{/* Menu content */}
<div className="mobile-menu-content">{children}</div>
</div>
</>
);
@@ -141,4 +141,3 @@ export function HamburgerButton({ onClick, isOpen }: HamburgerButtonProps) {
</button>
);
}

129
src/config/siteConfig.ts Normal file
View File

@@ -0,0 +1,129 @@
import { ReactNode } from "react";
// Re-export types from LogoMarquee for convenience
export type { LogoItem, LogoGalleryConfig } from "../components/LogoMarquee";
import type { LogoGalleryConfig } from "../components/LogoMarquee";
// Blog page configuration
// Controls whether posts appear on homepage, dedicated blog page, or both
export interface BlogPageConfig {
enabled: boolean; // Enable the /blog route
showInNav: boolean; // Show "Blog" link in navigation
title: string; // Page title for the blog page
description?: string; // Optional description shown on blog page
order?: number; // Nav order (lower = first, matches page frontmatter order)
}
// Posts display configuration
// Controls where the post list appears
export interface PostsDisplayConfig {
showOnHome: boolean; // Show post list on homepage
showOnBlogPage: boolean; // Show post list on /blog page (requires blogPage.enabled)
}
// Site configuration interface
export interface SiteConfig {
// Basic site info
name: string;
title: string;
logo: string | null;
intro: ReactNode;
bio: string;
// Featured section configuration
featuredViewMode: "cards" | "list";
showViewToggle: boolean;
// Logo gallery configuration
logoGallery: LogoGalleryConfig;
// Blog page configuration
blogPage: BlogPageConfig;
// Posts display configuration
postsDisplay: PostsDisplayConfig;
// Links for footer section
links: {
docs: string;
convex: string;
netlify: string;
};
}
// Default site configuration
// Customize this for your site
export const siteConfig: SiteConfig = {
// Basic site info
name: 'markdown "sync" site',
title: "markdown sync site",
// Optional logo/header image (place in public/images/, set to null to hide)
logo: "/images/logo.svg",
intro: null, // Set in Home.tsx to allow JSX with links
bio: `Write locally, sync instantly with real-time updates. Powered by Convex and Netlify.`,
// Featured section configuration
// viewMode: 'list' shows bullet list, 'cards' shows card grid with excerpts
featuredViewMode: "cards",
// Allow users to toggle between list and card views
showViewToggle: true,
// Logo gallery configuration
// Set enabled to false to hide, or remove/replace sample images with your own
logoGallery: {
enabled: true,
images: [
{
src: "/images/logos/sample-logo-1.svg",
href: "https://markdowncms.netlify.app/",
},
{
src: "/images/logos/convex-wordmark-black.svg",
href: "/about#the-real-time-twist",
},
{
src: "/images/logos/sample-logo-3.svg",
href: "https://markdowncms.netlify.app/",
},
{
src: "/images/logos/sample-logo-4.svg",
href: "https://markdowncms.netlify.app/",
},
{
src: "/images/logos/sample-logo-5.svg",
href: "https://markdowncms.netlify.app/",
},
],
position: "above-footer",
speed: 30,
title: "Trusted by (sample logos)",
},
// Blog page configuration
// Set enabled to true to create a dedicated /blog page
blogPage: {
enabled: true, // Enable the /blog route
showInNav: true, // Show "Blog" link in navigation
title: "Blog", // Page title
description: "All posts from the blog, sorted by date.", // Optional description
order: 2, // Nav order (lower = first, e.g., 0 = first, 5 = after pages with order 0-4)
},
// Posts display configuration
// Controls where the post list appears
// Both can be true to show posts on homepage AND blog page
// Set showOnHome to false to only show posts on /blog page
postsDisplay: {
showOnHome: true, // Show post list on homepage
showOnBlogPage: true, // Show post list on /blog page
},
// Links for footer section
links: {
docs: "/setup-guide",
convex: "https://convex.dev",
netlify: "https://netlify.com",
},
};
// Export the config as default for easy importing
export default siteConfig;

58
src/pages/Blog.tsx Normal file
View File

@@ -0,0 +1,58 @@
import { useNavigate } from "react-router-dom";
import { useQuery } from "convex/react";
import { api } from "../../convex/_generated/api";
import PostList from "../components/PostList";
import siteConfig from "../config/siteConfig";
import { ArrowLeft } from "lucide-react";
// Blog page component
// Displays all published posts in a year-grouped list
// Controlled by siteConfig.blogPage and siteConfig.postsDisplay settings
export default function Blog() {
const navigate = useNavigate();
// Fetch published posts from Convex
const posts = useQuery(api.posts.getAllPosts);
// Check if posts should be shown on blog page
const showPosts = siteConfig.postsDisplay.showOnBlogPage;
return (
<div className="blog-page">
{/* Navigation with back button */}
<nav className="post-nav">
<button onClick={() => navigate("/")} className="back-button">
<ArrowLeft size={16} />
<span>Back</span>
</button>
</nav>
{/* Blog page header */}
<header className="blog-header">
<h1 className="blog-title">{siteConfig.blogPage.title}</h1>
{siteConfig.blogPage.description && (
<p className="blog-description">{siteConfig.blogPage.description}</p>
)}
</header>
{/* Blog posts section */}
{showPosts && (
<section className="blog-posts">
{posts === undefined ? null : posts.length === 0 ? (
<p className="no-posts">No posts yet. Check back soon!</p>
) : (
<PostList posts={posts} />
)}
</section>
)}
{/* Message when posts are disabled on blog page */}
{!showPosts && (
<p className="blog-disabled-message">
Posts are configured to not display on this page. Update{" "}
<code>postsDisplay.showOnBlogPage</code> in siteConfig to enable.
</p>
)}
</div>
);
}

View File

@@ -3,89 +3,18 @@ import { useQuery } from "convex/react";
import { api } from "../../convex/_generated/api";
import PostList from "../components/PostList";
import FeaturedCards from "../components/FeaturedCards";
import LogoMarquee, {
LogoGalleryConfig,
LogoItem,
} from "../components/LogoMarquee";
// Site configuration - customize this for your site
// All configurable options in one place for easy developer experience
const siteConfig = {
// Basic site info
name: 'markdown "sync" site',
title: "markdown sync site",
// Optional logo/header image (place in public/images/, set to null to hide)
logo: "/images/logo.svg" as string | null,
intro: (
<>
An open-source markdown sync site for developers and AI agents. Publish
from the terminal with npm run sync.{" "}
<a
href="https://github.com/waynesutton/markdown-site"
target="_blank"
rel="noopener noreferrer"
className="home-text-link"
>
Fork it
</a>
, customize it, ship it.
</>
),
bio: `Write locally, sync instantly with real-time updates. Powered by Convex and Netlify.`,
// Featured section configuration
// viewMode: 'list' shows bullet list, 'cards' shows card grid with excerpts
featuredViewMode: "cards" as "cards" | "list",
// Allow users to toggle between list and card views
showViewToggle: true,
// Logo gallery configuration
// Set enabled to false to hide, or remove/replace sample images with your own
logoGallery: {
enabled: true, // Set to false to hide the logo gallery
images: [
// Sample logos with links (replace with your own)
// Each logo can have: { src: "/images/logos/logo.svg", href: "https://example.com" }
{
src: "/images/logos/sample-logo-1.svg",
href: "https://markdowncms.netlify.app/",
},
{
src: "/images/logos/convex-wordmark-black.svg",
href: "/about#the-real-time-twist",
},
{
src: "/images/logos/sample-logo-3.svg",
href: "https://markdowncms.netlify.app/",
},
{
src: "/images/logos/sample-logo-4.svg",
href: "https://markdowncms.netlify.app/",
},
{
src: "/images/logos/sample-logo-5.svg",
href: "https://markdowncms.netlify.app/",
},
] as LogoItem[],
position: "above-footer", // 'above-footer' or 'below-featured'
speed: 30, // Seconds for one complete scroll cycle
title: "Trusted by (sample logos)", // Optional title above the marquee (set to undefined to hide)
} as LogoGalleryConfig,
// Links for footer section
links: {
docs: "/setup-guide",
convex: "https://convex.dev",
netlify: "https://netlify.com",
},
};
import LogoMarquee from "../components/LogoMarquee";
import siteConfig from "../config/siteConfig";
// Local storage key for view mode preference
const VIEW_MODE_KEY = "featured-view-mode";
export default function Home() {
// Fetch published posts from Convex
const posts = useQuery(api.posts.getAllPosts);
// Fetch published posts from Convex (only if showing on home)
const posts = useQuery(
api.posts.getAllPosts,
siteConfig.postsDisplay.showOnHome ? {} : "skip",
);
// Fetch featured posts and pages from Convex (for list view)
const featuredPosts = useQuery(api.posts.getFeaturedPosts);
@@ -145,6 +74,9 @@ export default function Home() {
const featuredList = getFeaturedList();
const hasFeaturedContent = featuredList.length > 0;
// Check if posts should be shown on homepage
const showPostsOnHome = siteConfig.postsDisplay.showOnHome;
return (
<div className="home">
{/* Header section with intro */}
@@ -159,7 +91,20 @@ export default function Home() {
)}
<h1 className="home-name">{siteConfig.name}</h1>
<p className="home-intro">{siteConfig.intro}</p>
{/* Intro with JSX support for links */}
<p className="home-intro">
An open-source markdown sync site for developers and AI agents.
Publish from the terminal with npm run sync.{" "}
<a
href="https://github.com/waynesutton/markdown-site"
target="_blank"
rel="noopener noreferrer"
className="home-text-link"
>
Fork it
</a>
, customize it, ship it.
</p>
<p className="home-bio">{siteConfig.bio}</p>
@@ -234,14 +179,16 @@ export default function Home() {
{/* Logo gallery (below-featured position) */}
{renderLogoGallery("below-featured")}
{/* Blog posts section - no loading state to avoid flash (Convex syncs instantly) */}
<section id="posts" className="home-posts">
{posts === undefined ? null : posts.length === 0 ? (
<p className="no-posts">No posts yet. Check back soon!</p>
) : (
<PostList posts={posts} />
)}
</section>
{/* Blog posts section - conditionally shown based on config */}
{showPostsOnHome && (
<section id="posts" className="home-posts">
{posts === undefined ? null : posts.length === 0 ? (
<p className="no-posts">No posts yet. Check back soon!</p>
) : (
<PostList posts={posts} />
)}
</section>
)}
{/* Logo gallery (above-footer position) */}
{renderLogoGallery("above-footer")}

View File

@@ -916,6 +916,52 @@ body {
text-decoration: underline;
}
/* ============================================
Blog Page Styles
Dedicated /blog page that shows all posts
============================================ */
.blog-page {
padding-top: 20px;
}
.blog-header {
margin-bottom: 40px;
}
.blog-title {
font-size: 2rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
.blog-description {
font-size: 1rem;
color: var(--text-secondary);
line-height: 1.6;
max-width: 600px;
}
.blog-posts {
margin-top: 20px;
}
.blog-disabled-message {
color: var(--text-muted);
font-size: 0.95rem;
padding: 20px;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px dashed var(--border-color);
}
.blog-disabled-message code {
background: var(--inline-code-bg);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.85em;
}
/* Responsive styles */
@media (max-width: 768px) {
.main-content {
@@ -941,6 +987,15 @@ body {
font-size: 28px;
}
/* Blog page responsive */
.blog-title {
font-size: 1.75rem;
}
.blog-header {
margin-bottom: 24px;
}
.post-link .post-title {
font-size: 18px;
font-weight: 400;
@@ -2041,7 +2096,13 @@ body {
background-color: var(--bg-hover);
}
/* Menu footer with home link */
/* Menu header with home link */
.mobile-menu-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
}
/* Menu footer (kept for backwards compatibility) */
.mobile-menu-footer {
padding: 16px 24px;
border-top: 1px solid var(--border-color);