Add embeddable meetings widget for municipality websites#310
Add embeddable meetings widget for municipality websites#310christosporios wants to merge 2 commits intomainfrom
Conversation
🚀 Preview deployment ready!Preview URL: https://pr-310.preview.opencouncil.gr The preview will be automatically updated when you push new commits. This preview uses the staging database - any changes will affect other previews. |
805fc20 to
7b05244
Compare
| } | ||
|
|
||
| export function EmbedMeetingCard({ meeting, locale, showSubjects, baseUrl, cityTimezone, translations: t }: EmbedMeetingCardProps) { | ||
| const meetingUrl = `${baseUrl}/${meeting.cityId}/${meeting.id}`; |
There was a problem hiding this comment.
Embed widget links missing locale path segment
Low Severity
EmbedMeetingCard constructs meetingUrl as ${baseUrl}/${meeting.cityId}/${meeting.id} and EmbedFooter builds ${baseUrl}/${cityId}, both omitting the locale prefix (e.g., /el/). The locale prop is available in EmbedMeetingCard but unused for URL construction. Every click from the embedded widget triggers an unnecessary 302 redirect via the i18n middleware before reaching the destination page.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7b05244. Configure here.
There was a problem hiding this comment.
Intentional — the locale prefix was removed at the user's request. The i18n middleware handles the redirect automatically via locale detection from headers. The 302 is a single lightweight redirect that only happens on the first click (browsers cache it), and it means the widget doesn't need to hardcode a locale into the destination URL.
Generated by Claude Code
|
Tip: Greploop — Automatically fix all review issues by running Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal. |
7b05244 to
4646d05
Compare
4646d05 to
6325e71
Compare
Adds an iframe-based widget that municipalities can embed on their websites to display recent council meetings. Includes a visual configurator (accent color, dark/light mode, meeting count, subject visibility, card radius, admin body filter) with live preview and copy-to-clipboard embed code. - Embed route at /[locale]/embed/meetings with CSS custom properties theming derived from a single accent color - Widget configurator page at /[cityId]/widget (admin-only tab) - Lightweight EmbedMeetingCard with locale-aware date formatting - X-Frame-Options/CSP headers allowing cross-origin iframe embedding - Reuses existing HexColorPicker, Slider, Switch UI components - Reuses getCouncilMeetingsForCity and sortSubjectsByImportance https://claude.ai/code/session_01W4rHTFjT8mGNjqyTokU6FY
Rewrite sortSubjectsByImportance so MeetingCard and EmbedMeetingCard share the same ordering logic: 1. beforeAgenda subjects sort last; outOfAgenda and regular agenda items are treated equally 2. Number of speaker contributions (descending) 3. Agenda item index (ascending) 4. Alphabetical name tie-breaker Also adds _count.contributions to the getCouncilMeetingsForCity query so contribution counts are available for sorting. https://claude.ai/code/session_01W4rHTFjT8mGNjqyTokU6FY
6325e71 to
4d4e741
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4d4e741. Configure here.
| )} | ||
| {renderCards(past)} | ||
| </> | ||
| )} |
There was a problem hiding this comment.
Missing section label for past-only meeting display
Low Severity
The "Recent Meetings" section label is only rendered when upcoming.length > 0, so when a city has only past meetings (no upcoming), the widget displays meeting cards with no section label at all. In contrast, when only upcoming meetings exist, the "Upcoming Meetings" label is always shown. This asymmetry means most real-world widgets (cities rarely schedule many future meetings) render as a headerless list of cards, while the rarer upcoming-only case gets a proper header.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 4d4e741. Configure here.


