-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(lbac-2194): sitemap avec les offres (#1701)
* feat(lbac-2194): sitemap avec les offres * fix: suppression commentaire * fix: mise en place du cron + non remplacement si pas de changement * fix: build
- Loading branch information
1 parent
84f49e7
commit e846683
Showing
17 changed files
with
316 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { zRoutes } from "shared/index" | ||
|
||
import dayjs from "@/services/dayjs.service" | ||
import { getSitemap } from "@/services/sitemap.service" | ||
|
||
import { Server } from "../server" | ||
|
||
export default function (server: Server) { | ||
server.get( | ||
"/sitemap-offers.xml", | ||
{ | ||
schema: zRoutes.get["/sitemap-offers.xml"], | ||
}, | ||
async (_req, res) => { | ||
const sitemap = await getSitemap() | ||
const lastModified = dayjs(sitemap.created_at).utc().toString() | ||
return res | ||
.status(200) | ||
.headers({ | ||
"content-type": "text/xml", | ||
"last-modified": lastModified, | ||
}) | ||
.send(sitemap.xml) | ||
} | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { ObjectId } from "mongodb" | ||
import { RECRUITER_STATUS } from "shared/constants" | ||
import { IJob, IRecruiter, JOB_STATUS } from "shared/models" | ||
import { ISitemap } from "shared/models/sitemap.model" | ||
import { hashcode } from "shared/utils" | ||
import { generateSitemapFromUrlEntries } from "shared/utils/sitemapUtils" | ||
|
||
import { logger } from "@/common/logger" | ||
import { getDbCollection } from "@/common/utils/mongodbUtils" | ||
import { notifyToSlack } from "@/common/utils/slackUtils" | ||
import config from "@/config" | ||
import dayjs from "@/services/dayjs.service" | ||
|
||
type AggregateRecruiter = Pick<Omit<IRecruiter, "jobs">, "updatedAt"> & { | ||
jobs: Pick<IJob, "job_update_date" | "_id"> | ||
} | ||
|
||
const generateSitemapXml = async () => { | ||
const documents = (await getDbCollection("recruiters") | ||
.aggregate([ | ||
{ $match: { status: RECRUITER_STATUS.ACTIF, "jobs.job_status": JOB_STATUS.ACTIVE } }, | ||
{ $unwind: { path: "$jobs" } }, | ||
{ $match: { "jobs.job_status": JOB_STATUS.ACTIVE } }, | ||
{ $project: { updatedAt: 1, "jobs.job_update_date": 1, "jobs._id": 1 } }, | ||
]) | ||
.limit(Number.MAX_SAFE_INTEGER) | ||
.toArray()) as AggregateRecruiter[] | ||
|
||
const sitemap = generateSitemapFromUrlEntries( | ||
documents.map((document) => { | ||
const { jobs: job, updatedAt } = document | ||
const { job_update_date, _id } = job | ||
const lastMod = job_update_date && dayjs(updatedAt).isBefore(job_update_date) ? job_update_date : updatedAt | ||
const url = `${config.publicUrl}/recherche-apprentissage?type=matcha&itemId=${_id}` | ||
return { | ||
loc: url, | ||
lastmod: lastMod, | ||
changefreq: "daily", | ||
} | ||
}) | ||
) | ||
return { xml: sitemap, count: documents.length } | ||
} | ||
|
||
export const generateSitemap = async () => { | ||
const { xml, count } = await generateSitemapXml() | ||
const hash = hashcode(xml) | ||
let dbSitemap = await getDbCollection("sitemaps").findOne({ hashcode: hash.toString() }) | ||
if (!dbSitemap) { | ||
await getDbCollection("sitemaps").deleteMany({}) | ||
dbSitemap = { | ||
_id: new ObjectId(), | ||
created_at: new Date(), | ||
xml: xml, | ||
hashcode: hash.toString(), | ||
} | ||
await getDbCollection("sitemaps").insertOne(dbSitemap) | ||
const sizeInMo = xml.length / (1024 * 1024) | ||
const message = `Generated sitemap with ${count} offers. size: ~${sizeInMo.toFixed(1)} Mo. Max 50 Mo` | ||
logger.info(message) | ||
await notifyToSlack({ | ||
subject: "job de génération du sitemap des offres", | ||
message, | ||
error: count === 0 || sizeInMo > 40, | ||
}) | ||
} | ||
} | ||
|
||
export const getSitemap = async (): Promise<ISitemap> => { | ||
let dbSitemap = await getDbCollection("sitemaps").findOne({}) | ||
if (!dbSitemap) { | ||
// should not happen but added for safety | ||
const { xml } = await generateSitemapXml() | ||
dbSitemap = { | ||
_id: new ObjectId(), | ||
created_at: new Date(), | ||
xml, | ||
hashcode: hashcode(xml).toString(), | ||
} | ||
await getDbCollection("sitemaps").insertOne(dbSitemap) | ||
await notifyToSlack({ | ||
subject: "job de génération du sitemap des offres", | ||
message: `Inattendu : génération du sitemap par le backend alors qu il aurait dû être généré par le job processor !`, | ||
error: true, | ||
}) | ||
} | ||
return dbSitemap | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { z } from "../helpers/zodWithOpenApi" | ||
|
||
import { IModelDescriptor, zObjectId } from "./common" | ||
|
||
export const ZSitemap = z | ||
.object({ | ||
_id: zObjectId, | ||
created_at: z.date().describe("Date d'ajout en base de données"), | ||
xml: z.string(), | ||
hashcode: z.string().describe("hashcode du xml"), | ||
}) | ||
.strict() | ||
|
||
export type ISitemap = z.output<typeof ZSitemap> | ||
|
||
export default { | ||
zod: ZSitemap, | ||
indexes: [], | ||
collectionName: "sitemaps" as const, | ||
} as const satisfies IModelDescriptor |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { z } from "../helpers/zodWithOpenApi" | ||
|
||
import { IRoutesDef } from "./common.routes" | ||
|
||
export const zSitemapRoutes = { | ||
get: { | ||
"/sitemap-offers.xml": { | ||
method: "get", | ||
path: "/sitemap-offers.xml", | ||
response: { | ||
"200": z.string(), | ||
}, | ||
securityScheme: null, | ||
}, | ||
}, | ||
} as const satisfies IRoutesDef |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
export type SitemapUrlEntry = { | ||
loc: string | ||
lastmod?: Date | ||
changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never" | ||
priority?: number | ||
} | ||
|
||
const xmlEncode = (text: string): string => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'") | ||
|
||
export const generateSitemapFromUrlEntries = (urlEntries: SitemapUrlEntry[]) => `<?xml version="1.0" encoding="UTF-8"?> | ||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> | ||
${urlEntries | ||
.map((urlEntry) => { | ||
const { loc, changefreq, lastmod, priority } = urlEntry | ||
const fields = [ | ||
`<loc>${xmlEncode(loc)}</loc>`, | ||
lastmod ? `<lastmod>${lastmod.toISOString()}</lastmod>` : "", | ||
changefreq ? `<changefreq>${changefreq}</changefreq>` : "", | ||
priority !== undefined ? `<priority>${priority}</priority>` : "", | ||
] | ||
return `<url>${fields.filter((x) => x).join(`\n`)} | ||
</url>` | ||
}) | ||
.join("")} | ||
</urlset>` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,19 @@ | ||
export const removeAccents = (str: string) => str.normalize("NFD").replace(/[\u0300-\u036f]/g, "") | ||
export const removeRegexChars = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "") | ||
|
||
export const joinNonNullStrings = (values: (string | null | undefined)[]): string | null => { | ||
const result = values.flatMap((item) => (item && item.trim() ? [item.trim()] : [])).join(" ") | ||
return result || null | ||
} | ||
|
||
// cf https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript | ||
export const hashcode = (str: string) => { | ||
let hash = 0 | ||
if (str.length === 0) return hash | ||
for (let i = 0; i < str.length; i++) { | ||
const chr = str.charCodeAt(i) | ||
hash = (hash << 5) - hash + chr | ||
hash |= 0 // Convert to 32bit integer | ||
} | ||
return hash | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { publicConfig } from "@/config.public" | ||
import { mainSitemapLastModificationDate } from "@/services/generateMainSitemap" | ||
import { getHostFromHeader } from "@/utils/requestUtils" | ||
|
||
export async function GET(request: Request) { | ||
const sitemap = await generateSiteMap(request) | ||
|
||
return new Response(sitemap, { | ||
status: 200, | ||
headers: { | ||
"Content-Type": "text/xml", | ||
}, | ||
}) | ||
} | ||
|
||
async function generateSiteMap(request: Request) { | ||
const host = getHostFromHeader(request) | ||
const response = await fetch(`${publicConfig.apiEndpoint}/sitemap-offers.xml`, { | ||
cache: "no-cache", | ||
}) | ||
const lastModifiedHeader = response.headers.get("last-modified") | ||
const offersLastMod = new Date(lastModifiedHeader).toISOString() | ||
|
||
return `<?xml version="1.0" encoding="UTF-8"?> | ||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> | ||
<sitemap> | ||
<loc>${host}/sitemap-main.xml</loc> | ||
<lastmod>${mainSitemapLastModificationDate.toISOString()}</lastmod> | ||
</sitemap> | ||
<sitemap> | ||
<loc>${host}/sitemap-offers.xml</loc> | ||
<lastmod>${offersLastMod}</lastmod> | ||
</sitemap> | ||
</sitemapindex>` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { generateMainSitemap } from "@/services/generateMainSitemap" | ||
|
||
export async function GET(request: Request) { | ||
const sitemap = generateMainSitemap(request) | ||
return new Response(sitemap, { | ||
status: 200, | ||
headers: { | ||
"Content-Type": "text/xml", | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { publicConfig } from "@/config.public" | ||
|
||
// disable next cache. Cache is handled in the API | ||
export const dynamic = "force-dynamic" | ||
|
||
export async function GET(_request: Request) { | ||
const response = await fetch(`${publicConfig.apiEndpoint}/sitemap-offers.xml`, { | ||
cache: "no-cache", | ||
}) | ||
const xml = await response.text() | ||
return new Response(xml, { | ||
status: 200, | ||
headers: { | ||
"content-type": "text/xml", | ||
"last-modified": response.headers.get("last-modified"), | ||
}, | ||
}) | ||
} |
Oops, something went wrong.