Skip to content

Google for Jobs on any website

If your job pages are rendered by your own stack — Next.js, Rails, Laravel, Django, a headless CMS — pull jobs from the Public Jobs API and emit the JobPosting JSON-LD server-side. Google must see the markup in the initial HTML response; injecting it client-side after load (for example from a JavaScript jobs widget) is unreliable for crawling — which is exactly why an embedded widget alone doesn't get you listed.

1. Fetch your published jobs

The Public Jobs API returns every job you've published to your website, with the full advert content:

GET https://api.recruitly.io/api/nova/public/jobs?apiKey=YOUR_API_KEY
GET https://api.recruitly.io/api/nova/public/jobs/{jobIdOrReference}?apiKey=YOUR_API_KEY

See the API reference for the full endpoint list (counts, sectors, industries, locations, per-client filters) and Authentication for API keys.

The fields you need for JobPosting, as returned by the API:

API fieldJSON-LD use
titletitle
descriptiondescription (full HTML advert — Google allows HTML)
postedOndatePosted
estClosingDate / cutOffDatevalidThrough
referenceidentifier
companyNamehiringOrganization.name (fall back to your agency name)
jobTypemap to Google's employmentType values
remoteWorkingjobLocationType: 'TELECOMMUTE'
location.cityName / location.regionName / location.postCode / location.countryCodejobLocation.address
pay.minPay / pay.maxPay / pay.currency / pay.tenurebaseSalary

2. Render the JSON-LD per job page

Example in JavaScript — the same mapping works in any language:

js
const EMPLOYMENT_TYPES = {
  'Permanent': 'FULL_TIME',
  'Full Time': 'FULL_TIME',
  'Part Time': 'PART_TIME',
  'Contract': 'CONTRACTOR',
  'Temporary': 'TEMPORARY',
  'Intern': 'INTERN',
  // map every job type label you use in Recruitly
};

function jobPostingJsonLd(job, agency) {
  const ld = {
    '@context': 'https://schema.org/',
    '@type': 'JobPosting',
    title: job.title,
    description: job.description, // full advert; HTML allowed
    datePosted: job.postedOn?.slice(0, 10),
    employmentType: EMPLOYMENT_TYPES[job.jobType] || 'FULL_TIME',
    identifier: {
      '@type': 'PropertyValue',
      name: agency.name,
      value: job.reference,
    },
    hiringOrganization: {
      '@type': 'Organization',
      name: job.companyName || agency.name,
      sameAs: agency.website,
    },
    jobLocation: {
      '@type': 'Place',
      address: {
        '@type': 'PostalAddress',
        addressLocality: job.location?.cityName || undefined,
        addressRegion: job.location?.regionName || undefined,
        postalCode: job.location?.postCode || undefined,
        addressCountry: job.location?.countryCode, // 2-letter code
      },
    },
    directApply: false,
  };

  if (job.estClosingDate) ld.validThrough = job.estClosingDate;
  if (job.remoteWorking) ld.jobLocationType = 'TELECOMMUTE';

  if (job.pay && (job.pay.minPay > 0 || job.pay.maxPay > 0)) {
    ld.baseSalary = {
      '@type': 'MonetaryAmount',
      currency: job.pay.currency?.code,
      value: {
        '@type': 'QuantitativeValue',
        minValue: job.pay.minPay > 0 ? job.pay.minPay : undefined,
        maxValue: job.pay.maxPay > 0 ? job.pay.maxPay : undefined,
        unitText: job.pay.tenure?.code || 'YEAR', // HOUR | DAY | WEEK | MONTH | YEAR
      },
    };
  }

  // Stop "</script>"-style content in the description closing the tag early:
  return JSON.stringify(ld).replace(/<\//g, '<\\/');
}

In your job page template:

html
<script type="application/ld+json">${jobPostingJsonLd(job, agency)}</script>

3. The rules that get sites rejected

  • One job = one crawlable URL — public, in your sitemap, not blocked by robots.txt, no login wall.
  • Escape </ in the JSON output (see snippet) so descriptions containing HTML can't break out of the <script> element.
  • Expire closed jobs — return 404/410 or set validThrough in the past the moment a job closes. Sync on a schedule (or use webhooksjob_created / job_deleted events) so your pages track the CRM.
  • Full descriptions — Google drops thin listings; use description, not shortDescription.
  • hiringOrganization is mandatory — use your agency name or "Confidential Employer" for confidential roles.

Next: validate and submit

Head to Testing & Search Console.

Recruitly — Recruitment CRM for Agencies