Appearance
Google for Jobs on WordPress
The Recruitly WordPress plugin syncs your published jobs into WordPress as a current-vacancies custom post type, with every job attribute stored as post meta (full field reference). The plugin does not output JobPosting markup itself — you add it once in your theme, using the metadata the plugin has already synced.
Prerequisites
- A WordPress site with the Recruitly plugin installed and jobs syncing
- Access to your theme's
functions.php(WordPress Admin → Appearance → Theme Editor, or FTP/cPanel at/wp-content/themes/your-theme/functions.php) - A Google Search Console account for monitoring (recommended)
TIP
Use a child theme (or a small custom plugin / Code Snippets plugin) so the code survives theme updates.
The snippet
php
/**
* Add Google for Jobs (JobPosting) structured data to Recruitly job posts.
* Customize the values marked CUSTOMIZE below.
*/
function custom_recruitly_google_jobs_structured_data() {
if (is_singular('current-vacancies')) { // Recruitly post type
global $post;
// CUSTOMIZE THESE VALUES
$agency_name = 'Your Agency Name'; // Fallback hiring organisation
$currency = 'GBP'; // GBP, USD, EUR, AUD, CAD, INR, ...
$default_country = 'GB'; // 2-letter code: GB, US, AU, CA, IN, ...
// Map your Recruitly job types to Google's employment types
$employment_type_map = array(
'Full Time' => 'FULL_TIME',
'Part Time' => 'PART_TIME',
'Contract' => 'CONTRACTOR',
'Temporary' => 'TEMPORARY',
'Intern' => 'INTERN',
'Freelance' => 'CONTRACTOR',
'Permanent' => 'FULL_TIME',
'Casual' => 'PART_TIME',
'Fixed Term' => 'TEMPORARY',
// Add more mappings as needed
);
// Job meta synced by the Recruitly plugin
$job_type = get_post_meta($post->ID, 'jobType', true);
$posted_on = get_post_meta($post->ID, 'postedOn', true);
$company_name = get_post_meta($post->ID, 'companyName', true);
$min_salary = get_post_meta($post->ID, 'minSalaryRange', true);
$max_salary = get_post_meta($post->ID, 'maxSalaryRange', true);
// Location meta
$country = get_post_meta($post->ID, 'countryCode', true) ?: $default_country;
$city = get_post_meta($post->ID, 'town', true);
$region = get_post_meta($post->ID, 'county', true);
$postal_code = get_post_meta($post->ID, 'postCode', true);
$remote_working = get_post_meta($post->ID, 'remoteWorking', true);
$employment_type = isset($employment_type_map[$job_type]) ? $employment_type_map[$job_type] : 'FULL_TIME';
$structured_data = array(
'@context' => 'https://schema.org/',
'@type' => 'JobPosting',
'title' => get_the_title(),
'description' => strip_tags(get_the_content()),
'datePosted' => date('Y-m-d', strtotime($posted_on)),
'employmentType' => $employment_type,
'validThrough' => date('Y-m-d', strtotime($posted_on . ' + 60 days')) . 'T23:59:59'
);
// Hiring organisation — client company if present, otherwise your agency
$structured_data['hiringOrganization'] = array(
'@type' => 'Organization',
'name' => $company_name ?: $agency_name
);
if ($remote_working == 'true' || $remote_working == 1) {
$structured_data['jobLocationType'] = 'TELECOMMUTE';
}
$location_data = array(
'@type' => 'Place',
'address' => array(
'@type' => 'PostalAddress',
'addressCountry' => $country
)
);
if ($city) $location_data['address']['addressLocality'] = $city;
if ($region) $location_data['address']['addressRegion'] = $region;
if ($postal_code) $location_data['address']['postalCode'] = $postal_code;
$structured_data['jobLocation'] = $location_data;
// Salary — optional, but jobs with salary get significantly more clicks
if ($min_salary || $max_salary) {
$salary_data = array(
'@type' => 'MonetaryAmount',
'currency' => $currency
);
if ($min_salary && $max_salary) {
$salary_data['value'] = array(
'@type' => 'QuantitativeValue',
'minValue' => floatval($min_salary),
'maxValue' => floatval($max_salary),
'unitText' => 'YEAR'
);
} else {
$salary_data['value'] = array(
'@type' => 'QuantitativeValue',
'value' => floatval($min_salary ?: $max_salary),
'unitText' => 'YEAR'
);
}
$structured_data['baseSalary'] = $salary_data;
}
?>
<script type="application/ld+json">
<?php echo wp_json_encode($structured_data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); ?>
</script>
<?php
}
}
add_action('wp_head', 'custom_recruitly_google_jobs_structured_data');Customise your values
| Variable | What it does | Example |
|---|---|---|
$agency_name | Hiring organisation whenever a job has no client company (or you run confidential roles) | 'Acme Recruitment' |
$currency | ISO currency code for salaries | 'GBP', 'USD', 'EUR', 'AUD' |
$default_country | Fallback 2-letter country code when a job has none | 'GB', 'US', 'AU' |
$employment_type_map | Maps every job-type label your agency uses to a Google value | see snippet |
Google accepts only these employment types: FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, VOLUNTEER, PER_DIEM, OTHER. Any label of yours that isn't mapped falls back to FULL_TIME — add every label you use in Recruitly to the map.
Meta fields used
All synced by the plugin onto each current-vacancies post:
| Meta key | Contents |
|---|---|
jobType | Employment type label as set in Recruitly (e.g. Permanent, Contract) |
postedOn | Posting date |
companyName | Hiring company (empty for confidential clients) |
minSalaryRange / maxSalaryRange | Salary range |
countryCode / country | 2-letter country code / country name |
town / county / postCode | City, region, postcode |
remoteWorking | Set when the job is remote |
The full list of synced fields (recruiter, web advert sections, images, taxonomies) is in the Custom Post Type reference.
Next: validate and submit
Head to Testing & Search Console — check a job page with the Rich Results Test, submit your sitemap, and monitor the Job postings report.