bastianschwabe / jobs
Job postings for TYPO3: filterable list, detail view and standards-compliant schema.org/JobPosting JSON-LD output for Google for Jobs.
Package info
github.com/bastianschwabe/typo3-jobs
Type:typo3-cms-extension
pkg:composer/bastianschwabe/jobs
Requires
- php: ^8.2
- typo3/cms-core: ^14.3
- typo3/cms-extbase: ^14.3
- typo3/cms-fluid: ^14.3
- typo3/cms-frontend: ^14.3
Requires (Dev)
- bastianschwabe/jobs-testsite: @dev
- friendsofphp/php-cs-fixer: ^3.75
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- typo3/cms-backend: ^14.3
- typo3/cms-dashboard: ^14.3
- typo3/cms-filelist: ^14.3
- typo3/cms-fluid-styled-content: ^14.3
- typo3/cms-install: ^14.3
- typo3/cms-lowlevel: ^14.3
- typo3/cms-rte-ckeditor: ^14.3
- typo3/cms-tstemplate: ^14.3
- typo3/coding-standards: ^0.8
- typo3/testing-framework: ^9.2
Suggests
- typo3/cms-dashboard: Adds a dashboard widget with the most viewed and most applied-to jobs
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-06 12:04:52 UTC
README
A filterable job list, a detail view, and schema.org/JobPosting structured data
that passes Google's Rich Results Test — without an extra mapping layer.
Why another jobs extension
Most job extensions bolt structured data on afterwards and then fight the
vocabulary: an employmentType select that stores Vollzeit cannot become
FULL_TIME without a translation table, and educationRequirements free text is
silently ignored by Google.
Here the data model is the vocabulary. The select values are PHP backed enums whose backing values are the literal, case-sensitive schema.org values, and the TCA items are generated from those enums. A wrong value is structurally impossible rather than caught in review.
Requirements
| TYPO3 | 14.3 LTS or newer |
| PHP | 8.2 – 8.5 |
Installation
composer require bastianschwabe/jobs
Then assign the Jobs site set to your site and point it at a storage folder.
Quick start
- Create a storage folder and add records: Level, Field of work, Location, Contact person, Company profile, then Job.
- Add the Jobs: List plugin to your overview page and the Jobs: Detail plugin to a detail page.
- In the site settings, set the detail page and — important — the fallback
organization, so a job without a company profile still emits a valid
hiringOrganization.
Frontend
The plugins ship with a finished design: system fonts, neutral grays, no
external assets, so it blends into most sites as it is. Every color, radius
and the font are CSS custom properties on the .jobs wrapper:
.jobs { --jobs-font-family: inherit; --jobs-color-accent: #0a3d62; --jobs-radius: 0; }
The site setting Stylesheet (jobs.stylesheet) switches between
default, basic (structure only, no colors or rounded corners) and none.
The markup and its BEM class names stay the same in all three modes.
Structured data
The detail view emits a single <script type="application/ld+json"> block.
Optional properties are omitted rather than emitted empty, because Google treats
an empty value as malformed while a missing one is merely absent.
Covered properties:
- Required:
title,description,datePosted,hiringOrganization,jobLocation - Recommended:
validThrough,employmentType,baseSalary,identifier,directApply,educationRequirements,experienceRequirements,industry,occupationalCategory,image,url - Remote work:
jobLocationType: TELECOMMUTEplusapplicantLocationRequirements
Some values are inherited so editors do not repeat themselves: a job without
experienceMonths takes them from its Level, and industry /
occupationalCategory fall back to its Field of work.
Validate your output with the Rich Results Test and the Schema Markup Validator.
Tracking and statistics
The frontend records three things: views per job and session, clicks on the Apply now link, and every filter combination visitors submit. One switch in the extension configuration turns all of it off. The backend shows the data in a module group Jobs:
- Jobs — every job in the installation in one table, filterable by access state, with the view count in the last column.
- Statistics — top 5 by views and by applications, a table per job, the grouped filter combinations, and a Reset all statistics button.
With typo3/cms-dashboard installed, a Job statistics widget shows the two
charts on the dashboard. The apply link goes through a small proxy action, so
add /{job-slug}/apply to your route enhancer for speaking URLs; the
documentation has the full example. Only a
hash of the frontend session id is stored — no IP address, no user agent.
Notes on the TYPO3 v14 baseline
This extension targets v14 only, which keeps it free of compatibility layers:
- Almost no
ext_tables.sql— every record table, column and MM table is derived from TCA. Declared by hand are only the pair of geo coordinates (TCA typenumberwith formatdecimalis hard-wired to two decimals, about a kilometre of error) and the three tracking tables, which have no TCA on purpose.Tests/Functional/Database/SchemaTest.phpkeeps both halves honest. - No
ext_tables.php, which is deprecated as of v14.3. - Plugins are their own
CType;list_typewas removed in v14.0. - Labels use v14 translation domains (
jobs.db:…) instead of longLLL:EXT:paths. - Configuration ships as a site set, not as static TypoScript includes.
- Relations, dates and structured-data values carry
l10n_mode: exclude, so a translated job keeps thehiringOrganizationandjobLocationGoogle requires instead of inheriting an empty translation.
Development
The repository carries its own TYPO3 instance: DDEV, a development-only site package, and a seed command that builds the demo content.
ddev start
ddev typo3-install
That is a full reset — it drops the database, installs TYPO3, creates the page
tree and demo records, and prints the URLs. Afterwards a plain ddev start is
enough; the database persists.
| Backend | /typo3 — test / test |
| Job list | /jobs (English), /de/stellenangebote (German) |
| Job detail | /jobs/detail/<slug>, speaking URLs via route enhancer |
The demo data is deliberately varied: a job with a location and a salary range, one with a fixed salary and no expiry date, a fully remote one without any location, and one left untranslated so the German language fallback is visible.
DEVELOPMENT.md documents the setup in full — how the pieces fit together, what the install command does step by step, how to extend the demo data, and the handful of TYPO3 quirks that cost us a debugging round each.
Tests and static analysis
ddev composer ci
That runs, in order: PHP lint, coding standards, PHPStan level 8, unit tests and
functional tests. Functional tests default to SQLite and need no Docker, so
composer test:functional works straight from the host; CI runs the same suite
across PHP 8.2 to 8.5 and once more against MariaDB.
License
GPL-2.0-or-later. Author: Bastian Schwabe.