feat(seo): add ai resume prompts hub#526
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the 'AI Resume Prompts Hub' blog post, adding a new component, routing, and metadata for comparing AI tools in resume writing. Feedback suggests aligning with repository standards by passing FAQ data directly to the BlogLayout component for centralized rendering and schema generation. Additionally, it is recommended to import shared constants from a single source of truth to avoid duplication and to use prefixed indices for React keys in dynamic lists to ensure uniqueness.
| const faqSchema = generateFAQPageSchema(HUB_FAQS); | ||
| const combinedSchema = wrapInGraph([faqSchema]); | ||
|
|
||
| return ( | ||
| <> | ||
| <Helmet> | ||
| <script type="application/ld+json">{JSON.stringify(combinedSchema)}</script> | ||
| </Helmet> | ||
| <BlogLayout | ||
| title="AI Resume Prompts Hub: Best Prompts for ChatGPT, Claude, Gemini & More" | ||
| description="Compare Claude, ChatGPT, Gemini, Grok, Copilot, and DeepSeek for resume writing. Pick the best AI prompt for bullets, summaries, ATS keywords, and cover letters." | ||
| publishDate={REVIEW_DATE} | ||
| lastUpdated={REVIEW_DATE} | ||
| readTime="12 min" | ||
| keywords={[ | ||
| "ai resume prompts", | ||
| "best ai for resume writing", | ||
| "chatgpt resume prompts", | ||
| "claude resume prompts", | ||
| "gemini resume prompts", | ||
| "ats keyword prompts", | ||
| ]} | ||
| ctaType="resume" | ||
| > |
There was a problem hiding this comment.
According to the repository's general rules, blog posts with an FAQ section should pass the questions and answers as a prop to the component. The layout component is responsible for both rendering the FAQs and generating the FAQPage JSON-LD schema. This ensures consistency and reduces boilerplate in individual blog components.
| const faqSchema = generateFAQPageSchema(HUB_FAQS); | |
| const combinedSchema = wrapInGraph([faqSchema]); | |
| return ( | |
| <> | |
| <Helmet> | |
| <script type="application/ld+json">{JSON.stringify(combinedSchema)}</script> | |
| </Helmet> | |
| <BlogLayout | |
| title="AI Resume Prompts Hub: Best Prompts for ChatGPT, Claude, Gemini & More" | |
| description="Compare Claude, ChatGPT, Gemini, Grok, Copilot, and DeepSeek for resume writing. Pick the best AI prompt for bullets, summaries, ATS keywords, and cover letters." | |
| publishDate={REVIEW_DATE} | |
| lastUpdated={REVIEW_DATE} | |
| readTime="12 min" | |
| keywords={[ | |
| "ai resume prompts", | |
| "best ai for resume writing", | |
| "chatgpt resume prompts", | |
| "claude resume prompts", | |
| "gemini resume prompts", | |
| "ats keyword prompts", | |
| ]} | |
| ctaType="resume" | |
| > | |
| return ( | |
| <> | |
| <BlogLayout | |
| title="AI Resume Prompts Hub: Best Prompts for ChatGPT, Claude, Gemini & More" | |
| description="Compare Claude, ChatGPT, Gemini, Grok, Copilot, and DeepSeek for resume writing. Pick the best AI prompt for bullets, summaries, ATS keywords, and cover letters." | |
| publishDate={REVIEW_DATE} | |
| lastUpdated={REVIEW_DATE} | |
| readTime="12 min" | |
| keywords={[ | |
| "ai resume prompts", | |
| "best ai for resume writing", | |
| "chatgpt resume prompts", | |
| "claude resume prompts", | |
| "gemini resume prompts", | |
| "ats keyword prompts", | |
| ]} | |
| ctaType="resume" | |
| faqs={HUB_FAQS} | |
| > |
References
- For blog posts with an FAQ section, extract the questions and answers into a const FAQS array. Pass this array as a prop to the component, which will handle both rendering the FAQs and generating the FAQPage JSON-LD schema.
There was a problem hiding this comment.
Good catch — verified BlogLayout.tsx:33 declares faqs?: BlogFAQ[] and lines 98–104 auto-generate FAQPage schema. Eight existing blog files (ResumeLengthGuide, ResumeKeywordsGuide, ResumeActionVerbs, etc.) use this pattern. Fixing in a follow-up commit: switching to faqs={HUB_FAQS} and dropping the manual Helmet/wrapInGraph block.
| <section> | ||
| <h2 className="text-2xl font-bold text-ink">AI Resume Prompts FAQ</h2> | ||
| <div className="mt-4 space-y-4"> | ||
| {HUB_FAQS.map((faq) => ( | ||
| <div key={faq.question} className="rounded-lg border border-black/[0.06] bg-white p-4"> | ||
| <h3 className="text-lg font-bold text-ink">{faq.question}</h3> | ||
| <p className="mt-2 text-stone-warm">{faq.answer}</p> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </section> |
There was a problem hiding this comment.
This manual FAQ rendering section should be removed, as the component is intended to handle FAQ rendering via the faqs prop, per the general rules.
References
- For blog posts with an FAQ section, extract the questions and answers into a const FAQS array. Pass this array as a prop to the component, which will handle both rendering the FAQs and generating the FAQPage JSON-LD schema.
There was a problem hiding this comment.
Not actioning. BlogLayout's faqs prop only emits the JSON-LD schema (BlogLayout.tsx:98–104) — it does NOT render visible FAQ JSX. Verified against ResumeLengthGuide.tsx, which uses faqs={FAQS} AND renders the visible FAQ section in the body at line 587 ("Frequently Asked Questions"). Per Google's structured-data guidelines, FAQPage schema must match visible page content. Keeping the JSX intact.
| const INTERNAL_LINKS = [ | ||
| { href: "/templates", label: "Browse resume templates" }, | ||
| { href: "/free-resume-builder-no-sign-up", label: "Build a resume without sign-up" }, | ||
| { href: "/blog/claude-resume-prompts", label: "Claude resume prompts" }, | ||
| { href: "/blog/gemini-resume-prompts", label: "Gemini resume prompts" }, | ||
| { href: "/blog/best-free-resume-builders-2026", label: "Best free resume builders" }, | ||
| { href: "/blog/ai-cover-letter-prompts", label: "AI cover letter prompts" }, | ||
| { href: "/blog/ai-resume-writing-guide", label: "AI resume writing guide" }, | ||
| { href: "/resume-keyword-scanner", label: "ATS keyword scanner" }, | ||
| ]; |
There was a problem hiding this comment.
To avoid data duplication, shared constants like blog post slugs and titles should be imported from a single source of truth (e.g., src/data/blogPosts.ts). The INTERNAL_LINKS array currently hardcodes several paths that are already defined in the blog metadata.
References
- To avoid data duplication, import shared constants from a single source of truth and map over them to add component-specific metadata if needed.
There was a problem hiding this comment.
Considered. Net duplication is ~5 slug strings out of 8 entries (3 — /templates, /free-resume-builder-no-sign-up, /resume-keyword-scanner — aren't blog posts and can't be imported from blogPosts.ts anyway). Labels are hub-specific UX copy that wouldn't come from imports either. Mixing import-derived and hardcoded entries in one array adds complexity for marginal benefit. Skipping for now; can revisit if slug drift becomes a real problem.
| {model.prompts.map((prompt) => ( | ||
| <li key={prompt}>{prompt}</li> | ||
| ))} |
There was a problem hiding this comment.
When rendering lists in React where items lack stable IDs, use a prefixed index to create unique keys. This prevents potential collisions with other lists on the page and follows the repository's defensive patterns for list rendering.
| {model.prompts.map((prompt) => ( | |
| <li key={prompt}>{prompt}</li> | |
| ))} | |
| {model.prompts.map((prompt, index) => ( | |
| <li key={'prompt-' + index}>{prompt}</li> | |
| ))} |
References
- When rendering dynamic lists in React where items lack stable IDs, use a prefixed index (e.g., key={'prefix-' + index}) to create unique keys and prevent collisions with other lists.
There was a problem hiding this comment.
Not actioning. The "prefixed index" rule applies when items lack stable IDs. Here, each prompt string is itself unique (all 18 prompts across 6 AIs are distinct) and acts as the stable ID. React's docs explicitly recommend content-derived keys when uniqueness is guaranteed. Current key={prompt} is correct.
|
Shipped via #532 (post-cliff bundle → main). Closing. |
Summary
Test Plan
Notes