Skip to content

Add embeddable meetings widget for municipality websites#310

Open
christosporios wants to merge 2 commits intomainfrom
claude/municipality-meetings-widget-zjx8h
Open

Add embeddable meetings widget for municipality websites#310
christosporios wants to merge 2 commits intomainfrom
claude/municipality-meetings-widget-zjx8h

Conversation

@christosporios
Copy link
Copy Markdown
Collaborator

@christosporios christosporios commented Apr 10, 2026

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=...)

  • Lists recent and upcoming council meetings with topic icons, dates, admin body names
  • Upcoming meetings shown under "Επερχόμενες Συνεδριάσεις" section header
  • Customizable: accent color, light/dark mode, card radius, number of meetings, subject visibility, admin body type filter
  • "OpenCouncil" footer with logo linking back to the city page
  • Cached via unstable_cache + CDN headers (s-maxage=300, stale-while-revalidate=3600)

Configurator (/[cityId]/widget, admin-only)

  • Live iframe preview, color picker, switches, sliders
  • Reuses BadgePicker for body type filter (same as meetings list)
  • Generates copy-to-clipboard iframe code
  • Installation notes section (CSP, WordPress/Joomla tips)
  • Accessible via button next to "Επεξεργασία πόλης"

Subject sorting (shared by MeetingCard + widget)

  • New ordering: beforeAgenda last → contributions count desc → agenda index asc
  • Adds _count.contributions to meeting query
  • formatDate now accepts locale parameter

Test plan

  • Visit /el/embed/meetings?cityId=chania — verify widget renders with meetings, subjects, topic icons, footer
  • Visit /el/embed/meetings?cityId=chania&mode=dark&accent=2B6181 — verify dark mode and custom accent
  • As admin, visit city page → click Widget button → verify configurator loads with live preview
  • Change accent color, toggle dark mode, adjust slider — verify preview updates
  • Copy embed code, paste in an HTML file, open in browser — verify iframe loads
  • Verify no hydration errors in browser console
  • Run npm test — all tests pass

Note

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/meetings that 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/widget configurator (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.contributions included), and makes formatDate locale-aware; adds corresponding en/el translations 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/meetings with CDN caching, theming via query params, and an admin-only configurator at /:cityId/widget. Also updates getCouncilMeetingsForCity to support timeFilter and administrativeBodyTypes, and refines sortSubjectsByImportance to 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 in EmbedMeetingCard are 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

Filename Overview
src/app/[locale]/(embed)/embed/meetings/page.tsx Two-query approach for upcoming/past meetings correctly avoids the previous cut-off bug; query params are validated and sanitised before use.
src/components/embed/EmbedMeetingCard.tsx Locale is accepted but not applied to the meeting URL, causing English-locale embeds to redirect visitors to the Greek meeting page.
src/components/embed/EmbedConfigurator.tsx Admin configurator with live preview, clipboard copy (with try/catch), and BadgePicker for body type filter; no issues found.
next.config.mjs Adds CSP frame-ancestors * and Cache-Control headers for /embed routes; previous X-Frame-Options issue resolved.
src/lib/db/meetings.ts Added timeFilter (upcoming/past) and administrativeBodyTypes support; includes _count.contributions for subject sorting; correct ASC/DESC ordering per filter direction.
src/lib/utils.ts sortSubjectsByImportance updated to prioritise contributions count over agenda index; beforeAgenda items sorted last. Logic and tests look correct.
src/lib/utils/embedTheme.ts Clean accent-color derivation utility with proper hex parsing and HSL manipulation; parseAccentColor safely validates input.
src/lib/cache/queries.ts New getCouncilMeetingsForCityPublicCached correctly keys cache on timeFilter and body types; no auth call (safe for static/CDN paths).
src/app/[locale]/(city)/[cityId]/(other)/(tabs)/widget/page.tsx Admin-only page gated by isUserAuthorizedToEdit; fetches distinct body types for the configurator; uses prisma directly for a lightweight query.
src/components/embed/EmbedFooter.tsx City link uses a locale-less URL; consistent with EmbedMeetingCard issue but lower impact since city pages work correctly via default-locale redirect.

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)
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/components/embed/EmbedMeetingCard.tsx
Line: 31

Comment:
**Meeting URL discards locale**

`locale` is accepted as a prop and correctly used for `localize()`, but is not applied to `meetingUrl`. With `localePrefix: 'as-needed'` and `defaultLocale: 'el'`, a bare `${baseUrl}/${cityId}/${meetingId}` resolves to the Greek locale for all visitors — so clicks from an English-locale embed will land on the Greek meeting page.

```suggestion
    const localePart = locale !== 'el' ? `/${locale}` : '';
    const meetingUrl = `${baseUrl}${localePart}/${meeting.cityId}/${meeting.id}`;
```

The same issue exists for the city link in `EmbedFooter` — it uses `${baseUrl}/${cityId}` and the locale is never passed into that component.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "Unify subject sorting: agenda status, th..." | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 10, 2026

🚀 Preview deployment ready!

Preview URL: https://pr-310.preview.opencouncil.gr
Commit: 4d4e741
Database: Shared staging

The preview will be automatically updated when you push new commits.
It will be destroyed when this PR is closed or merged.


This preview uses the staging database - any changes will affect other previews.

Comment thread src/app/[locale]/(embed)/embed/meetings/page.tsx Outdated
Comment thread next.config.mjs Outdated
Comment thread src/components/embed/EmbedConfigurator.tsx
Comment thread src/components/embed/EmbedConfigurator.tsx
Comment thread src/components/embed/EmbedConfigurator.tsx Outdated
Comment thread next.config.mjs Outdated
@christosporios christosporios force-pushed the claude/municipality-meetings-widget-zjx8h branch from 805fc20 to 7b05244 Compare April 10, 2026 17:49
Comment thread src/components/embed/EmbedConfigurator.tsx Outdated
}

export function EmbedMeetingCard({ meeting, locale, showSubjects, baseUrl, cityTimezone, translations: t }: EmbedMeetingCardProps) {
const meetingUrl = `${baseUrl}/${meeting.cityId}/${meeting.id}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7b05244. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 10, 2026

Tip:

Greploop — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.

Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

@christosporios christosporios force-pushed the claude/municipality-meetings-widget-zjx8h branch from 7b05244 to 4646d05 Compare April 10, 2026 18:07
Comment thread src/app/[locale]/(embed)/embed/meetings/page.tsx Outdated
@christosporios christosporios force-pushed the claude/municipality-meetings-widget-zjx8h branch from 4646d05 to 6325e71 Compare April 11, 2026 10:12
Comment thread src/app/[locale]/(embed)/embed/meetings/page.tsx Outdated
claude added 2 commits April 11, 2026 14:09
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
@christosporios christosporios force-pushed the claude/municipality-meetings-widget-zjx8h branch from 6325e71 to 4d4e741 Compare April 11, 2026 14:09
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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)}
</>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d4e741. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants