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 webhooks — job_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