Skip to content

Commit 553a880

Browse files
fix(events): dedupe generated content
1 parent c3017ee commit 553a880

10 files changed

Lines changed: 137 additions & 125 deletions

.github/workflows/deploy-pages.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ name: Deploy Pages
33
on:
44
push:
55
branches:
6-
- main
6+
- main
77
workflow_dispatch:
88
schedule:
9-
- cron: "30 16 * * *"
10-
9+
- cron: '30 16 * * *'
1110

1211
permissions:
1312
contents: write

public/vigotech-generated.json

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,13 +1195,6 @@
11951195
}
11961196
],
11971197
"eventList": [
1198-
{
1199-
"sourceId": "AIndustriosa-314059100",
1200-
"title": "Introdución ao procesado de audio con STM32 e I2S",
1201-
"date": 1776499200000,
1202-
"url": "https://www.meetup.com/aindustriosa/events/314059100/",
1203-
"location": "Vigo - es"
1204-
},
12051198
{
12061199
"sourceId": "aindustriosa-1776499200000-introducion-ao-procesado-de-audio-con-stm32-e-i2s",
12071200
"title": "Introdución ao procesado de audio con STM32 e I2S",
@@ -1210,13 +1203,6 @@
12101203
"location": "Vigo, es",
12111204
"description": "O vindeiro **18 de abril en A Industriosa** teremos unha sesión práctica para achegarnos ao mundo do procesado de audio dixital con microcontroladores **ST"
12121205
},
1213-
{
1214-
"sourceId": "AIndustriosa-314167108",
1215-
"title": "Pensaches algunha vez en crear unha biblioteca de compoñentes UI?",
1216-
"date": 1776965400000,
1217-
"url": "https://www.meetup.com/aindustriosa/events/314167108/",
1218-
"location": "A Industriosa - Avda. Gregorio Espino 38 entresuelo 3, Vigo, pw - Vigo - pw - es"
1219-
},
12201206
{
12211207
"sourceId": "aindustriosa-1776965400000-pensaches-algunha-vez-en-crear-unha-biblioteca-de-componentes-ui",
12221208
"title": "Pensaches algunha vez en crear unha biblioteca de compoñentes UI?",
@@ -1225,13 +1211,6 @@
12251211
"location": "A Industriosa, Avda. Gregorio Espino 38 entresuelo 3, Vigo, pw, Vigo, pw, es",
12261212
"description": "Queres saber todo o que necesitas para facelo?\nO vindeiro **18 de abril ás 19:30 en A Industriosa**, **Sergio Carracedo** explicará dende o principio:\n\n* *"
12271213
},
1228-
{
1229-
"sourceId": "AIndustriosa-314059417",
1230-
"title": "Ciberseguridade orientada ao ámbito doméstico",
1231-
"date": 1777109400000,
1232-
"url": "https://www.meetup.com/aindustriosa/events/314059417/",
1233-
"location": "Vigo - es"
1234-
},
12351214
{
12361215
"sourceId": "aindustriosa-1777109400000-ciberseguridade-orientada-ao-ambito-domestico",
12371216
"title": "Ciberseguridade orientada ao ámbito doméstico",
@@ -3707,7 +3686,13 @@
37073686
}
37083687
}
37093688
],
3710-
"eventList": []
3689+
"eventList": [
3690+
{
3691+
"date": 1750356000000,
3692+
"title": "Reunión junio 2025",
3693+
"url": "https://www.python-vigo.es/posts/reunion-junio-2025/"
3694+
}
3695+
]
37113696
},
37123697
"seogalicia": {
37133698
"name": "Seo Galicia",
@@ -5625,13 +5610,6 @@
56255610
}
56265611
],
56275612
"eventList": [
5628-
{
5629-
"sourceId": "Vigo-WordPress-Meetup-314167142",
5630-
"title": "Marketing4eCommerce: Cómo crear unha comunidade masiva e internacional con WP",
5631-
"date": 1776963600000,
5632-
"url": "https://www.meetup.com/vigo-wordpress-meetup/events/314167142/",
5633-
"location": "Círculo de Empresarios de Galicia - Rúa de García Barbón, 62, Vigo, Po - Vigo - Po - es"
5634-
},
56355613
{
56365614
"sourceId": "vigowordpress-1776963600000-marketing4ecommerce-como-crear-unha-comunidade-masiva-e-internacional-con-wp",
56375615
"title": "Marketing4eCommerce: Cómo crear unha comunidade masiva e internacional con WP",

scripts/generate-vigotech-json.mjs

Lines changed: 116 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
1+
import { mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises'
22
import { dirname, resolve } from 'node:path'
33
import { fileURLToPath } from 'node:url'
44
import { createRequire } from 'node:module'
@@ -110,7 +110,15 @@ const ensureDirectory = async (directoryPath) => {
110110
await mkdir(directoryPath, { recursive: true })
111111
}
112112

113-
const safeYamlScalar = (value) => JSON.stringify(value ?? '')
113+
const safeYamlScalar = (value) => {
114+
const text = String(value ?? '')
115+
116+
if (text.includes('\n')) {
117+
return JSON.stringify(text)
118+
}
119+
120+
return `'${text.replace(/'/g, "''")}'`
121+
}
114122

115123
const formatFrontmatterValue = (value, indent = 0) => {
116124
const prefix = ' '.repeat(indent)
@@ -192,6 +200,34 @@ const writeIfChanged = async (filePath, content) => {
192200
return true
193201
}
194202

203+
const pruneGeneratedMarkdownFiles = async (directoryPath, expectedFileNames) => {
204+
const expected = new Set(expectedFileNames)
205+
206+
let entries = []
207+
try {
208+
entries = await readdir(directoryPath, { withFileTypes: true })
209+
} catch {
210+
return 0
211+
}
212+
213+
let deletedFiles = 0
214+
215+
for (const entry of entries) {
216+
if (!entry.isFile() || !entry.name.endsWith('.md')) {
217+
continue
218+
}
219+
220+
if (expected.has(entry.name)) {
221+
continue
222+
}
223+
224+
await unlink(resolve(directoryPath, entry.name))
225+
deletedFiles += 1
226+
}
227+
228+
return deletedFiles
229+
}
230+
195231
const getVideoId = (video) =>
196232
video && typeof video === 'object' && typeof video.id === 'string' && video.id.length > 0
197233
? video.id
@@ -248,27 +284,78 @@ const sortVideosByDate = (videos) =>
248284
})
249285

250286
const dedupeEvents = (events) => {
251-
const orderedIds = []
252-
const eventsById = new Map()
287+
const orderedKeys = []
288+
const eventsByKey = new Map()
289+
290+
const getEventKey = (event) => {
291+
if (!event || typeof event !== 'object') {
292+
return null
293+
}
294+
295+
const link = typeof event.link === 'string' ? event.link.trim() : ''
296+
if (link) {
297+
return `link:${link}`
298+
}
299+
300+
const title = typeof event.title === 'string' ? normalizeForIdentity(event.title) : ''
301+
const date = typeof event.date === 'number' ? event.date : Number.NaN
302+
if (title && Number.isFinite(date)) {
303+
return `title-date:${title}:${date}`
304+
}
305+
306+
if (event?.sourceId) {
307+
return `source:${event.sourceId}`
308+
}
309+
310+
return null
311+
}
312+
313+
const choosePreferredEvent = (current, incoming) => {
314+
if (!current) {
315+
return incoming
316+
}
317+
318+
const currentDescription = typeof current.description === 'string' && current.description.trim()
319+
const incomingDescription =
320+
typeof incoming.description === 'string' && incoming.description.trim()
321+
const currentLocation = typeof current.location === 'string' && current.location.trim()
322+
const incomingLocation = typeof incoming.location === 'string' && incoming.location.trim()
323+
324+
if (!currentDescription && incomingDescription) {
325+
return { ...current, ...incoming }
326+
}
327+
328+
if (!currentLocation && incomingLocation) {
329+
return { ...current, ...incoming }
330+
}
331+
332+
return { ...incoming, ...current }
333+
}
253334

254335
for (const event of events) {
255-
if (!event?.sourceId) {
336+
const key = getEventKey(event)
337+
if (!key) {
256338
continue
257339
}
258340

259-
if (!eventsById.has(event.sourceId)) {
260-
orderedIds.push(event.sourceId)
341+
if (!eventsByKey.has(key)) {
342+
orderedKeys.push(key)
261343
}
262344

263-
eventsById.set(event.sourceId, {
264-
...eventsById.get(event.sourceId),
265-
...event,
266-
})
345+
eventsByKey.set(key, choosePreferredEvent(eventsByKey.get(key), event))
267346
}
268347

269-
return orderedIds.map((id) => eventsById.get(id))
348+
return orderedKeys.map((key) => eventsByKey.get(key))
270349
}
271350

351+
const normalizeForIdentity = (value) =>
352+
String(value ?? '')
353+
.normalize('NFKD')
354+
.replace(/[\u0300-\u036f]/g, '')
355+
.toLowerCase()
356+
.replace(/\s+/g, ' ')
357+
.trim()
358+
272359
const sortEventsByDate = (events) => [...events].sort((a, b) => a.date - b.date)
273360

274361
const getYoutubeUploadsPlaylistId = (channelId) => {
@@ -619,18 +706,21 @@ const writeVideoContentFiles = async (members) => {
619706
await ensureDirectory(groupDir)
620707

621708
const rawList = Array.isArray(member.videoList) ? member.videoList : []
709+
const expectedFileNames = []
710+
622711
for (const [index, videoRaw] of rawList.entries()) {
623712
const video = normalizeVideoEntry(groupId, groupName, groupLogo, videoRaw, index)
624713
if (!video) {
625714
continue
626715
}
627716

628-
const filePath = resolve(
629-
groupDir,
630-
`${toContentSlug(video.sourceId, `${groupId}-${index + 1}`)}.md`,
631-
)
717+
const fileName = `${toContentSlug(video.sourceId, `${groupId}-${index + 1}`)}.md`
718+
expectedFileNames.push(fileName)
719+
const filePath = resolve(groupDir, fileName)
632720
writtenFiles += Number(await writeIfChanged(filePath, buildFrontmatterDocument(video)))
633721
}
722+
723+
await pruneGeneratedMarkdownFiles(groupDir, expectedFileNames)
634724
}
635725

636726
return writtenFiles
@@ -644,8 +734,10 @@ const writeEventContentFiles = async (members, historicalEventsByGroup, rootEntr
644734
if (rootEntry) {
645735
const rootDir = resolve(generatedEventsDir, 'root')
646736
await ensureDirectory(rootDir)
647-
const filePath = resolve(rootDir, `${toContentSlug(rootEntry.sourceId, 'root-next-event')}.md`)
737+
const rootFileName = `${toContentSlug(rootEntry.sourceId, 'root-next-event')}.md`
738+
const filePath = resolve(rootDir, rootFileName)
648739
writtenFiles += Number(await writeIfChanged(filePath, buildFrontmatterDocument(rootEntry)))
740+
await pruneGeneratedMarkdownFiles(rootDir, [rootFileName])
649741
}
650742

651743
for (const [groupId, member] of Object.entries(members)) {
@@ -656,18 +748,21 @@ const writeEventContentFiles = async (members, historicalEventsByGroup, rootEntr
656748

657749
const events =
658750
historicalEventsByGroup.get(groupId) ?? getPersistedEventsForMember(groupId, member)
751+
const expectedFileNames = []
752+
659753
for (const [index, eventRaw] of events.entries()) {
660754
const event = normalizeEventEntry(groupId, groupName, groupLogo, eventRaw, index)
661755
if (!event) {
662756
continue
663757
}
664758

665-
const filePath = resolve(
666-
groupDir,
667-
`${toContentSlug(event.sourceId, `${groupId}-${index + 1}`)}.md`,
668-
)
759+
const fileName = `${toContentSlug(event.sourceId, `${groupId}-${index + 1}`)}.md`
760+
expectedFileNames.push(fileName)
761+
const filePath = resolve(groupDir, fileName)
669762
writtenFiles += Number(await writeIfChanged(filePath, buildFrontmatterDocument(event)))
670763
}
764+
765+
await pruneGeneratedMarkdownFiles(groupDir, expectedFileNames)
671766
}
672767

673768
return writtenFiles

src/content/events/aindustriosa/aindustriosa-314059100.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/content/events/aindustriosa/aindustriosa-314059417.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/content/events/aindustriosa/aindustriosa-314167108.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/content/events/pythonvigo/pythonvigo-1671130800000-reunion-diciembre-2022.md

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
sourceId: 'pythonvigo-1750356000000-reunion-junio-2025'
3+
groupId: 'pythonvigo'
4+
groupName: 'PythonVigo'
5+
groupLogo: 'https://vigotech.org/images/python_vigo.png'
6+
title: 'Reunión junio 2025'
7+
description: null
8+
date: 1750356000000
9+
dateISO: '2025-06-19T18:00:00.000Z'
10+
location: null
11+
link: 'https://www.python-vigo.es/posts/reunion-junio-2025/'
12+
---

src/content/events/vigowordpress/vigo-wordpress-meetup-314167142.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/content/events/vigowordpress/vigowordpress-1774548000000-posicionando-sin-ia-en-tiempos-de-ia.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)