mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
fix: use frontmatter image for OG meta instead of default
This commit is contained in:
@@ -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 I’m asking? Never assume anything on your own, if anything isn’t 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
3
.gitignore
vendored
@@ -30,3 +30,6 @@ dist-ssr
|
||||
|
||||
# Cursor rules
|
||||
.cursor/rules/write.mdc
|
||||
|
||||
# PRD files
|
||||
prds/metadataforsubs.md
|
||||
|
||||
11
AGENTS.md
11
AGENTS.md
@@ -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: {
|
||||
|
||||
33
README.md
33
README.md
@@ -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
20
TASK.md
@@ -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
|
||||
|
||||
|
||||
56
changelog.md
56
changelog.md
@@ -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
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: "Contact"
|
||||
slug: "contact"
|
||||
published: true
|
||||
order: 3
|
||||
order: 4
|
||||
---
|
||||
|
||||
You found the contact page. Nice
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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.
|
||||
|
||||
102
convex/http.ts
102
convex/http.ts
@@ -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 });
|
||||
}
|
||||
|
||||
9
files.md
9
files.md
@@ -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 |
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
---
|
||||
Type: page
|
||||
Date: 2025-12-20
|
||||
Date: 2025-12-21
|
||||
---
|
||||
|
||||
You found the contact page. Nice
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
129
src/config/siteConfig.ts
Normal 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
58
src/pages/Blog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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")}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user