Fix W3C validator errors: duplicate guids and missing atom:link self

- Recurring events share one UID across all occurrences (confirmed via
  the API: same UID, different start dates), which produced duplicate
  <guid> values the RSS spec forbids. Suffix the guid with the
  occurrence's start date to make it unique per occurrence.
- Add atom:link rel="self" to the channel, per the validator's
  recommendation, in both the PHP and Python implementations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
para
2026-09-25 23:49:31 +02:00
co-authored by Claude Sonnet 5
parent 13ea82e347
commit 13e5c94ee7
4 changed files with 29 additions and 8 deletions
+2 -1
View File
@@ -44,7 +44,8 @@ function build_news_feed(): string
'Silly.be - Actualités',
SITE . '/actualites',
'Actualités de la commune de Silly (Belgique)',
$items
$items,
'https://sillycone-village.be/silly-rss/actualites.php'
);
}
+7 -2
View File
@@ -56,10 +56,14 @@ function build_events_feed(): string
$meta = format_event_meta($item);
$description = ($meta !== '' ? '<p>' . htmlspecialchars($meta, ENT_QUOTES, 'UTF-8') . '</p>' : '')
. full_body_html($item);
// Un evenement recurrent partage le meme UID sur toutes ses
// occurrences (memes UID, dates differentes) : on suffixe le guid
// avec la date de debut pour rester unique par occurrence, comme
// l'exige la spec RSS.
$items[] = [
'title' => $title,
'link' => SITE . '/agenda/' . slugify($title) . '?u=' . rawurlencode($uid),
'guid' => $uid,
'guid' => $uid . '-' . ($item['start'] ?? ''),
'pubDate' => $item['created'] ?? $item['start'] ?? null,
'description' => $description,
];
@@ -68,7 +72,8 @@ function build_events_feed(): string
'Silly.be - Agenda',
SITE . '/agenda',
"Agenda / événements de la commune de Silly (Belgique)",
$items
$items,
'https://sillycone-village.be/silly-rss/agenda.php'
);
}
+5 -2
View File
@@ -166,14 +166,17 @@ function xml_escape(string $text): string
/**
* Construit un flux RSS 2.0 complet a partir d'une liste d'items deja
* formates (chacun un tableau title/link/guid/pubDate/description).
* $selfUrl est l'URL du flux lui-meme (atom:link rel="self", recommande
* par le validateur W3C).
*/
function build_rss(string $title, string $link, string $description, array $items): string
function build_rss(string $title, string $link, string $description, array $items, string $selfUrl): string
{
$now = rfc822(null);
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$xml .= '<rss version="2.0">' . "\n <channel>\n";
$xml .= '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">' . "\n <channel>\n";
$xml .= ' <title>' . xml_escape($title) . "</title>\n";
$xml .= ' <link>' . xml_escape($link) . "</link>\n";
$xml .= ' <atom:link href="' . xml_escape($selfUrl) . '" rel="self" type="application/rss+xml" />' . "\n";
$xml .= ' <description>' . xml_escape($description) . "</description>\n";
$xml .= " <language>fr-be</language>\n";
$xml .= " <lastBuildDate>$now</lastBuildDate>\n";
+15 -3
View File
@@ -37,6 +37,11 @@ MAX_PAGES = 2 # jusqu'a 40 items par flux
NEWS_API = f"{SITE}/actualites/@results"
EVENTS_API = f"{SITE}/agenda/@results"
# Base publique ou ce script publie ses flux une fois deploye (voir
# README.md, variante Yunohost/cron) - sert a l'atom:link rel="self".
# A adapter si vous deployez ailleurs.
SELF_BASE_URL = "https://akbar.nohost.me/silly-rss"
def fetch_json(url: str) -> dict:
result = subprocess.run(
@@ -170,11 +175,15 @@ def build_event_items() -> list[str]:
meta = format_event_meta(item)
description = (f"<p>{html.escape(meta)}</p>" if meta else "") + full_body_html(item)
pub_dt = parse_dt(item.get("created") or item.get("start"))
# Un evenement recurrent partage le meme UID sur toutes ses
# occurrences : on suffixe le guid avec la date de debut pour
# rester unique par occurrence, comme l'exige la spec RSS.
occurrence_guid = f"{guid}-{item.get('start', '')}"
entries.append(
" <item>\n"
f" <title>{escape(title)}</title>\n"
f" <link>{escape(link)}</link>\n"
f" <guid isPermaLink=\"false\">{escape(guid)}</guid>\n"
f" <guid isPermaLink=\"false\">{escape(occurrence_guid)}</guid>\n"
f" <pubDate>{rfc822(pub_dt)}</pubDate>\n"
f" <description>{escape(description)}</description>\n"
" </item>"
@@ -182,14 +191,15 @@ def build_event_items() -> list[str]:
return entries
def write_feed(path: str, title: str, link: str, description: str, items: list[str]) -> None:
def write_feed(path: str, title: str, link: str, description: str, items: list[str], self_url: str) -> None:
now = rfc822(datetime.now(timezone.utc))
xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<rss version="2.0">\n'
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n'
" <channel>\n"
f" <title>{escape(title)}</title>\n"
f" <link>{escape(link)}</link>\n"
f' <atom:link href="{escape(self_url)}" rel="self" type="application/rss+xml" />\n'
f" <description>{escape(description)}</description>\n"
" <language>fr-be</language>\n"
f" <lastBuildDate>{now}</lastBuildDate>\n"
@@ -211,6 +221,7 @@ def main() -> None:
link=f"{SITE}/actualites",
description="Actualités de la commune de Silly (Belgique)",
items=news_items,
self_url=f"{SELF_BASE_URL}/actualites.xml",
)
print(f"[OK] actualites.xml : {len(news_items)} items")
@@ -221,6 +232,7 @@ def main() -> None:
link=f"{SITE}/agenda",
description="Agenda / événements de la commune de Silly (Belgique)",
items=event_items,
self_url=f"{SELF_BASE_URL}/agenda.xml",
)
print(f"[OK] agenda.xml : {len(event_items)} items")