Skip to content

DevAdd SEO module, blogging editor improvements, and navigation UX enhancements - #187

Merged
ddon merged 14 commits into
BeamLabEU:devfrom
mdon:dev
Nov 19, 2025
Merged

DevAdd SEO module, blogging editor improvements, and navigation UX enhancements#187
ddon merged 14 commits into
BeamLabEU:devfrom
mdon:dev

Conversation

@mdon

@mdon mdon commented Nov 19, 2025

Copy link
Copy Markdown
Contributor

TL;DR

This PR adds a new SEO module with noindex configuration, implements autosave for the blogging editor, fixes timezone handling for timestamps,
improves navigation UX with better hover states and active highlighting, and resolves CSP compatibility issues for production environments.


SEO Module

  • New SEO settings module with configurable noindex directive (/admin/settings/seo)
  • Injects robots meta tags when enabled
  • Module toggle in admin dashboard
  • Complete routing and sidebar integration

Blogging Editor Enhancements

Autosave Functionality

  • 2-second debounced autosave prevents accidental data loss
  • Visual status indicators: "Saving..." (blue spinner), "Unsaved changes" (yellow), "Saved" (green checkmark)
  • Replaced manual save button with automatic saving
  • Silent autosave with no intrusive flash messages
  • Browser exit protection for in-progress saves

Timezone-Aware Timestamps

  • Store times in UTC, display in user's configured timezone
  • Respect user's date/time format preferences from Settings
  • Publish at "5pm their time" without UTC math
  • Performance: cached settings loaded once per mount, no per-row queries
  • Resilient to nil user scenarios (tests, unauthenticated access)

CSP Compatibility Fixes

  • Add nonce support for inline scripts (CSP compliance)
  • User-friendly warning system if JavaScript features fail
  • Better error handling for blocked scripts
  • Fixes component insertion and unsaved changes popup in production/staging
  • Proper DOM initialization for LiveView updates

UI Polish

  • Update "PhoenixKit Storage file ID" → "Phoenix Kit Media ID" label
  • Clean up excessive console logging
  • Better quote escaping in template strings

Navigation & Dashboard Improvements

Navigation UX

  • Fix hover states: active items show darker blue (hover:bg-primary/90) instead of grey
  • Add exact_match_only option to prevent parent highlighting on subtabs
  • Blogging parent nav only highlights on exact /admin/blogging, not on subtabs

Blogging Dashboard

  • Add dedicated stat square for drafts (grid: xl:grid-cols-4 → xl:grid-cols-5)
  • Standardize storage mode badges to consistent light grey (badge-ghost)
  • Improved visual hierarchy across index and settings pages

Technical Details

