If you run a job board, standard SEO rules do not apply.
Normally, you submit a sitemap, wait a few days or weeks, and eventually, Google indexers find your page. But for job boards, this standard cycle is a death sentence. By the time Google crawls your new posting, the role is often closed, filled, or stale.
To show up in the premium Google for Jobs search widget (which drives the majority of organic job search traffic), you need two things: strictly validated schema data and real-time indexing.
In this guide, we'll walk through the exact schema pitfalls that keep job postings out of Google Search, how to format remote jobs correctly, and how to integrate the Google Indexing API to get new postings live in minutes.
1. Schema Pitfalls That Get Your Jobs Rejected
Google reads job postings through the JobPosting JSON-LD schema. If your schema has even one error or missing recommended property, Google Search Console (GSC) will flag it, and Google for Jobs might ignore your listing entirely.
Here are the four biggest schema mistakes and how to fix them.
Pitfall #1: Setting jobLocation on 100% Remote Jobs
Google has strict guidelines for remote jobs. If a job is fully remote (work from home), Google's guidelines state you must omit the jobLocation property entirely.
If you specify both jobLocation (e.g., pointing to a physical address) and jobLocationType: "TELECOMMUTE", Google's parser gets confused. As a result, your listing won't show up when users apply the "Remote" filter.
The Correct Way to Define Remote Jobs:
{
"@context": "https://schema.org",
"@type": "JobPosting",
"title": "Senior React Developer",
"jobLocationType": "TELECOMMUTE",
"applicantLocationRequirements": {
"@type": "Country",
"name": "IN"
}
}
Pitfall #2: Using Country Names instead of ISO 2-Letter Codes
For both addressCountry (inside jobLocation) and applicantLocationRequirements, Google requires country codes to follow the ISO 3166-1 alpha-2 standard.
Passing "India" or "United States" is one of the most common validation warnings. You must map country names to their respective two-letter codes:
const COUNTRY_TO_ISO: Record<string, string> = {
india: 'IN',
'united states': 'US',
usa: 'US',
'united kingdom': 'GB',
uk: 'GB',
canada: 'CA',
germany: 'DE'
}
function toIsoCountry(countryName: string): string {
return COUNTRY_TO_ISO[countryName.trim().toLowerCase()] ?? countryName.trim();
}
Pitfall #3: Sending Plain Text Instead of HTML Descriptions
Google's job widget displays the description field exactly as it's provided in your schema. If you strip all formatting to a plain-text string, your job post will look like a massive, unreadable wall of text.
Google for Jobs explicitly supports basic HTML tags in the schema description: <p>, <ul>, <li>, <strong>, <em>, and <br/>.
Instead of stripping markdown, convert it to basic HTML before injecting it into the JSON-LD script:
function markdownToBasicHtml(text: string): string {
if (!text) return '';
let html = text.replace(/\r\n/g, '\n').trim();
// Convert Markdown syntax to basic HTML tags
html = html
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/^#{1,6}\s+(.*?)$/gm, '<strong>$1</strong>');
// Map bullet points to <ul> and <li> tags
const lines = html.split('\n');
const result: string[] = [];
let inList = false;
for (const line of lines) {
const trimmed = line.trim();
const listMatch = trimmed.match(/^[-*+]\s+(.*)$/);
if (listMatch) {
if (!inList) {
result.push('<ul>');
inList = true;
}
result.push(`<li>${listMatch[1]}</li>`);
} else {
if (inList) {
result.push('</ul>');
inList = false;
}
if (trimmed === '') {
result.push('<br/>');
} else {
result.push(`<p>${trimmed}</p>`);
}
}
}
if (inList) result.push('</ul>');
return result.join('\n');
}
Pitfall #4: Missing the validThrough Field
Google requires or highly recommends a validThrough date (expiration date). If you leave this out, Google Search Console will flag your listings with warnings.
If your database doesn't store an explicit expiration date, derive one dynamically (e.g., datePosted + 60 days) so Google knows the listing is fresh:
const validThrough = job.valid_through
?? new Date(new Date(job.date_posted).getTime() + 60 * 24 * 60 * 60 * 1000)
.toISOString().split('T')[0];
2. Pushing Updates in Real-Time: Google Indexing API
Even with perfect schema, waiting for Google's bot to find your sitemaps is too slow. To get jobs listed instantly, you must use the Google Indexing API.
The Indexing API allows you to notify Google immediately when a page is added, updated, or removed, forcing Googlebot to schedule a crawl within minutes.
Setting Up the Indexing API in 4 Steps:
- Enable the API: Go to the Google Cloud Console, select your project, and search for the Web Search Indexing API. Click Enable.
- Create a Service Account: Go to IAM & Admin > Service Accounts, create a service account, and download its credentials as a JSON private key file.
- Add as Property Owner (Crucial): Open Google Search Console. Go to Settings > Users and permissions. Click Add User, enter the Service Account email address, and select Owner permissions. (Note: The Indexing API will reject requests with a 403 error if the service account has only 'Full' or 'Viewer' access. It must be an Owner).
- Deploy Credentials: Save the JSON key securely in your environment variables (e.g.,
GOOGLE_INDEXING_KEY_JSON).
Node.js Script to Notify Google:
Below is a complete, dependency-free Node.js script to authorize your service account using JWT and notify Google of new job URLs:
import { createSign } from 'crypto';
function base64url(str) {
return Buffer.from(str).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
// Generate JWT token to authenticate with Google APIs
async function getAccessToken(serviceAccount) {
const now = Math.floor(Date.now() / 1000);
const header = base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const claim = base64url(JSON.stringify({
iss: serviceAccount.client_email,
scope: 'https://www.googleapis.com/auth/indexing',
aud: 'https://oauth2.googleapis.com/token',
exp: now + 3600,
iat: now,
}));
const signingInput = `${header}.${claim}`;
const sign = createSign('RSA-SHA256');
sign.update(signingInput);
const signature = sign.sign(serviceAccount.private_key, 'base64')
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
const jwt = `${signingInput}.${signature}`;
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt
}),
});
const data = await res.json();
return data.access_token;
}
// Notify Google Search index of new or updated URL
export async function notifyGoogle(url, keyJson) {
const serviceAccount = JSON.parse(keyJson);
const token = await getAccessToken(serviceAccount);
const res = await fetch('https://indexing.googleapis.com/v3/urlNotifications:publish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ url, type: 'URL_UPDATED' }),
});
if (!res.ok) {
const errorData = await res.json();
throw new Error(`Google API error: ${errorData.error?.message}`);
}
return true;
}
3. The "Email Not Found" Search Console Workaround
When adding your Service Account email (e.g., bot@my-project.iam.gserviceaccount.com) in Search Console, Google often returns a frustrating "Email not found" error. This is a known, long-standing bug in the modern Search Console interface.
The Legacy Workaround:
If the modern interface fails, use Google's legacy Webmaster Central portal:
- Visit the Legacy Webmaster Verification Portal.
- Click on your website property.
- Scroll to the bottom of the page and click Add an Owner.
- Paste the Service Account email address and click Continue.
This legacy endpoint bypasses the modern email validation check and immediately registers your service account as a verified owner.
Google for Jobs SEO Checklist
Before launching your job board, run a quick checklist:
- Fully remote jobs have
jobLocationomitted. - Country codes are standard 2-letter ISO codes (
IN,US,GB). - Job descriptions are formatted using basic HTML tags in schema.
-
validThroughdate is set (explicitly or derived). - Google Indexing API service account is set up with Owner access.
- New URLs trigger an Indexing API call immediately upon publish.
By implementing these fixes, we aligned OnlyFrontendJobs' SEO structure to match and exceed the top players in the ecosystem. Your jobs will get indexed in minutes, maintain clean layout structures in Google's widgets, and correctly capture remote-work traffic.
