diff --git a/apps/site/app/sitemap.ts b/apps/site/app/sitemap.ts index 73305cc..789502c 100644 --- a/apps/site/app/sitemap.ts +++ b/apps/site/app/sitemap.ts @@ -1,6 +1,7 @@ import type { MetadataRoute } from "next"; import { siteConfig } from "@/config/site"; +import { documentTypeSummaries, templateSummaries } from "@/config/templates"; export const revalidate = 86400; @@ -83,8 +84,21 @@ const publicRoutes = [ export default function sitemap(): MetadataRoute.Sitemap { const lastModified = new Date(); + const templateRoutes = documentTypeSummaries + .filter((docType) => docType.status === "available") + .map((docType) => ({ + url: `${siteConfig.url}${docType.href}`, + changeFrequency: "weekly" as const, + priority: 0.85, + })); - return publicRoutes.map((route) => ({ + const templateDetailRoutes = templateSummaries.map((template) => ({ + url: `${siteConfig.url}/templates/${template.documentType}/${template.id}`, + changeFrequency: "monthly" as const, + priority: 0.75, + })); + + return [...publicRoutes, ...templateRoutes, ...templateDetailRoutes].map((route) => ({ ...route, lastModified, })); diff --git a/apps/site/app/templates/[docType]/[templateId]/page.tsx b/apps/site/app/templates/[docType]/[templateId]/page.tsx new file mode 100644 index 0000000..53fa9a8 --- /dev/null +++ b/apps/site/app/templates/[docType]/[templateId]/page.tsx @@ -0,0 +1,236 @@ +import Link from "next/link"; +import Image from "next/image"; +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { ArrowRight, CheckCircle2 } from "lucide-react"; + +import { + templateSummaries, + getDocumentTypeSummary, + getTemplateByDocumentTypeAndId, +} from "@/config/templates"; +import { siteConfig } from "@/config/site"; + +import { Button, Container } from "@veriworkly/ui"; + +import { buildEditorUrl } from "../../components/utils"; +import { TemplateDetailHeader } from "../../components/TemplateHeader"; + +type PageProps = { + params: Promise<{ docType: string; templateId: string }>; +}; + +export async function generateMetadata({ params }: PageProps): Promise { + const { docType, templateId } = await params; + + const template = getTemplateByDocumentTypeAndId(docType, templateId); + + if (!template) { + return { + title: "Template Not Found | VeriWorkly", + }; + } + + return { + title: template.seo.title, + description: template.seo.description, + alternates: { + canonical: `${siteConfig.url}/templates/${docType}/${templateId}`, + }, + openGraph: { + title: template.seo.title, + description: template.seo.description, + url: `${siteConfig.url}/templates/${docType}/${templateId}`, + siteName: siteConfig.name, + images: [ + { + url: template.previewImage, + width: 1200, + height: 1600, + alt: `${template.name} ${template.documentTypeLabel.toLowerCase()} template preview`, + }, + ], + type: "website", + }, + }; +} + +export function generateStaticParams() { + return templateSummaries.map((template) => ({ + docType: template.documentType, + templateId: template.id, + })); +} + +const TemplateDetailPage = async ({ params }: PageProps) => { + const { docType, templateId } = await params; + + const docTypeData = getDocumentTypeSummary(docType); + const template = getTemplateByDocumentTypeAndId(docType, templateId); + + if (!docTypeData || !template) notFound(); + + const editorUrl = buildEditorUrl(template); + + return ( + + + +
+
+
+
+

Full document preview

+

Rendered from the production template asset

+
+ +
+ +
+ +
+ + +
+ +
+ {template.bestFor.map((item) => ( +
+
+ ))} +
+ +
+
+
+

+ Template system +

+ +

+ The layout is not just a skin. These choices decide what recruiters see first and how + the exported document scans. +

+
+
+ +
+ {template.typography.map((choice) => ( +
+

+ Typography +

+ +

{choice}

+
+ ))} +
+
+ +
+

+ Structure walkthrough +

+ +
+ {template.structure.map((section, index) => ( +
+
+ {(index + 1).toString().padStart(2, "0")} +
+ +
+
+

{section.title}

+

{section.description}

+
+ +
+ {section.items.map((item) => ( + + {item} + + ))} +
+
+
+ ))} +
+
+
+ ); +}; + +export default TemplateDetailPage; diff --git a/apps/site/app/templates/[docType]/page.tsx b/apps/site/app/templates/[docType]/page.tsx new file mode 100644 index 0000000..e0900f7 --- /dev/null +++ b/apps/site/app/templates/[docType]/page.tsx @@ -0,0 +1,166 @@ +import Link from "next/link"; +import type { Metadata } from "next"; +import { notFound, permanentRedirect } from "next/navigation"; + +import { + documentTypeSummaries, + getTemplateByLegacyId, + getDocumentTypeSummary, + getTemplatesByDocumentType, +} from "@/config/templates"; +import { siteConfig } from "@/config/site"; + +import { Container } from "@veriworkly/ui"; + +import EmptyState from "../components/EmptyState"; +import TemplateGroup from "../components/TemplateGroup"; +import TemplatesHeader from "../components/TemplatesHeader"; +import { getSingleParam, getTemplateHref } from "../components/utils"; + +type PageProps = { + params: Promise<{ docType: string }>; + searchParams?: Promise<{ + family?: string; + layout?: string; + }>; +}; + +export async function generateMetadata({ params }: PageProps): Promise { + const { docType } = await params; + + const docTypeData = getDocumentTypeSummary(docType); + + if (!docTypeData || docTypeData.status !== "available") { + return { + title: "Templates Not Found | VeriWorkly", + }; + } + + return { + title: docTypeData.seoTitle, + description: docTypeData.seoDescription, + alternates: { + canonical: `${siteConfig.url}/templates/${docType}`, + }, + openGraph: { + title: docTypeData.seoTitle, + description: docTypeData.seoDescription, + url: `${siteConfig.url}/templates/${docType}`, + siteName: siteConfig.name, + type: "website", + }, + }; +} + +export function generateStaticParams() { + return documentTypeSummaries + .filter((docType) => docType.status === "available") + .map((docType) => ({ docType: docType.id })); +} + +const TemplatesByDocumentTypePage = async ({ params, searchParams }: PageProps) => { + const [{ docType }, resolvedSearchParams] = await Promise.all([params, searchParams]); + + const legacyTemplate = getTemplateByLegacyId(docType); + if (legacyTemplate) permanentRedirect(getTemplateHref(legacyTemplate)); + + const docTypeData = getDocumentTypeSummary(docType); + if (!docTypeData || docTypeData.status !== "available") notFound(); + + const templates = getTemplatesByDocumentType(docType); + const selectedFamily = getSingleParam(resolvedSearchParams?.family, "All"); + const selectedLayout = getSingleParam(resolvedSearchParams?.layout, "All"); + + const visibleTemplates = templates.filter((template) => { + const familyMatch = selectedFamily === "All" || template.family === selectedFamily; + const layoutMatch = selectedLayout === "All" || template.layout === selectedLayout; + + return familyMatch && layoutMatch; + }); + + const familyGroups = Array.from(new Set(templates.map((template) => template.family))).map( + (family) => ({ + title: family, + description: + family === "Compact Core" + ? "High-density layouts for applications where parsing, keywords, and page control matter." + : family === "Modern Core" + ? "Polished application layouts with contemporary spacing and calm hierarchy." + : family === "Classic Letter" + ? "Formal letter systems for conservative, high-trust application moments." + : "Distinctive letter systems for modern applicants who still need a credible PDF.", + items: visibleTemplates.filter((template) => template.family === family), + }), + ); + + return ( + + + +
+
+ {templates.map((template) => ( + +
+
+

+ {template.family} +

+ +

+ {template.name} +

+ +

+ {template.shortDescription} +

+
+ +
+ +
+ {[template.layout, ...template.audience.slice(0, 2)].map((item) => ( + + {item} + + ))} +
+ + ))} +
+
+ + {visibleTemplates.length ? ( +
+ {familyGroups.map( + (group) => group.items.length > 0 && , + )} +
+ ) : ( + + )} +
+ ); +}; + +export default TemplatesByDocumentTypePage; diff --git a/apps/site/app/templates/[template]/error.tsx b/apps/site/app/templates/[template]/error.tsx deleted file mode 100644 index a3f0c6d..0000000 --- a/apps/site/app/templates/[template]/error.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import Link from "next/link"; - -import { Button } from "@veriworkly/ui"; - -export default function TemplateError({ - reset, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { - return ( -
-
-

- Preview Error -

- -

- Something went wrong -

- -

- We encountered an error while trying to load the template preview. -

-
- -
-
-
- - - -
-

Loading Failed

-

- This might be due to a network error or a temporary service disruption. -

-
- -
- - - -
-
-
- ); -} diff --git a/apps/site/app/templates/[template]/loading.tsx b/apps/site/app/templates/[template]/loading.tsx deleted file mode 100644 index cb17cf2..0000000 --- a/apps/site/app/templates/[template]/loading.tsx +++ /dev/null @@ -1,23 +0,0 @@ -export default function TemplatePreviewLoading() { - return ( -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- ); -} diff --git a/apps/site/app/templates/[template]/not-found.tsx b/apps/site/app/templates/[template]/not-found.tsx deleted file mode 100644 index 5879ef0..0000000 --- a/apps/site/app/templates/[template]/not-found.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import Link from "next/link"; - -import { Button } from "@veriworkly/ui"; -import { siteConfig } from "@/config/site"; - -const TemplateNotFound = () => { - return ( -
-
-

- Template Preview -

- -

- Template not found -

- -

- The template you requested does not exist or has been removed from our collection. -

-
- -
-
-
- - - -
- -

Missing Layout

- -

- We couldn't locate this specific resume format. Try browsing our other modern - templates. -

-
- -
- - - -
-
-
- ); -}; - -export default TemplateNotFound; diff --git a/apps/site/app/templates/[template]/page.tsx b/apps/site/app/templates/[template]/page.tsx deleted file mode 100644 index 3bd2588..0000000 --- a/apps/site/app/templates/[template]/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import Link from "next/link"; -import Image from "next/image"; -import { Metadata } from "next"; -import { notFound } from "next/navigation"; - -import { getTemplateById, templateSummaries } from "@/config/templates"; - -import { Card, Button, Container } from "@veriworkly/ui"; - -import { buildEditorUrl } from "../components/utils"; -import { TemplateDetailHeader } from "../components/TemplateHeader"; - -interface Props { - params: Promise<{ template: string }>; -} - -export async function generateMetadata({ params }: Props): Promise { - const { template } = await params; - - const data = getTemplateById(template); - - if (!data) return { title: "Template Not Found" }; - - return { - title: `${data.name} Template | VeriWorkly`, - description: data.description, - }; -} - -export function generateStaticParams() { - return templateSummaries.map((t) => ({ template: t.id })); -} - -export default async function TemplatePreviewPage({ params }: Props) { - const { template } = await params; - - const templateDefinition = getTemplateById(template); - - if (!templateDefinition) { - notFound(); - } - - const editorUrl = buildEditorUrl(templateDefinition.id, templateDefinition.documentType); - - return ( - - - -
- -
- {templateDefinition.previewImage ? ( - {`${templateDefinition.name} - ) : ( -
- Preview not available -
- )} - -
- -
-
-
- -
- -
-
-
- ); -} diff --git a/apps/site/app/templates/components/EmptyState.tsx b/apps/site/app/templates/components/EmptyState.tsx index f860e27..17124a2 100644 --- a/apps/site/app/templates/components/EmptyState.tsx +++ b/apps/site/app/templates/components/EmptyState.tsx @@ -1,13 +1,13 @@ import Link from "next/link"; -const EmptyState = () => { +const EmptyState = ({ resetHref = "/templates" }: { resetHref?: string }) => { return (

No templates match these filters.

Try switching family or layout to see more options.

- + Reset filters
diff --git a/apps/site/app/templates/components/TemplateCard.tsx b/apps/site/app/templates/components/TemplateCard.tsx index 8c16570..be39b61 100644 --- a/apps/site/app/templates/components/TemplateCard.tsx +++ b/apps/site/app/templates/components/TemplateCard.tsx @@ -1,110 +1,127 @@ -import Link from "next/link"; import Image from "next/image"; +import Link from "next/link"; +import { ArrowRight, CheckCircle2, FileText } from "lucide-react"; + +import type { TemplateSummary } from "@/config/templates"; + +import { Badge } from "@veriworkly/ui"; -import { Card, Badge } from "@veriworkly/ui"; - -interface TemplateCardProps { - template: { - id: string; - name: string; - description: string; - accentColor: string; - previewImage: string; - family: string; - layout: string; - tags: string[]; - }; -} +import { getTemplateHref } from "./utils"; + +type TemplateCardProps = { + template: TemplateSummary; +}; const TemplateCard = ({ template }: TemplateCardProps) => { - const topTags = template.tags.filter((tag) => tag !== "One column" && tag !== "Two column"); + const previewAlt = `${template.name} ${template.documentTypeLabel.toLowerCase()} template preview`; return ( - -
-
-
- -
- {template.previewImage ? ( - - ) : null} - -
- -

- Quick Preview -

-
+
+ + -
-
- {template.previewImage ? ( - + {previewAlt} +
+ +
+ + +
+
+
+
); }; diff --git a/apps/site/app/templates/components/TemplateFilters.tsx b/apps/site/app/templates/components/TemplateFilters.tsx index 51cda09..49a43f7 100644 --- a/apps/site/app/templates/components/TemplateFilters.tsx +++ b/apps/site/app/templates/components/TemplateFilters.tsx @@ -1,18 +1,29 @@ import Link from "next/link"; -import { hrefWithFilters } from "./utils"; +import type { TemplateSummary } from "@/config/templates"; -const familyOptions = ["All", "Modern Core", "Compact Core"]; -const layoutOptions = ["All", "One column", "Two column", "Other"]; +import { hrefWithFilters } from "./utils"; type Props = { + docType: string; + templates: TemplateSummary[]; selectedFamily: string; selectedLayout: string; }; -const TemplateFilters = ({ selectedFamily, selectedLayout }: Props) => { +function unique(values: string[]) { + return ["All", ...Array.from(new Set(values))]; +} + +const TemplateFilters = ({ docType, templates, selectedFamily, selectedLayout }: Props) => { + const familyOptions = unique(templates.map((template) => template.family)); + const layoutOptions = unique(templates.map((template) => template.layout)); + return ( -
+
{familyOptions.map((family) => { const active = selectedFamily === family; @@ -20,12 +31,12 @@ const TemplateFilters = ({ selectedFamily, selectedLayout }: Props) => { return ( {family} @@ -41,12 +52,12 @@ const TemplateFilters = ({ selectedFamily, selectedLayout }: Props) => { return ( {layout} diff --git a/apps/site/app/templates/components/TemplateGroup.tsx b/apps/site/app/templates/components/TemplateGroup.tsx index 78303bf..056aad8 100644 --- a/apps/site/app/templates/components/TemplateGroup.tsx +++ b/apps/site/app/templates/components/TemplateGroup.tsx @@ -1,29 +1,30 @@ -import TemplateCard from "../components/TemplateCard"; +import type { TemplateSummary } from "@/config/templates"; -interface TemplateItem { - id: string; - name: string; - description: string; - accentColor: string; - previewImage: string; - family: string; - layout: string; - tags: string[]; -} +import TemplateCard from "./TemplateCard"; -interface TemplateGroupProps { +type TemplateGroupProps = { group: { title: string; - items: TemplateItem[]; + description: string; + items: TemplateSummary[]; }; -} +}; const TemplateGroup = ({ group }: TemplateGroupProps) => { return ( -
-

{group.title}

+
+
+
+

{group.title}

+

{group.description}

+
+ +

+ {group.items.length} available +

+
-
+
{group.items.map((template) => ( ))} diff --git a/apps/site/app/templates/components/TemplateHeader.tsx b/apps/site/app/templates/components/TemplateHeader.tsx index 86b7bb9..bb54628 100644 --- a/apps/site/app/templates/components/TemplateHeader.tsx +++ b/apps/site/app/templates/components/TemplateHeader.tsx @@ -1,41 +1,67 @@ import Link from "next/link"; +import { ArrowLeft, ArrowRight } from "lucide-react"; -import { Badge } from "@veriworkly/ui"; +import type { TemplateSummary } from "@/config/templates"; -import { TemplateDefinition } from "@/types/template"; +import { Badge, Button } from "@veriworkly/ui"; import { buildEditorUrl } from "./utils"; -export function TemplateDetailHeader({ template }: { template: TemplateDefinition }) { - const editorUrl = buildEditorUrl(template.id, template.documentType); +export function TemplateDetailHeader({ template }: { template: TemplateSummary }) { + const editorUrl = buildEditorUrl(template); return ( -
-
-

- Template Detail -

+
+
+ +
- + +
); } diff --git a/apps/site/app/templates/components/TemplatesHeader.tsx b/apps/site/app/templates/components/TemplatesHeader.tsx index cc88e72..9d6de08 100644 --- a/apps/site/app/templates/components/TemplatesHeader.tsx +++ b/apps/site/app/templates/components/TemplatesHeader.tsx @@ -1,27 +1,81 @@ +import { Badge } from "@veriworkly/ui"; +import type { DocumentTypeSummary, TemplateSummary } from "@/config/templates"; + import TemplateFilters from "./TemplateFilters"; type Props = { + docType: DocumentTypeSummary; + templates: TemplateSummary[]; selectedFamily: string; selectedLayout: string; }; -const TemplatesHeader = ({ selectedFamily, selectedLayout }: Props) => { +const TemplatesHeader = ({ docType, templates, selectedFamily, selectedLayout }: Props) => { + const layouts = Array.from(new Set(templates.map((template) => template.layout))); + const families = Array.from(new Set(templates.map((template) => template.family))); + return ( -
-

- Template Gallery -

+
+
+
+
+ {templates.length} live templates + {layouts.join(" + ")} +
+ +
+

+ Choose by job-to-be-done, then by taste. +

+ +

+ {docType.description} Each option below is shown as a working document, with enough + context to decide before opening the editor. +

+
+
+ +
+
+
+

{templates.length}

+

ready to use

+
+ +
+

{families.length}

+

style systems

+
+ +
+

{layouts.length}

+

layout modes

+
+
-

- Free Resume Templates (ATS-Friendly & Modern) -

+
+

Optimized for

-

- Browse our collection of free resume templates designed to pass ATS systems. Choose from - modern, professional, and simple layouts - no login required. -

+
+ {docType.highlights.map((highlight) => ( + + {highlight} + + ))} +
+
+
+
- +
); }; diff --git a/apps/site/app/templates/components/utils.ts b/apps/site/app/templates/components/utils.ts index 653fc54..6f16eeb 100644 --- a/apps/site/app/templates/components/utils.ts +++ b/apps/site/app/templates/components/utils.ts @@ -1,32 +1,36 @@ -import { siteConfig } from "@/config/site"; +import type { TemplateSummary } from "@/config/templates"; -export const familyByTemplateId: Record = { - "executive-clarity": "Modern Core", - "precision-ats": "Compact Core", -}; +import { siteConfig } from "@/config/site"; export function getSingleParam(value: string | string[] | undefined, fallback: string) { if (!value) return fallback; return Array.isArray(value) ? (value[0] ?? fallback) : value; } -export function getLayout(tags: string[]) { - if (tags.includes("Two column")) return "Two column"; - if (tags.includes("One column")) return "One column"; - return "Other"; +export function getTemplateHref(template: Pick) { + return `/templates/${template.documentType}/${template.id}`; } -export function hrefWithFilters(family: string, layout: string) { +export function hrefWithFilters(docType: string, family: string, layout: string) { const params = new URLSearchParams(); if (family !== "All") params.set("family", family); if (layout !== "All") params.set("layout", layout); - return params.toString() ? `/templates?${params.toString()}` : "/templates"; + return params.toString() ? `/templates/${docType}?${params.toString()}` : `/templates/${docType}`; } -export function buildEditorUrl(templateId: string, documentType: string): string { +export function buildEditorUrl( + template: Pick, +) { const base = siteConfig.links.app; - return `${base}/editor?template=${encodeURIComponent(templateId)}&type=${encodeURIComponent(documentType)}`; + return `${base}/editor?template=${encodeURIComponent(template.editorTemplateId)}&type=${encodeURIComponent(template.documentType)}`; +} + +export function toTitle(value: string) { + return value + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); } diff --git a/apps/site/app/templates/page.tsx b/apps/site/app/templates/page.tsx index cb76c69..1329640 100644 --- a/apps/site/app/templates/page.tsx +++ b/apps/site/app/templates/page.tsx @@ -1,35 +1,28 @@ +import Link from "next/link"; +import Image from "next/image"; import type { Metadata } from "next"; +import { ArrowRight, Clock, FileText, Layers } from "lucide-react"; -import { siteConfig } from "@/config/site"; -import { templateSummaries } from "@/config/templates"; - -import { Container } from "@veriworkly/ui"; - -import EmptyState from "./components/EmptyState"; -import TemplateGroup from "./components/TemplateGroup"; -import TemplatesHeader from "./components/TemplatesHeader"; -import TemplatesSEOContent from "./components/TemplatesSEOContent"; +import { Badge, Container } from "@veriworkly/ui"; -import { getLayout, getSingleParam, familyByTemplateId } from "./components/utils"; +import { siteConfig } from "@/config/site"; +import { documentTypeSummaries, templateSummaries } from "@/config/templates"; export const metadata: Metadata = { - title: `Free Resume Templates (ATS Friendly) `, - + title: "Document Template Directory | VeriWorkly", description: - "Browse free ATS-friendly resume templates. No login required. Choose from modern, professional, and simple resume designs and build your resume instantly.", - + "Explore VeriWorkly templates by document type, including resumes and cover letters with production-ready previews and editor-ready layouts.", keywords: [ + "document templates", "resume templates", - "free resume templates", + "cover letter templates", "ATS resume templates", - "professional resume formats", - "modern resume templates", - "resume builder templates", + "professional document templates", ], - openGraph: { - title: "Free Resume Templates (ATS Friendly) | VeriWorkly", - description: "Explore modern and ATS-friendly resume templates. 100% free, no signup required.", + title: "Document Template Directory | VeriWorkly", + description: + "Browse resume and cover letter templates by document type, compare design intent, and start from the right layout.", url: `${siteConfig.url}/templates`, siteName: siteConfig.name, images: [ @@ -37,80 +30,178 @@ export const metadata: Metadata = { url: "/og/template-page-og.png", width: 1200, height: 630, - alt: "Resume Templates Gallery - VeriWorkly", + alt: "VeriWorkly template directory", }, ], type: "website", }, - twitter: { card: "summary_large_image", - title: "Free Resume Templates | VeriWorkly", - description: "Modern, ATS-friendly resume templates. Free & no login required.", + title: "Document Template Directory | VeriWorkly", + description: "Resume and cover letter templates, organized by document type.", images: ["/og/template-page-og.png"], }, - alternates: { canonical: `${siteConfig.url}/templates`, }, }; -type PageProps = { - searchParams?: Promise<{ - family?: string; - layout?: string; - }>; -}; - -const TemplatesPage = async ({ searchParams }: PageProps) => { - const resolvedSearchParams = await searchParams; - - const selectedFamily = getSingleParam(resolvedSearchParams?.family, "All"); - const selectedLayout = getSingleParam(resolvedSearchParams?.layout, "All"); - - const enrichedTemplates = templateSummaries.map((template) => ({ - ...template, - family: familyByTemplateId[template.id] ?? "Compact Core", - layout: getLayout(template.tags), - })); - - const visibleTemplates = enrichedTemplates.filter((template) => { - const familyMatch = selectedFamily === "All" || template.family === selectedFamily; - const layoutMatch = selectedLayout === "All" || template.layout === selectedLayout; +const TemplatesPortalPage = () => { + const availableDocTypes = documentTypeSummaries.filter( + (docType) => docType.status === "available", + ); + const plannedDocTypes = documentTypeSummaries.filter((docType) => docType.status === "planned"); - return familyMatch && layoutMatch; - }); + return ( + +
+
+
+ Document-first templates + {templateSummaries.length} live layouts +
- const templateGroups = [ - { - title: "Modern Core", - items: visibleTemplates.filter((t) => t.family === "Modern Core"), - }, - { - title: "Compact Core", - items: visibleTemplates.filter((t) => t.family === "Compact Core"), - }, - ]; +
+

+ Pick the artifact. The right layout follows. +

- return ( - <> - - - - {visibleTemplates.length ? ( -
- {templateGroups.map( - (group) => group.items.length && , - )} +

+ A resume and a cover letter do different jobs. This directory separates them from the + start, then shows real document previews, fit signals, and the exact editor path for + each template. +

+
+
+ +
+
+
+

{availableDocTypes.length}

+

live types

+
+ +
+

{templateSummaries.length}

+

templates

+
+ +
+

{plannedDocTypes.length}

+

coming soon

+
- ) : ( - - )} - - - +
+

Designed for choosing fast

+

+ Compare the document type first, then inspect individual layouts only when the fit is + obvious. +

+
+
+
+ +
+ {availableDocTypes.map((docType) => { + const templatesForType = templateSummaries.filter( + (template) => template.documentType === docType.id, + ); + + return ( + +
+
+
+
+ + {templatesForType.slice(0, 2).map((template, index) => ( +
+ +
+ ))} +
+ +
+
+
+

+ {docType.pluralLabel} +

+ +
+ +

{docType.description}

+
+ +
+ {docType.highlights.map((highlight) => ( + + {highlight} + + ))} +
+
+
+ + ); + })} +
+ +
+
+ {plannedDocTypes.map((docType) => ( +
+
+
+ + + +

{docType.pluralLabel}

+
+ +
+ +

{docType.description}

+
+ ))} +
+
+
); }; -export default TemplatesPage; +export default TemplatesPortalPage; diff --git a/apps/site/config/templates.ts b/apps/site/config/templates.ts index 1e48692..9ae7fd5 100644 --- a/apps/site/config/templates.ts +++ b/apps/site/config/templates.ts @@ -1,37 +1,411 @@ +export type TemplateDocumentType = "resume" | "cover-letter"; + +export type TemplateStatus = "available" | "planned"; + +export type TemplateDetailSection = { + title: string; + description: string; + items: string[]; +}; + export type TemplateSummary = { id: string; + editorTemplateId: string; + legacyIds: string[]; name: string; - documentType: "resume" | "cover-letter"; + documentType: TemplateDocumentType; + documentTypeLabel: string; description: string; + shortDescription: string; accentColor: string; previewImage: string; tags: string[]; + family: string; + layout: string; + audience: string[]; + bestFor: string[]; + designVision: string; + typography: string[]; + structure: TemplateDetailSection[]; + proofPoints: string[]; + seo: { + title: string; + description: string; + }; +}; + +export type DocumentTypeSummary = { + id: TemplateDocumentType | "formal-letter" | "invoice" | "portfolio-website"; + label: string; + pluralLabel: string; + description: string; + href: string; + status: TemplateStatus; + cta: string; + seoTitle: string; + seoDescription: string; + highlights: string[]; }; +export const documentTypeSummaries: DocumentTypeSummary[] = [ + { + id: "resume", + label: "Resume", + pluralLabel: "Resume Templates", + description: + "ATS-safe resume systems with real PDF exports, recruiter-friendly hierarchy, and strong first-page scanning.", + href: "/templates/resume", + status: "available", + cta: "Explore Resume Templates", + seoTitle: "Free Resume Templates | ATS-Friendly Resume Designs", + seoDescription: + "Browse free, ATS-friendly resume templates for modern job applications. Compare structure, typography, layout, and target use cases.", + highlights: ["ATS-aware layouts", "PDF-ready previews", "Recruiter scan paths"], + }, + + { + id: "cover-letter", + label: "Cover Letter", + pluralLabel: "Cover Letter Templates", + description: + "Cover letter formats built for polished applications, clear intent, and consistent branding beside your resume.", + href: "/templates/cover-letter", + status: "available", + cta: "Explore Cover Letters", + seoTitle: "Free Cover Letter Templates | Professional Letter Designs", + seoDescription: + "Browse free cover letter templates with professional structure, letterhead treatments, and editor-ready layouts.", + highlights: ["Letterhead systems", "Formal spacing", "Resume pairing"], + }, + + { + id: "formal-letter", + label: "Formal Letter", + pluralLabel: "Formal Letter Templates", + description: + "Business letters, recommendations, notices, and structured correspondence templates are planned next.", + href: "/templates/formal-letter", + status: "planned", + cta: "Planned", + seoTitle: "Formal Letter Templates | VeriWorkly", + seoDescription: "Formal letter templates are planned for VeriWorkly.", + highlights: ["Business correspondence", "Printable formats", "Reusable identity blocks"], + }, + + { + id: "invoice", + label: "Invoice", + pluralLabel: "Invoice Templates", + description: + "Clean invoice templates for freelancers and operators are planned for future document workflows.", + href: "/templates/invoice", + status: "planned", + cta: "Planned", + seoTitle: "Invoice Templates | VeriWorkly", + seoDescription: "Invoice templates are planned for VeriWorkly.", + highlights: ["Line items", "Payment details", "Client-ready export"], + }, + + { + id: "portfolio-website", + label: "Portfolio Website", + pluralLabel: "Portfolio Website Templates", + description: + "Website templates for personal portfolios are coming soon. Fill information, publish portfolio site, and share it.", + href: "/templates/portfolio-website", + status: "planned", + cta: "Coming soon", + seoTitle: "Portfolio Website Templates | VeriWorkly", + seoDescription: + "Portfolio website templates are coming soon for VeriWorkly, with hosted subdomain publishing.", + highlights: ["Hosted subdomain", "Project sections", "Public website"], + }, +]; + export const templateSummaries: TemplateSummary[] = [ { - id: "executive-clarity", + id: "resume-executive-clarity", + editorTemplateId: "executive-clarity", + legacyIds: ["executive-clarity"], name: "Executive Clarity", documentType: "resume", + documentTypeLabel: "Resume", description: - "A polished single-column resume with refined spacing, strong section rhythm, and ATS-safe structure. Ideal for professionals who want a sophisticated, modern look.", + "A polished single-column resume with refined spacing, strong section rhythm, and ATS-safe structure. Ideal for experienced professionals who need authority without visual noise.", + shortDescription: + "Executive-grade spacing and hierarchy for a calm, senior resume presentation.", accentColor: "#0ea5e9", - previewImage: "/templates/executive-clarity.png", + previewImage: "/templates/resume/executive-clarity.png", tags: ["One column", "ATS-friendly", "Modern", "Professional"], + family: "Modern Core", + layout: "One column", + audience: ["Senior individual contributors", "Managers", "Consultants", "Operators"], + bestFor: [ + "Career stories where judgment and scope matter more than visual flash.", + "Applications where the first page needs to feel composed, current, and easy to skim.", + "Professionals who want a modern resume without risky columns or decorative parsing traps.", + ], + designVision: + "Executive Clarity treats the resume like a high-trust business document: measured whitespace, a confident name block, and section rhythm that lets senior accomplishments breathe.", + typography: [ + "Large identity block for immediate name recognition.", + "Quiet section labels that create rhythm without shouting.", + "Comfortable body measure for accomplishment bullets and leadership context.", + ], + structure: [ + { + title: "Opening Scan", + description: + "The top band prioritizes identity, contact context, and a concise professional summary.", + items: [ + "Name-led hierarchy", + "Contact line kept readable", + "Summary placed before dense history", + ], + }, + { + title: "Experience Core", + description: + "Role entries use steady spacing so outcomes, ownership, and business scope can be compared quickly.", + items: [ + "Single-column flow", + "Clear date alignment", + "Bullet rhythm for measurable outcomes", + ], + }, + { + title: "Supporting Proof", + description: + "Education and skills stay compact, letting the strongest work history carry the document.", + items: ["Compact skill groups", "ATS-readable text", "No fragile graphical meters"], + }, + ], + proofPoints: [ + "Best when your resume needs to feel senior, calm, and editorially controlled.", + "Keeps every important section in a predictable order for recruiters and parsers.", + "Pairs well with the Professional cover letter template for conservative applications.", + ], + seo: { + title: "Executive Clarity Resume Template | ATS-Friendly Senior Resume", + description: + "Use the Executive Clarity resume template for senior, management, consulting, and professional resumes that need a polished ATS-safe layout.", + }, }, { - id: "precision-ats", + id: "resume-precision-ats", + editorTemplateId: "precision-ats", + legacyIds: ["precision-ats"], name: "Precision ATS", documentType: "resume", + documentTypeLabel: "Resume", description: - "A dense, recruiter-friendly layout for longer resumes that still exports as a real matching PDF. Built for clarity and parsing accuracy above all else.", + "A dense, recruiter-friendly layout for longer resumes that still exports as a real matching PDF. Built for clarity, parsing accuracy, and fast comparison.", + shortDescription: "A compact ATS-first resume for detailed histories and high-signal bullets.", accentColor: "#10b981", - previewImage: "/templates/precision-ats.png", + previewImage: "/templates/resume/precision-ats.png", tags: ["One column", "ATS-friendly", "Compact", "Simple"], + family: "Compact Core", + layout: "One column", + audience: ["Engineers", "Analysts", "Technical specialists", "Multi-role professionals"], + bestFor: [ + "Longer work histories that still need to fit into a controlled page count.", + "Keyword-sensitive applications where parsing accuracy matters.", + "Candidates who want structure and density without a visually crowded result.", + ], + designVision: + "Precision ATS is built like a disciplined index of evidence: tight vertical rhythm, clear headings, and very little ornamentation between the recruiter and the facts.", + typography: [ + "Compact heading scale to preserve vertical space.", + "Readable bullet density for technical achievements.", + "Minimal accent usage so keywords and outcomes remain the focus.", + ], + structure: [ + { + title: "Dense Header", + description: + "Contact and identity details stay compact so the work history starts quickly.", + items: [ + "Space-efficient contact line", + "Small accent surface", + "No image or sidebar dependency", + ], + }, + { + title: "ATS Work History", + description: + "The body is optimized for readable chronology, strong keyword placement, and clean export text.", + items: ["Chronological role blocks", "Parser-safe bullets", "Consistent date treatment"], + }, + { + title: "Skill Compression", + description: + "Skills and education remain compact enough to support longer experience sections.", + items: ["Grouped skills", "Short education rows", "Simple section dividers"], + }, + ], + proofPoints: [ + "Best when every line needs to earn its place.", + "Keeps formatting conservative for applicant tracking systems.", + "Strong fit for technical resumes with many tools, projects, and measurable results.", + ], + seo: { + title: "Precision ATS Resume Template | Compact ATS Resume Format", + description: + "Use the Precision ATS resume template for compact, keyword-friendly resumes that prioritize clean parsing and recruiter readability.", + }, + }, + + { + id: "cover-letter-professional", + editorTemplateId: "professional", + legacyIds: ["professional"], + name: "Professional", + documentType: "cover-letter", + documentTypeLabel: "Cover Letter", + description: + "A formal cover letter with a strong letterhead, conservative spacing, and a recruiter-safe structure for direct, polished applications.", + shortDescription: "A formal letterhead layout for conservative, high-trust applications.", + accentColor: "#0ea5e9", + previewImage: "/templates/cover-letter/professional.png", + tags: ["Formal", "Professional", "Conservative", "Recruiter-friendly"], + family: "Classic Letter", + layout: "One column", + audience: [ + "Corporate applicants", + "Graduate candidates", + "Operations roles", + "Public-sector roles", + ], + bestFor: [ + "Applications where tone, clarity, and restraint matter.", + "Pairing with an ATS-friendly resume without changing visual language.", + "Cover letters that need to look credible when exported as a standalone PDF.", + ], + designVision: + "Professional keeps the letter unmistakably formal while giving the sender identity enough presence to feel intentional rather than generic.", + typography: [ + "Clear sender block for letterhead authority.", + "Readable paragraph spacing for hiring-manager review.", + "Conservative heading weight that avoids over-branding.", + ], + structure: [ + { + title: "Letterhead", + description: "The top block frames the sender and recipient before the letter begins.", + items: ["Sender identity", "Recipient context", "Date and subject treatment"], + }, + { + title: "Body Flow", + description: "Paragraph spacing keeps motivation, fit, and proof points easy to follow.", + items: ["Formal greeting", "Readable body paragraphs", "Controlled closing block"], + }, + { + title: "Export Shape", + description: + "The design stays printable and professional across PDF export and browser preview.", + items: ["No fragile overlays", "Letter-sized composition", "Recruiter-safe contrast"], + }, + ], + proofPoints: [ + "Best when the letter should feel established and serious.", + "Useful for applications where a highly designed letter would feel out of place.", + "Pairs well with Precision ATS for a clean, conservative application set.", + ], + seo: { + title: "Professional Cover Letter Template | Formal Letterhead Design", + description: + "Use the Professional cover letter template for formal applications with a clean letterhead, readable body structure, and PDF-ready layout.", + }, + }, + + { + id: "cover-letter-veriworkly-special", + editorTemplateId: "veriworkly-special", + legacyIds: ["veriworkly-special"], + name: "VeriWorkly Special", + documentType: "cover-letter", + documentTypeLabel: "Cover Letter", + description: + "A branded two-column cover letter with an identity rail and numbered proof points for applicants who want a more distinctive application page.", + shortDescription: "A branded cover letter with an identity rail and structured proof points.", + accentColor: "#2563eb", + previewImage: "/templates/cover-letter/veriworkly-special.png", + tags: ["Branded", "Two-column", "Identity rail", "Distinctive"], + family: "Branded Letter", + layout: "Two column", + audience: [ + "Product builders", + "Design-minded candidates", + "Startup applicants", + "Portfolio-led roles", + ], + bestFor: [ + "Applications where tasteful distinctiveness is an advantage.", + "Candidates who want a letter that visually aligns with a modern resume.", + "Cover letters that benefit from highlighted proof points beside the main narrative.", + ], + designVision: + "VeriWorkly Special turns the cover letter into a composed application page: identity on the rail, narrative in the body, and proof points placed where they can be scanned.", + typography: [ + "Prominent sender identity for a strong first impression.", + "Balanced paragraph width for human reading.", + "Numbered proof markers that add structure without becoming decorative clutter.", + ], + structure: [ + { + title: "Identity Rail", + description: + "A side rail keeps contact details and applicant identity visible without crowding the body.", + items: ["Name and role rail", "Contact grouping", "Accent-led section rhythm"], + }, + { + title: "Narrative Column", + description: + "The main column gives the letter a conventional reading path with a modern page feel.", + items: ["Subject emphasis", "Readable paragraphs", "Clear closing and signature"], + }, + { + title: "Proof Points", + description: + "Highlights are shaped for scanning, helping the letter carry evidence as well as intent.", + items: ["Numbered highlights", "Visual separation", "PDF-ready composition"], + }, + ], + proofPoints: [ + "Best when you want the cover letter to feel crafted, not default.", + "Helps product, design, and startup candidates show taste without sacrificing readability.", + "Pairs well with Executive Clarity for a polished modern application set.", + ], + seo: { + title: "VeriWorkly Special Cover Letter Template | Branded Application Letter", + description: + "Use the VeriWorkly Special cover letter template for a branded two-column application letter with identity rail and structured proof points.", + }, }, ]; +export function getDocumentTypeSummary(docType: string): DocumentTypeSummary | undefined { + return documentTypeSummaries.find((type) => type.id === docType); +} + +export function getTemplatesByDocumentType(docType: string): TemplateSummary[] { + return templateSummaries.filter((template) => template.documentType === docType); +} + export function getTemplateById(id: string): TemplateSummary | undefined { - return templateSummaries.find((t) => t.id === id); + return templateSummaries.find((template) => template.id === id); +} + +export function getTemplateByDocumentTypeAndId( + docType: string, + id: string, +): TemplateSummary | undefined { + return templateSummaries.find( + (template) => template.documentType === docType && template.id === id, + ); +} + +export function getTemplateByLegacyId(id: string): TemplateSummary | undefined { + return templateSummaries.find((template) => template.legacyIds.includes(id)); } diff --git a/apps/site/features/landing/components/TemplatesPreview.tsx b/apps/site/features/landing/components/TemplatesPreview.tsx index 4cc5e51..adf0297 100644 --- a/apps/site/features/landing/components/TemplatesPreview.tsx +++ b/apps/site/features/landing/components/TemplatesPreview.tsx @@ -68,7 +68,7 @@ const TemplatesPreview = () => {
diff --git a/apps/site/public/templates/cover-letter/professional.png b/apps/site/public/templates/cover-letter/professional.png new file mode 100644 index 0000000..b2488f4 Binary files /dev/null and b/apps/site/public/templates/cover-letter/professional.png differ diff --git a/apps/site/public/templates/cover-letter/veriworkly-special.png b/apps/site/public/templates/cover-letter/veriworkly-special.png new file mode 100644 index 0000000..10d242e Binary files /dev/null and b/apps/site/public/templates/cover-letter/veriworkly-special.png differ diff --git a/apps/site/public/templates/resume/executive-clarity.png b/apps/site/public/templates/resume/executive-clarity.png new file mode 100644 index 0000000..99a01ae Binary files /dev/null and b/apps/site/public/templates/resume/executive-clarity.png differ diff --git a/apps/site/public/templates/resume/precision-ats.png b/apps/site/public/templates/resume/precision-ats.png new file mode 100644 index 0000000..bbc2cc7 Binary files /dev/null and b/apps/site/public/templates/resume/precision-ats.png differ