Files Modified:

  • lib/modules/seo/* - New SEO module
  • lib/phoenix_kit/utils/date.ex - Cached timezone conversion helpers
  • lib/phoenix_kit_web/live/modules/blogging/editor.ex - Autosave implementation
  • lib/phoenix_kit_web/live/modules/blogging/editor.html.heex - CSP nonce support, warnings
  • lib/phoenix_kit_web/live/modules/blogging/{index,blog}.ex - Timezone formatting
  • lib/phoenix_kit_web/components/admin_nav.ex - Navigation improvements
  • lib/phoenix_kit_web/components/layout_wrapper.ex - Exact match routing

Breaking Changes: None

mdon and others added 14 commits November 18, 2025 23:48
* introduce PhoenixKit.Modules.SEO plus defaults and persistence keys
* expose new `/admin/settings/seo` LiveView, router entry, and sidebar link
* update modules dashboard card/toggle and inject robots meta tags when enabled
* update admin_nav_item to use conditional hover classes
* active items now show hover:bg-primary/90 instead of hover:bg-base-200
* prevents grey hover background from overriding blue active background
…tabs

* introduce exact_match_only attribute to admin_nav_item component
* update nav_item_active? to skip hierarchical matching when exact_match_only is true
* apply exact_match_only={true} to blogging nav item
* blogging parent now highlights only on /admin/blogging, not on /admin/blogging/{blog-slug}
* individual blog subtabs continue to highlight correctly as nested items
* update blogging index grid from xl:grid-cols-4 to xl:grid-cols-5
* move drafts from stat description to its own dedicated stat square
* improve visual hierarchy by giving drafts equal prominence with published posts
* change both slug-based and timestamp-based badges to use badge-ghost
* remove conditional styling that made slug-based primary and timestamp-based ghost
* apply consistent light grey appearance across blogging index and settings pages
Implements proper timezone handling for blog post timestamps:
- Store all times in UTC internally
- Display times in logged-in user's timezone
- Respect user's date/time format preferences from Settings
- Allow users to publish at "5pm their time" without UTC math

Performance optimizations:
- Load date_time_settings once per mount using Settings.get_settings_cached/2
- Pass settings through call chain to cached formatter functions
- Add cached timezone conversion helpers in PhoenixKit.Utils.Date:
  * shift_to_user_timezone_cached/3
  * format_datetime_with_timezone_cached/4
  * format_date_with_timezone_cached/4
  * format_time_with_timezone_cached/4
- These use get_user_timezone_cached/2 to avoid per-row Settings queries

Resilience improvements:
- Add nil user fallback: user = current_user || %{user_timezone: nil}
- Graceful handling during tests or unauthenticated access

Files modified:
- lib/phoenix_kit/utils/date.ex: Add cached timezone conversion helpers
- lib/phoenix_kit_web/live/modules/blogging/index.ex: Load settings, use cached formatters
- lib/phoenix_kit_web/live/modules/blogging/blog.ex: Load settings, use cached formatters
- lib/phoenix_kit_web/live/modules/blogging/blog.html.heex: Pass user and settings to formatter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed label from "PhoenixKit Storage file ID" to "Phoenix Kit Media ID"
in the blogging editor's advanced manual file ID input section for better
clarity and consistency with product naming.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Implements automatic saving with debouncing to prevent accidental data loss:

Features:
- 2-second debounced autosave triggers on content or metadata changes
- Proper timer management to prevent multiple simultaneous saves
- Visual status indicators replacing manual save button:
  * "Saving..." badge (blue, with spinner) during autosave
  * "Unsaved changes" badge (yellow) when changes pending
  * "Saved" badge (green, with checkmark) when all saved
- Silent autosave (no flash messages for automatic saves)
- Manual save events still show confirmation messages
- Browser exit protection remains for in-progress saves

Technical implementation:
- Added :is_autosaving and :autosave_timer assigns to track state
- schedule_autosave/1 helper cancels old timers and schedules new ones
- perform_save/1 extracted from save event for reuse in autosave
- handle_info(:autosave) processes debounced autosave requests
- Conditional flash messages based on is_autosaving flag

Benefits:
- Users no longer need to remember to click save
- Work is automatically preserved during editing
- Reduced cognitive load with clear visual feedback
- Prevents data loss from browser crashes or accidental navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Resolves issues where component insertion and unsaved changes popup
weren't working in environments with Content Security Policy enabled.

Key fixes:
- Add CSP nonce support to inline scripts for policy compliance
- Add visible warning system if JavaScript features fail to initialize
- Improve error handling with helpful messages for blocked scripts
- Remove excessive console.log debugging statements
- Better DOM initialization handling for LiveView updates
- Add MutationObserver to manage warning visibility

CSP Compliance:
- Extract nonce from assigns (script_csp_nonce or csp_nonce)
- Apply nonce attribute to all inline script tags
- Scripts now work in strict CSP environments

User Experience:
- Warning banner hidden automatically when JS works
- Shows helpful message if inline scripts blocked
- Noscript fallback for fully disabled JavaScript
- Init retry with timeout and error reporting

Technical improvements:
- Proper quote escaping in template strings
- Cleaner event listener parameter naming
- Warning element reference caching with validation
- 20-attempt retry limit before showing error

This fixes production/staging issues while maintaining local dev compatibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Prefix unused session parameter with underscore in SEO mount
- Remove default value from nav_item_active?/4 (always called with 4 args)

This resolves all compilation warnings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
RateLimiter fixes for Hammer 7.x API:
- Backend.set/3 returns integer count, not :ok/{:error, reason}
- Backend.get/2 returns integer directly, not {:ok, count}
- Backend.hit/3 only returns {:allow, count} or {:deny, retry_after}
- Remove unreachable error handling for non-existent {:error, reason} case

Autosave pattern match fix:
- Remove unreachable 'other ->' clause in handle_info(:autosave)
- perform_save/1 always returns {:noreply, socket}, so pattern match directly

All Dialyzer warnings resolved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Hammer 7.x removed delete_buckets with no replacement, and Backend.set/3
requires positive integers (cannot set to 0 to reset).

Changes:
- Mark reset_rate_limit/2 as @deprecated with explanation
- Return {:error, :not_supported} instead of attempting reset
- Add migration guide in documentation
- Log warning when function is called

Alternative approaches for users:
- For testing: Use Application.put_env to disable rate limiting
- For admin intervention: Wait for time window to expire naturally
- For immediate reset: Restart application (clears ETS tables)

This resolves Dialyzer errors about invalid Backend.set/3 arguments.

See: https://hexdocs.pm/hammer/upgrade-v7.html

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add alias for PhoenixKit.Users.RateLimiter.Backend at module level
to avoid repeated fully-qualified module references.

This resolves all Credo software design suggestions about nested
module aliasing in rate_limiter.ex.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Extract error handling logic into helper functions to bring complexity
down from 14 to acceptable levels:

- Add handle_post_update_result/4 to centralize update success path
- Add handle_post_update_error/2 for update error handling
- Add handle_post_creation_error/3 for creation error handling
- Refactor create_new_post/2 to use new helpers
- Refactor create_new_translation/2 to use new helpers

This eliminates the Credo cyclomatic complexity warnings while maintaining
the same functionality and improving code maintainability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@ddon
ddon merged commit e384b7d into BeamLabEU:dev Nov 19, 2025
6 checks passed
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