Summary
Municipalities can now embed a meetings widget on their websites (e.g. chania.gr). An admin configures the widget's appearance via a visual configurator, copies an iframe snippet, and pastes it into their CMS.
Widget (
/[locale]/embed/meetings?cityId=...)unstable_cache+ CDN headers (s-maxage=300, stale-while-revalidate=3600)Configurator (
/[cityId]/widget, admin-only)BadgePickerfor body type filter (same as meetings list)Subject sorting (shared by MeetingCard + widget)
_count.contributionsto meeting queryformatDatenow accepts locale parameterTest plan
/el/embed/meetings?cityId=chania— verify widget renders with meetings, subjects, topic icons, footer/el/embed/meetings?cityId=chania&mode=dark&accent=2B6181— verify dark mode and custom accentnpm test— all tests passNote
Medium Risk
Adds a new public embeddable surface (iframe/CSP headers) and changes meeting queries/sorting logic, which could affect what meetings/subjects appear and how they’re cached.
Overview
Adds a new public embed route
/:locale/embed/meetingsthat renders a themed, CDN-cached list of upcoming/recent meetings (optional subjects) designed for iframe embedding, plus minimal embed layout/CSS and footer branding.Introduces an admin-only
/:cityId/widgetconfigurator (linked from the city header) that live-previews the iframe, lets admins tune accent color/light-dark/radius/limits/body-type filters, and copy-paste the generated embed code.Extends meeting fetching/caching to support administrative-body filtering and upcoming vs past time slicing, updates subject “importance” sorting to use agenda status + contributions counts (with DB
_count.contributionsincluded), and makesformatDatelocale-aware; adds correspondingen/eltranslations and Next.js headers to allow framing embed pages (frame-ancestors *) with cache headers.Reviewed by Cursor Bugbot for commit 4d4e741. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
Adds a public embeddable meetings widget at
/:locale/embed/meetingswith CDN caching, theming via query params, and an admin-only configurator at/:cityId/widget. Also updatesgetCouncilMeetingsForCityto supporttimeFilterandadministrativeBodyTypes, and refinessortSubjectsByImportanceto rank by contribution count before agenda index.Prior review concerns (upcoming meeting ordering,
X-Frame-Options, clipboard error handling) have all been addressed. One remaining minor issue: meeting URLs inEmbedMeetingCardare constructed without a locale prefix, so English-locale embeds will redirect visitors to the Greek (el) meeting page when a card is clicked.Confidence Score: 5/5
Safe to merge — all prior P1 concerns are resolved; only a minor locale-in-URL P2 remains.
Previous P1 findings (upcoming meeting order cut-off, X-Frame-Options, clipboard rejection) are all addressed. The only outstanding finding is P2: meeting card links drop the locale prefix, which causes English-locale embed clicks to redirect to the Greek meeting page rather than the English one. This doesn't break functionality and is a low-traffic path given el is the primary locale.
src/components/embed/EmbedMeetingCard.tsx — locale not applied to meetingUrl construction.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Admin as Admin (City Staff) participant Configurator as /cityId/widget (EmbedConfigurator) participant MuniSite as Municipality Website participant CDN as CDN / Edge participant EmbedPage as /locale/embed/meetings participant DB as PostgreSQL (via Prisma) Admin->>Configurator: Open widget page (admin-only) Configurator->>Configurator: Render live iframe preview Admin->>Configurator: Adjust accent, mode, limit, body types Configurator->>Configurator: Generate iframe embed code Admin->>Configurator: Copy embed code Admin->>MuniSite: Paste iframe into CMS MuniSite->>CDN: Load page containing iframe CDN-->>MuniSite: Page HTML MuniSite->>CDN: GET /locale/embed/meetings?cityId=... alt Cache HIT (s-maxage=300) CDN-->>MuniSite: Cached embed HTML else Cache MISS CDN->>EmbedPage: Forward request EmbedPage->>DB: getCouncilMeetingsForCityPublicCached(upcoming) EmbedPage->>DB: getCouncilMeetingsForCityPublicCached(past) DB-->>EmbedPage: Meeting data EmbedPage-->>CDN: HTML + Cache-Control: s-maxage=300 CDN-->>MuniSite: Embed HTML (cached for 5 min) endPrompt To Fix All With AI
Reviews (5): Last reviewed commit: "Unify subject sorting: agenda status, th..." | Re-trigger Greptile