+
+ About this sync
+
-
+
GitHub data is synced on the server, persisted as normalized rows, and served in
filtered slices so the frontend can render quickly without re-fetching the entire board
on every visit.
diff --git a/apps/site/components/layout/Footer.tsx b/apps/site/components/layout/Footer.tsx
index 8c7e2e5..37ac08a 100644
--- a/apps/site/components/layout/Footer.tsx
+++ b/apps/site/components/layout/Footer.tsx
@@ -8,9 +8,9 @@ const Footer = () => {
homeHref="/"
logoAlt="VeriWorkly Logo"
logoSrc="/veriworkly-logo.png"
+ email={siteConfig.email}
brandName={siteConfig.name}
shortName={siteConfig.shortName}
- email="hello@veriworkly.com"
location="Open Source Everywhere"
description="Empowering job seekers with most advanced, privacy-first, and open-source resume building experience. 100% free, forever."
socialLinks={[
diff --git a/apps/site/components/roadmap/KanbanBoard.tsx b/apps/site/components/roadmap/KanbanBoard.tsx
index 737cd8f..7686256 100644
--- a/apps/site/components/roadmap/KanbanBoard.tsx
+++ b/apps/site/components/roadmap/KanbanBoard.tsx
@@ -20,7 +20,6 @@ export interface KanbanItem {
export interface KanbanColumn {
title: string;
items: KanbanItem[];
- color?: "blue" | "amber" | "emerald" | "gray";
}
interface KanbanBoardProps {
@@ -32,13 +31,6 @@ interface KanbanBoardProps {
refreshHrefMap?: Partial>;
}
-const statusColorMap = {
- blue: "bg-blue-500/10 text-blue-700 dark:text-blue-400",
- amber: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
- emerald: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
- gray: "bg-gray-500/10 text-gray-700 dark:text-gray-400",
-};
-
const KanbanBoard = ({
columns,
showDescription = true,
@@ -51,15 +43,11 @@ const KanbanBoard = ({
return (
- {columns.map((column, idx) => {
- const columnColor = column.color || ["blue", "amber", "emerald"][idx];
- const colorClass = statusColorMap[columnColor as keyof typeof statusColorMap];
-
+ {columns.map((column) => {
return (
+
+
+ ),
+
+ "In Progress": (
+
+ ),
+
+ Done: (
+
+ ),
+};
+
const KanbanColumnView = ({
column,
- colorClass,
columnHref,
refreshHref,
singleStatusMode,
@@ -16,7 +44,6 @@ const KanbanColumnView = ({
showRoadmapLinks,
}: {
column: KanbanColumn;
- colorClass: string;
columnHref?: string;
refreshHref?: string;
singleStatusMode: boolean;
@@ -24,43 +51,60 @@ const KanbanColumnView = ({
showUrl: boolean;
showRoadmapLinks: boolean;
}) => {
+ const icon = columnIcons[column.title as keyof typeof columnIcons] || (
+
+ );
+
return (
-
-
- {columnHref ? (
+
+ {/* Column Header */}
+
+
+
+ {icon}
+ {columnHref ? (
+
+ {column.title}
+
+ ) : (
+
+ {column.title}
+
+ )}
+
+
+
+ {column.items.length}
+
+
+
+ {refreshHref && (
- {column.title}
+
+ Refresh
- ) : (
-
{column.title}
)}
-
-
- {column.items.length}
-
- {refreshHref && (
-
- Refresh {column.title}
-
- )}
-
{column.items.length === 0 ? (
-
-
No items yet
+
+
+ No items
+
+
+
Planned items will appear here.
) : (
column.items.map((item) => (
diff --git a/apps/site/components/roadmap/KanbanItemCard.tsx b/apps/site/components/roadmap/KanbanItemCard.tsx
index bc381de..00a3da1 100644
--- a/apps/site/components/roadmap/KanbanItemCard.tsx
+++ b/apps/site/components/roadmap/KanbanItemCard.tsx
@@ -1,11 +1,48 @@
import Link from "next/link";
+import { Calendar, ArrowUpRight } from "lucide-react";
import { cn } from "@/lib/utils";
-import { Card, Badge } from "@veriworkly/ui";
-
import { KanbanItem } from "./KanbanBoard";
-import { tagColorMap } from "./KanbanTagColor";
+
+const tagDotMap: Record
= {
+ core: "bg-purple-500",
+ ai: "bg-blue-500",
+ "ai-assisted": "bg-blue-500",
+ export: "bg-green-500",
+ import: "bg-pink-500",
+ integration: "bg-indigo-500",
+ templates: "bg-cyan-500",
+ privacy: "bg-red-500",
+ ux: "bg-yellow-500",
+ ui: "bg-yellow-500",
+ testing: "bg-orange-500",
+ cicd: "bg-teal-500",
+ ats: "bg-violet-500",
+ analysis: "bg-violet-500",
+ personalization: "bg-fuchsia-500",
+ format: "bg-lime-500",
+ i18n: "bg-rose-500",
+ content: "bg-amber-500",
+ research: "bg-slate-500",
+ design: "bg-pink-500",
+ done: "bg-emerald-500",
+ product: "bg-sky-500",
+ docs: "bg-lime-500",
+ platform: "bg-yellow-500",
+ api: "bg-cyan-500",
+ admin: "bg-rose-500",
+ editor: "bg-indigo-500",
+ blog: "bg-orange-500",
+ security: "bg-red-500",
+ auth: "bg-violet-500",
+ performance: "bg-amber-500",
+ billing: "bg-emerald-500",
+ database: "bg-pink-500",
+ seo: "bg-teal-500",
+ infrastructure: "bg-lime-500",
+ accessibility: "bg-fuchsia-500",
+};
const KanbanItemCard = ({
item,
@@ -18,53 +55,109 @@ const KanbanItemCard = ({
showUrl: boolean;
showRoadmapLinks: boolean;
}) => {
+ const isInteractive = (showUrl && item.url) || showRoadmapLinks;
+
const cardContent = (
-
-
-
-
{item.title}
-
- {item.eta &&
ETA: {item.eta}
}
-
- {!item.eta && item.status === "done" && (
-
- {item.completedQuarter
- ? `Shipped: ${item.completedQuarter}`
- : item.completedAt
- ? `Completed: ${new Date(item.completedAt).toLocaleDateString()}`
- : "Shipped"}
-
+ {isInteractive && (
+
+ )}
+
+
+
+
+ {item.title}
+
+
+ {isInteractive && (
+
)}
{showDescription && item.description && (
-
{item.description}
+
{item.description}
)}
- {item.tags && item.tags.length > 0 && (
-
- {item.tags.map((tag) => (
-
- {tag}
-
- ))}
-
+ {(item.eta || item.status === "done" || (item.tags && item.tags.length > 0)) && (
+
)}
+
+
+ {item.eta ? (
+
+
+
+
+ ETA: {item.eta}
+
+
+ ) : item.status === "done" ? (
+
+
+
+
+ {item.completedQuarter
+ ? `Shipped: ${item.completedQuarter}`
+ : item.completedAt
+ ? `Shipped: ${new Date(item.completedAt).toLocaleDateString("en-US", { month: "short", year: "numeric" })}`
+ : "Shipped"}
+
+
+ ) : (
+
+ )}
+
+ {item.tags && item.tags.length > 0 && (
+
+ {item.tags.slice(0, 2).map((tag) => {
+ const dotColor = tagDotMap[tag.toLowerCase()] || "bg-muted";
+ return (
+
+
+ {tag}
+
+ );
+ })}
+
+ {item.tags.length > 2 && (
+
+ +{item.tags.length - 2}
+
+ )}
+
+ )}
+
-
+
);
- if (showRoadmapLinks) {
- return
{cardContent};
- }
+ if (showRoadmapLinks) return
{cardContent};
if (showUrl && item.url) {
return (
diff --git a/apps/site/config/site.ts b/apps/site/config/site.ts
index 82ca037..0ef2145 100644
--- a/apps/site/config/site.ts
+++ b/apps/site/config/site.ts
@@ -5,6 +5,7 @@ export const siteConfig = {
shortName: "VeriWorkly",
creator: "Gautam Raj",
+ email: "info@veriworkly.com",
url: process.env.SITE_URL || "https://veriworkly.com",
diff --git a/apps/site/features/landing/components/BenefitsSection.tsx b/apps/site/features/landing/components/BenefitsSection.tsx
index 1dd8023..bed3492 100644
--- a/apps/site/features/landing/components/BenefitsSection.tsx
+++ b/apps/site/features/landing/components/BenefitsSection.tsx
@@ -1,42 +1,43 @@
import { Card } from "@veriworkly/ui";
+import { Zap, Shield, Smartphone, Paintbrush, Layers, RefreshCw } from "lucide-react";
const BenefitsSection = () => {
const benefits = [
{
- icon: "⚡",
+ icon: Zap,
title: "Lightning Fast",
description: "Create professional resumes in seconds with a fast, modern builder.",
},
{
- icon: "🔒",
+ icon: Shield,
title: "100% Private",
description:
"Your resume data stays local-first by default. No mandatory account and no data selling.",
},
{
- icon: "📱",
+ icon: Smartphone,
title: "Works Everywhere",
description:
"Desktop, tablet, or mobile - your resume editing experience is seamless across all devices.",
},
{
- icon: "🎨",
+ icon: Paintbrush,
title: "Fully Customizable",
description: "Complete control over colors, fonts, spacing, and layout. Make it truly yours.",
},
{
- icon: "📊",
+ icon: Layers,
title: "Multiple Sections",
description:
"Add languages, interests, awards, certificates, publications, volunteer work, and more.",
},
{
- icon: "🔄",
+ icon: RefreshCw,
title: "Smart Sync",
description:
"Update your master profile once and sync across all your resumes automatically.",
@@ -60,13 +61,21 @@ const BenefitsSection = () => {
- {benefits.map((benefit) => (
-
- {benefit.icon}
- {benefit.title}
- {benefit.description}
-
- ))}
+ {benefits.map((benefit) => {
+ const Icon = benefit.icon;
+
+ return (
+
+
+
+
+
+ {benefit.title}
+
+ {benefit.description}
+
+ );
+ })}
);
diff --git a/apps/site/features/landing/components/BlogSection.tsx b/apps/site/features/landing/components/BlogSection.tsx
index 76d4d18..22ed806 100644
--- a/apps/site/features/landing/components/BlogSection.tsx
+++ b/apps/site/features/landing/components/BlogSection.tsx
@@ -2,37 +2,10 @@ import Link from "next/link";
import { ArrowRight, Clock, Calendar } from "lucide-react";
import { Card } from "@veriworkly/ui";
-
-const latestPosts = [
- {
- title: "Building a Scalable Resume Platform: The VeriWorkly Multi-App Architecture",
- description:
- "Why we run separate apps for builder, docs, blog, and backend, and how that keeps development and deployments reliable.",
- href: "https://blog.veriworkly.com/building-scalable-resume-platform-multi-app-architecture",
- date: "April 28, 2026",
- readTime: "5 min read",
- },
-
- {
- title: "Mastering ATS-Friendly Resumes in 2026",
- description:
- "A deep dive into how modern Applicant Tracking Systems parse resumes and why structure matters more than visual design.",
- href: "https://blog.veriworkly.com/mastering-ats-friendly-resumes-2026",
- date: "April 27, 2026",
- readTime: "6 min read",
- },
-
- {
- title: "Why Privacy is the Future of Resume Technology",
- description:
- "Exploring data ownership in modern resume tools and why VeriWorkly is built on a local-first architecture.",
- href: "https://blog.veriworkly.com/privacy-future-local-first-resume-builder",
- date: "April 26, 2026",
- readTime: "6 min read",
- },
-];
+import { getLatestBlogPosts } from "@/lib/blog-reader";
const BlogSection = () => {
+ const latestPosts = getLatestBlogPosts();
return (
diff --git a/apps/site/lib/blog-reader.ts b/apps/site/lib/blog-reader.ts
new file mode 100644
index 0000000..b49d87a
--- /dev/null
+++ b/apps/site/lib/blog-reader.ts
@@ -0,0 +1,80 @@
+import fs from "fs";
+import path from "path";
+
+export interface BlogPostFeedItem {
+ title: string;
+ description: string;
+ href: string;
+ date: string;
+ readTime: string;
+}
+
+export function getLatestBlogPosts(): BlogPostFeedItem[] {
+ try {
+ const blogDir = path.join(process.cwd(), "..", "blog-platform", "content", "blog");
+
+ if (!fs.existsSync(blogDir)) {
+ console.warn("Blog directory does not exist at:", blogDir);
+ return [];
+ }
+
+ const files = fs.readdirSync(blogDir);
+ const posts: BlogPostFeedItem[] = [];
+
+ for (const filename of files) {
+ if (!filename.endsWith(".mdx")) continue;
+
+ const fullPath = path.join(blogDir, filename);
+ const content = fs.readFileSync(fullPath, "utf-8");
+
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
+ if (!match) continue;
+
+ const frontmatterText = match[1];
+ const metadata: Record = {};
+
+ frontmatterText.split("\n").forEach((line) => {
+ const separatorIndex = line.indexOf(":");
+
+ if (separatorIndex !== -1) {
+ const key = line.slice(0, separatorIndex).trim();
+ const value = line
+ .slice(separatorIndex + 1)
+ .trim()
+ .replace(/^['"]|['"]$/g, "");
+ metadata[key] = value;
+ }
+ });
+
+ const bodyContent = content.replace(/^---\r?\n[\s\S]*?\r?\n---/, "");
+ const words = bodyContent.trim().split(/\s+/).length;
+ const minutes = Math.ceil(words / 200);
+
+ const slug = filename.replace(/\.mdx$/, "");
+ const rawDate = metadata.date || "";
+
+ const formattedDate = rawDate
+ ? new Date(rawDate).toLocaleDateString("en-US", {
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ })
+ : "";
+
+ posts.push({
+ title: metadata.title || "Untitled Post",
+ description: metadata.description || "",
+ href: `https://blog.veriworkly.com/${slug}`,
+ date: formattedDate,
+ readTime: `${minutes} min read`,
+ });
+ }
+
+ return posts
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
+ .slice(0, 3);
+ } catch (error) {
+ console.error("Failed to read latest blog posts dynamically:", error);
+ return [];
+ }
+}