-
Notifications
You must be signed in to change notification settings - Fork 489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP create product engineer job board #9943
Changes from all commits
8c41418
3526c18
d823214
0561287
074cae1
22f5281
48e95d2
7936601
01883d1
53babe2
dee1147
39942fb
817549b
60d4500
ef9b6a3
d7a49d4
b8c9cb8
3b76f93
2d7218b
6865ea4
0d81563
4029f24
efb2753
85a6659
dd8e7e9
b44a5f7
033281f
f1f063b
d2d07c0
2a34a60
fbdcd79
9d22926
fc65544
f63c138
1c7292e
b29ce5e
a48fef0
04d8bf6
26502a6
2bed784
cc72320
a429a38
6f721e9
1ca8838
166e585
28244ab
a78ec0c
c847ef9
9fac7f6
ba8ae89
7f56a46
e70139b
a0a0ca1
b6c14dc
393382e
bb1a408
fbbbda1
6646db1
437917d
f95f8f1
2cc9cbd
9cc7fb1
ddef66b
5a43cb3
2912e6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,4 +30,5 @@ yarn.lock | |
src/components/Layout/Fonts.scss | ||
.vercel | ||
.env | ||
*Type.ts | ||
*Type.ts | ||
.env.development |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
--- | ||
title: Managing cool tech jobs | ||
--- | ||
|
||
### Create a company/jobs: | ||
|
||
- Login to PostHog.com as a moderator | ||
- Navigate to `/cool-tech-jobs` | ||
- Click “Add a company” | ||
- Fill out the fields in the side modal | ||
- Click Create company | ||
|
||
When a company is created, its jobs are automatically scraped based on the job board URL/slug provided. If no jobs are found, the company doesn’t appear on `/cool-tech-jobs` (unless you’re a moderator, in which case it will appear semi-transparent). | ||
|
||
### Edit a company | ||
|
||
- Login to PostHog.com as a moderator | ||
- Navigate to `/cool-tech-jobs` | ||
- Click “Edit” under the desired company | ||
- Fill out the fields in the side modal | ||
- Click Update company | ||
|
||
Jobs will be re-scraped when a company is edited. | ||
|
||
### Delete a company | ||
|
||
- Login to PostHog.com as a moderator | ||
- Navigate to `/cool-tech-jobs` | ||
- Click “Delete” under the desired company | ||
- Confirm that you want to delete that company | ||
|
||
All jobs associated with the deleted company will be deleted along with the original company record. | ||
|
||
### Company fields | ||
|
||
- **Company name** | ||
- **Company website URL** - Used for the “Learn more” link | ||
- **Job board type** (Ashby, Greenhouse, Other) - If “Other” is selected, a custom scraper will need to be built. When the company is published, it will be hidden as no jobs will be scraped. | ||
- **Job board URL** (if Job board type is set to “Other”) - we’ll use this to build a custom scraper | ||
- **Job board slug** (if Job board type is *not* “Other”) - the job board’s slug. This is automatically created as you type the company name, as these usually mirror each other. Must be unique and is checked for uniqueness as you type. | ||
- **Company perks** | ||
- **Company logos** - SVG/PNG only | ||
|
||
Unless required conditionally (job board URL/slug), every company field is required. | ||
|
||
### Scraping | ||
|
||
Jobs are scraped hourly based on the provided job board URL/slug. Jobs are individually checked for freshness hourly. If a job URL 404s, it is deleted. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import axios from 'axios' | ||
|
||
// Helper function to get company logo URL | ||
async function getCompanyLogo(company: string): Promise<string | null> { | ||
try { | ||
// Try to get the company's homepage | ||
const response = await axios.get(`https://${company}.com`) | ||
const baseUrl = `https://${company}.com` | ||
|
||
// Try favicon patterns first | ||
const faviconPatterns = [ | ||
/<link[^>]*rel="icon"[^>]*href="([^"]*)"/, | ||
/<link[^>]*rel="shortcut icon"[^>]*href="([^"]*)"/, | ||
/<link[^>]*rel="favicon"[^>]*href="([^"]*)"/, | ||
] | ||
|
||
// Helper function to resolve relative URLs | ||
const resolveUrl = (path: string): string => { | ||
if (path.startsWith('http')) return path | ||
if (path.startsWith('//')) return 'https:' + path | ||
if (path.startsWith('/')) return baseUrl + path | ||
return baseUrl + '/' + path | ||
} | ||
|
||
// Try favicons | ||
for (const pattern of faviconPatterns) { | ||
const match = response.data.match(pattern) | ||
if (match && match[1]) { | ||
const faviconUrl = resolveUrl(match[1]) | ||
console.log(`Found favicon for ${company}:`, faviconUrl) | ||
|
||
// Verify the favicon URL is accessible | ||
try { | ||
await axios.head(faviconUrl) | ||
return faviconUrl | ||
} catch (e) { | ||
console.log(`Favicon not accessible for ${company}, trying next option`) | ||
continue | ||
} | ||
} | ||
} | ||
|
||
// If no favicon found or accessible, try OpenGraph image | ||
const ogImageMatch = response.data.match(/<meta[^>]*property="og:image"[^>]*content="([^"]*)"/) | ||
if (ogImageMatch && ogImageMatch[1]) { | ||
const ogImageUrl = resolveUrl(ogImageMatch[1]) | ||
console.log(`Found OG image for ${company}:`, ogImageUrl) | ||
|
||
// Verify the OG image URL is accessible | ||
try { | ||
await axios.head(ogImageUrl) | ||
return ogImageUrl | ||
} catch (e) { | ||
console.log(`OG image not accessible for ${company}, trying next option`) | ||
} | ||
} | ||
|
||
// Try direct favicon.ico as last resort | ||
const directFaviconUrl = `${baseUrl}/favicon.ico` | ||
try { | ||
await axios.head(directFaviconUrl) | ||
console.log(`Found direct favicon.ico for ${company}:`, directFaviconUrl) | ||
return directFaviconUrl | ||
} catch (error) { | ||
console.log(`No direct favicon.ico found for ${company}`) | ||
} | ||
|
||
console.log(`No logo found for ${company}`) | ||
return null | ||
} catch (error) { | ||
console.error(`Error fetching logo for ${company}:`, error) | ||
return null | ||
} | ||
} | ||
|
||
/** | ||
* @param {import('next').NextApiRequest} req | ||
* @param {import('next').NextApiResponse} res | ||
*/ | ||
export default async function scrape(req, res) { | ||
if (req.method !== 'GET') { | ||
res.setHeader('Allow', 'GET') | ||
res.status(405).json({ success: false, message: 'Method not allowed' }) | ||
return | ||
} | ||
|
||
const company = req.query.company || 'supabase' | ||
|
||
try { | ||
// Get jobs data | ||
const response = await axios.get(`https://jobs.ashbyhq.com/${company}`) | ||
const match = response.data.match(/window\.__appData\s*=\s*({[^;]+})/) | ||
if (!match) { | ||
throw new Error(`Could not find job data in response for ${company}`) | ||
} | ||
|
||
const jobData = JSON.parse(match[1]) | ||
const jobs = jobData?.jobBoard?.jobPostings || [] | ||
|
||
// Try to get company logo | ||
const logo = await getCompanyLogo(company) | ||
|
||
const jobsWithLogo = jobs.map((job) => ({ | ||
...job, | ||
company, | ||
logo, | ||
link: `https://jobs.ashbyhq.com/${company}/${job.id}`, | ||
})) | ||
|
||
res.status(200).json({ | ||
success: true, | ||
jobs: jobsWithLogo, | ||
}) | ||
} catch (error) { | ||
console.error(`API Error for ${company}:`, error) | ||
res.status(500).json({ | ||
success: false, | ||
message: `Error scraping jobs for ${company}`, | ||
details: error.message, | ||
}) | ||
} | ||
} | ||
|
||
const ATS_PATTERNS = [ | ||
{ | ||
name: 'Greenhouse', | ||
pattern: /greenhouse\.io/i, | ||
boardPattern: /greenhouse\.io\/[^/]+$/i, | ||
}, | ||
Comment on lines
+125
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Greenhouse API:
|
||
{ | ||
name: 'Lever', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lever API
|
||
pattern: /lever\.co/i, | ||
boardPattern: /lever\.co\/[^/]+$/i, | ||
}, | ||
{ | ||
name: 'Workday', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Workday is slightly trickier since there are so many jobs.
|
||
pattern: /myworkdayjobs\.com/i, | ||
boardPattern: /myworkdayjobs\.com\/[^/]+$/i, | ||
}, | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { NextApiRequest, NextApiResponse } from 'next' | ||
import { findNewAshbyJobs, addNewCompaniesToConfig } from '../components/Jobs/jobsAshby' | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
const logs: string[] = [] | ||
|
||
// Capture console.log output | ||
const originalLog = console.log | ||
console.log = (...args) => { | ||
logs.push(args.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg)).join(' ')) | ||
originalLog.apply(console, args) | ||
} | ||
|
||
try { | ||
const companies = await findNewAshbyJobs() | ||
const addedCompanies = await addNewCompaniesToConfig() | ||
|
||
// Restore original console.log | ||
console.log = originalLog | ||
|
||
res.status(200).json({ | ||
debug: { | ||
nodeEnv: process.env.NODE_ENV, | ||
hasApiKey: !!process.env.GATSBY_SCRAPING_BEE_API_KEY, | ||
apiKeyFirstChars: process.env.GATSBY_SCRAPING_BEE_API_KEY | ||
? process.env.GATSBY_SCRAPING_BEE_API_KEY.substring(0, 4) + '...' | ||
: 'none', | ||
logs: logs, | ||
}, | ||
companiesFound: companies, | ||
companiesAdded: addedCompanies, | ||
}) | ||
} catch (error) { | ||
// Restore original console.log | ||
console.log = originalLog | ||
|
||
res.status(500).json({ | ||
error: 'Failed to fetch companies', | ||
details: error.message, | ||
debug: { | ||
nodeEnv: process.env.NODE_ENV, | ||
hasApiKey: !!process.env.GATSBY_SCRAPING_BEE_API_KEY, | ||
logs: logs, | ||
errorMessage: error.message, | ||
errorStack: error.stack, | ||
}, | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ashby has an api you can use, which just returns a JSON. Could be simpler: