Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/ronalfy/rh-cover-letter-generator/archive/refs/heads/main.zipA WordPress plugin that provides an interactive cover letter generator with JSX and Webpack. Users answer tone-specific questions (formal, casual, awkward) and see a live preview they can copy.
https://github.com/user-attachments/assets/b76ffe5b-cb62-4ddd-a44b-0356088491b1
1. Project Description
This app was initially AI-coded, with the answers written by Ronald Huereca.
- Frontend: React app (tone tabs, question forms, live preview) built with Webpack, Tanstack Router, and @wordpress/data.
- Content: Questions and preview copy are driven by a single JSON config (
src/js/data/questions.json) so you can change prompts and templates without editing PHP or JSX. - Features: Multiple tones, conditional preview sections, inline conditionals in templates (e.g. “show company name when provided, otherwise ‘your company’”), and copy-to-clipboard for the generated text.
2. Installation & Shortcode Usage
WordPress installation
- Install the plugin (copy the plugin folder into
wp-content/plugins/or install via ZIP). - Activate RH - Cover Letter Generator in the WordPress admin (Plugins screen).
- (Optional) Run the build step so the app bundle exists — see Setup & build below.
Shortcode
The generator is rendered by a shortcode. Scripts and styles are only enqueued on pages that contain it.
Shortcode: rh_cover_letter_generator
Example:
[rh_cover_letter_generator]
The shortcode outputs a single div: <div id="rh-cover-letter-generator-app"></div>. The React app mounts there and handles the rest (tabs, forms, preview, copy).
3. Setup & Build (CLI)
You need Node.js and npm to build the JS and CSS.
One-time setup
cd wp-content/plugins/rh-cover-letter-generator
npm install
Build commands
| Command | Description |
|---|---|
npm run build |
Production build. Outputs dist/js/app.js, dist/js/app.asset.php, and dist/css/app.css. |
npm run dev or npm start |
Development build with watch; recompiles on file changes. |
After the first npm run build, the plugin will work in WordPress. Re-run npm run build whenever you change anything under src/ (JS, JSX, or CSS).
Grunt (distribution zip)
Grunt is used to package the plugin for distribution. Run a production build first, then:
npx grunt
or, with Grunt installed globally, grunt. The default task creates rh-cover-letter-generator.zip in the plugin root, containing the main plugin file, includes/, dist/, and vendor/. You can upload that zip in the WordPress admin (Plugins → Add New → Upload Plugin) or use it for deployment.
4. Question Schema & Instructions
All questions and preview content live in one file:
src/js/data/questions.json
The root is keyed by tone (e.g. formal, casual, awkward). You can add or rename tones. Each tone object has:
| Property | Required | Description |
|---|---|---|
tone |
Yes | Same as the key (e.g. "formal"). |
label |
Yes | Tab label (e.g. "Formal"). |
description |
Yes | Short description under the tab. |
questions |
Yes | Array of question objects. |
previewTemplate |
Yes | Object with a sections array (see below). |
Example root:
{
"formal": { "tone": "formal", "label": "Formal", "description": "...", "questions": [], "previewTemplate": { "sections": [] } },
"casual": { ... }
}
Question object
Each item in questions must have:
| Property | Required | Description |
|---|---|---|
id |
Yes | Unique ID within this tone. Used in form state and in templates as {id}. |
label |
Yes | Label shown above the field. |
type |
Yes | "text" or "choice". |
placeholder |
For text |
Placeholder for the input. |
options |
For choice |
Array of { "value": "...", "label": "..." }. value is stored and used in templates (use snake_case). label is shown to the user. |
validation |
Yes | Object. Use { "required": true } or { "required": false }. |
Text question example:
{
"id": "company",
"label": "Company Name",
"type": "text",
"placeholder": "Acme Corp",
"validation": { "required": false }
}
Choice question example:
{
"id": "position",
"label": "What type of position?",
"type": "choice",
"options": [
{ "value": "engineer", "label": "Engineer" },
{ "value": "software_developer", "label": "Software Developer" }
],
"validation": { "required": true }
}
Preview template and sections
previewTemplate has one property:
sections(array): List of section objects. They are evaluated in order; all sections that pass their dependencies are concatenated to form the preview text.
Each section is either static or dynamic.
Static section
Always included. No variables.
| Property | Required | Description |
|---|---|---|
type |
Yes | "static" |
content |
Yes | String. Use \n\n for paragraph breaks. |
{
"type": "static",
"content": "Thank you for reading my application.\n\n"
}
Dynamic section
Included only when all of its dependencies are satisfied. Supports placeholders and inline conditions in the template.
| Property | Required | Description |
|---|---|---|
type |
Yes | "dynamic" |
template |
Yes | String with {questionId} placeholders and/or inline conditions (see below). |
dependencies |
Yes | Array of dependency objects. |
Dependency object:
| Property | Required | Description |
|---|---|---|
questionId |
Yes | Question whose answer is used in the template and/or evaluated by rules. |
defaultValue |
No | Used when the dependency fails validation (optional). |
rules |
No | Array of rule objects. If any rule fails, the entire section is skipped. |
Rule object — one of:
| Rule | Description | Example |
|---|---|---|
not_empty |
Answer must be non-empty. | { "type": "not_empty" } |
empty |
Answer must be empty. | { "type": "empty" } |
equals |
Answer must equal value. |
{ "type": "equals", "value": "startup" } |
not_equals |
Answer must not equal value. |
{ "type": "not_equals", "value": "agency" } |
Example dynamic section:
{
"type": "dynamic",
"template": "For {company}, this role clearly values {priority}.\n\nThe experience below reflects a steady approach.\n\n",
"dependencies": [
{
"questionId": "company",
"rules": [ { "type": "not_empty" } ]
},
{
"questionId": "priority",
"rules": [ { "type": "not_empty" } ]
}
]
}
If either company or priority is empty, this section is skipped. Otherwise {company} and {priority} are replaced by the user’s answers (for choice questions, the value is used, not the label).
Template placeholders
In a dynamic section’s template:
{questionId}— Replaced by that question’s answer. Must match anidinquestions. For choice questions, the option’s value is used (e.g.engineer), not the label. Case-sensitive.
Inline conditions
Inside a template you can conditionally show one value or a fallback using:
Format:
{condition:questionId:ruleType:ruleValue? trueValue|falseValue}
- questionId — Which question to evaluate.
- ruleType —
not_empty,empty,equals, ornot_equals. - ruleValue — Only for
equals/not_equals(e.g.engineer). Omit fornot_empty/empty. - trueValue — Used when the condition passes. Can be:
- A question ID → replaced by that question’s answer (or its label for choice questions).
- Literal text (e.g.
your company). - Empty → nothing inserted (leave a space before the pipe).
- falseValue — Literal text when the condition fails. No question ID resolution.
There must be a space after ruleType (or ruleValue) before trueValue. The two alternatives are separated by a pipe |.
Examples:
| Inline condition | Meaning |
|---|---|
{condition:company:not_empty company|your company} |
If company not empty → company answer; else → “your company”. |
{condition:position:equals:engineer company|your team} |
If position is “engineer” → company answer; else → “your team”. |
{condition:company:not_empty »|your team} (use a space where » is) |
If company not empty → nothing; else → “your team”. (Empty trueValue = space before the pipe.) |
Full example in a section:
{
"type": "dynamic",
"template": "I have worked at several startups. I can adapt to help {condition:company:not_empty company|your company} succeed.\n\n",
"dependencies": [
{
"questionId": "company_type",
"rules": [ { "type": "equals", "value": "startup" } ]
}
]
}
This section only appears when company_type equals startup. The sentence uses the company name when provided, otherwise “your company”.