Templating
The listing
{{ craft.hire.listing() }}
That is a working careers page — search box, filters, results, pagination — rendered on the server, so it works with JavaScript off, is indexable, and every filter is a link you can paste into an email. A small script enhances it; it never creates content the server didn't render.
Options:
{{ craft.hire.listing({
heading: 'Open positions',
limit: 10,
search: false,
orderBy: 'expiryDate asc',
workplaceType: 'remote',
}) }}
| Option | |
|---|---|
heading | A heading above the listing |
limit | Jobs per page (default from Settings → General) |
search | Show the search box |
orderBy | postDate, expiryDate, applications, title, dateCreated, dateUpdated or a custom field handle, then asc or desc |
filters | Fixed filters, as { groupHandle: ['term-slug'] } |
workplaceType, formId, specId, siteId | Passed straight to the job query |
Closed jobs are left out unless Keep closed jobs in the listing is on.
The data without the markup
{% set result = craft.hire.results({ limit: 10 }) %}
<p>{{ result.total }} jobs</p>
{% for job in result.jobs %}
<a href="{{ job.url }}">{{ job.title }}</a> — {{ job.locationLabel }}
{% endfor %}
results() is the same call the built-in listing and the JSON endpoint make, so
the three can never disagree.
Querying jobs
craft.hire.jobs is an ordinary element query:
{% set jobs = craft.hire.jobs
.status('open')
.spec({ department: ['engineering'], jobType: ['full-time'] })
.orderBy('hire_jobs.postDate desc')
.all() %}
Within a group, filters are OR; across groups, AND — see Specifications and filtering.
A job page
Set the template for each site under Settings → Job URLs. The job is available as job:
{% block head %}{{ craft.hire.schema(job) }}{% endblock %}
<h1>{{ job.title }}</h1>
<p>{{ job.locationLabel }} · {{ job.salary }} · closes {{ job.expiryDate|date('j F Y') }}</p>
{{ job.description }} {# a field of your own #}
{{ craft.hire.applyForm(job) }}
applyForm() renders nothing when the job takes applications by URL, email or not at all — use
job.applyUrl for the first two.
Structured data
craft.hire.schema(job) outputs the JobPosting JSON-LD that Google for Jobs reads. Hire builds it
rather than leaving it to a template, because an invalid posting fails silently — nothing is
flagged anywhere a site owner looks; the job simply never appears. It includes datePosted,
validThrough, identifier, directApply, baseSalary, employmentType,
occupationalCategory, and both halves of a remote posting: jobLocationType and
applicantLocationRequirements.
Fill in the organisation name, URL, logo, default country and currency under
Settings → General. craft.hire.listSchema(jobs) outputs an ItemList for a listing page.
Changing the markup
Every template Hire renders is an ordinary Twig file. Copy it into your own templates/hire/ and
edit it — Hire looks for the site's copy first. No overrides file, no theme layer.
| Copy this | To change |
|---|---|
_listing/listing.twig | the whole careers page |
_listing/_filters.twig | the filter panel |
_listing/_job.twig | one row of the listing |
_form/form.twig | the application form |
_form/_field.twig | one question |
_portal/status.twig | the applicant's status page |
Writing your own form
If you hand-write the form instead, it must post to hire/apply/submit with the jobId, and carry
the signed timestamp — a form posted without one is refused by the timing check:
<form method="post" enctype="multipart/form-data">
{{ csrfInput() }}
{{ actionInput('hire/apply/submit') }}
{{ hiddenInput('jobId', job.id) }}
{{ hiddenInput('hireStamp', craft.hire.formTimestamp()) }}
…
</form>
Starting from a copy of _form/form.twig is easier, and keeps the honeypot and any CAPTCHA.
Everything else on craft.hire
craft.hire.job(id) | One job, by ID, reference (JOB-104) or slug |
craft.hire.applications | An application query |
craft.hire.specGroups, specGroup(handle), specs(handle) | Specification groups and terms |
craft.hire.specCounts() | Job counts per term |
craft.hire.stages | Pipeline stages |
craft.hire.form(handle) | An application form |
craft.hire.settings | Plugin settings |