Skip to content

feat(main): newsletter & top banners #380

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

majahendzel-va
Copy link
Contributor

@majahendzel-va majahendzel-va commented Feb 6, 2025

Summary by CodeRabbit

  • New Features

    • Introduced support for multiple banner types (slider, top banner, card banner) with device-specific images and display controls.
    • Added a new newsletter banner component that displays a clickable card banner when available.
  • Enhancements

    • Banner data is now fetched from a remote API, supporting richer and more flexible banner configurations.
    • UI components and templates updated to conditionally display new banner types based on availability.
  • Refactor

    • Updated state management and service methods to handle the new unified banner model.
    • Adjusted component logic and property names to reflect the expanded banner functionality.
  • Style

    • Minor adjustments to CSS class ordering for consistency.

Copy link

cloudflare-workers-and-pages bot commented Feb 6, 2025

Deploying angular-love-client with  Cloudflare Pages  Cloudflare Pages

Latest commit: 87d4b61
Status:🚫  Build failed.

View logs

@majahendzel-va majahendzel-va force-pushed the feat/multiple-banner-logic branch from 927dee7 to 5a7f82f Compare February 6, 2025 10:13
Comment on lines +12 to 14
getVisibleBanners() {
return this._http.get<Banners>(`${this._apiBaseUrl}/banners`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

can we host this data as a json asset? it would be created in the build time as a part of the process in apps/blog/scripts/build-routes.mjs. This resource can be received via http client, similarly to libs/blog/articles/data-access/src/lib/guards/article-exists.guard.ts.

With current implementation we fetch banners from BFF for every available page. Basically it adds an extra request during prerendering. If we prerender 500 pages, it ends up with BFF 1000 request, where 50% is the same banners request.

Copy link
Contributor

Choose a reason for hiding this comment

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

sure, we can do that

Copy link
Contributor

Choose a reason for hiding this comment

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

this pr resolves the issue #415

Copy link

coderabbitai bot commented May 14, 2025

Walkthrough

This update refactors the banner system to support multiple banner types—slider, top, and card banners—with device-specific images and dynamic display controls. It introduces new DTOs, type definitions, UI components, and service methods, shifting banner data fetching to a remote API and updating state management and component integration accordingly.

Changes

File(s) Change Summary
libs/blog-bff/banners/api/src/lib/dtos.ts Expanded WPBannerDto to include flags and properties for slider, top, and card banners; updated slides to support desktop/mobile images and new display controls.
libs/blog-bff/banners/api/src/lib/mappers.ts Refactored toBanner to return a Banners type, conditionally mapping slider, top, and card banners based on DTO flags and new structure.
libs/blog-contracts/banners/src/lib/banners.ts Modified Slider interface (optional url/alt), added new TopBanner, CardBanner, and Banners interfaces for multi-banner support.
libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts Changed data source from local JSON to remote API, updated method to getVisibleBanners() returning Banners instead of Slider.
libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts Renamed state from slider to banners, updated initialization and data fetching to use new service and banner model.
libs/blog/ad-banner/ui/src/index.ts Added export for new al-newsletter-banner.component.
libs/blog/ad-banner/ui/src/lib/newsletter-banner/al-newsletter-banner.component.ts Introduced new Angular component AlNewsletterBannerComponent for card banners with device-specific image handling.
libs/blog/articles/feature-article/src/lib/article-details/article-details.component.html Updated sticky aside logic to use bannerStore.banners()?.topBanner for conditional class application.
libs/blog/articles/feature-article/src/lib/article-details/article-details.component.ts Replaced local signal state with injected AdBannerStore for banner visibility.
libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.html Added conditional rendering for newsletter: uses AlNewsletterBannerComponent if cardBanner exists, otherwise falls back to default.
libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts Injected AdBannerStore, added computed cardBanner property, updated imports for new banner UI component.
libs/blog/articles/ui-article-card/src/lib/components/article-horizontal-card/article-horizontal-card.component.html Changed order of CSS classes in <article> element.
libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts Refactored from slider-centric to multi-banner model, updated store usage, removed viewport offset logic, added top banner handling.

Sequence Diagram(s)

sequenceDiagram
    participant Component as UI Component
    participant Store as AdBannerStore
    participant Service as AdBannerService
    participant API as Remote Banner API

    Component->>Store: getData()
    Store->>Service: getVisibleBanners()
    Service->>API: GET /banners
    API-->>Service: Banners DTO
    Service-->>Store: Banners
    Store-->>Component: banners (slider, topBanner, cardBanner)
    Component->>Component: Conditionally render banners based on presence
Loading

Possibly related PRs

Suggested reviewers

  • majahendzel-va
  • dokolyski

Poem

In the meadow of banners, new types now bloom,
Slider, card, and top—each finds its own room.
With images for desktop and mobile to show,
The API feeds them, so swiftly they grow.
Components now dance to a dynamic tune—
Banners for all, from morning to noon!
🐇✨

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/multiple-banner-logic
BFF URL: https://6d54b4c9-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://3fb0e041.angular-love-client.pages.dev
Alias URL: https://feat-multiple-banner-logic.angular-love-client.pages.dev

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
libs/blog-contracts/banners/src/lib/banners.ts (1)

1-8: Consider documenting the optional properties

The url and alt properties were changed to optional, which could affect existing code.

Consider adding JSDoc comments to explain why these properties are optional:

 export interface Slider {
+  /** Time in milliseconds to display each slide before transitioning to the next */
   slideDisplayTimeMs: number;
   slides: {
+    /** Optional URL to the slide image - may be undefined if no image is available */
     url?: string;
+    /** Optional alt text for accessibility - may be undefined if no alt text is provided */
     alt?: string;
+    /** URL to navigate to when the slide is clicked */
     navigateTo: string;
   }[];
 }
libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts (2)

26-26: Unused template reference variable

The template reference variable #topBannerRef is defined but never used in the component. Consider removing it if it's not needed for future functionality.

-    <al-top-banner #topBannerRef />
+    <al-top-banner />

75-86: Handle possible undefined values more safely

The current implementation uses optional chaining but could still result in runtime errors if data is malformed.

protected readonly slides = computed<AdImageBanner[] | undefined>(() =>
-  this.bannerStore.banners()?.slider?.slides.map((slide) => ({
+  this.bannerStore.banners()?.slider?.slides?.map((slide) => ({
    url: slide.url!,
    alt: slide.alt!,
    action: {
      type: 'url',
      url: slide.navigateTo,
    },
  })),
);
protected readonly msPerSlide = computed(
-  () => this.bannerStore.banners()?.slider?.slideDisplayTimeMs,
+  () => this.bannerStore.banners()?.slider?.slideDisplayTimeMs ?? 5000, // Provide a default value
);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3be6ade and db7cc3d.

⛔ Files ignored due to path filters (1)
  • apps/blog/src/assets/icons/arrow-down.svg is excluded by !**/*.svg
📒 Files selected for processing (13)
  • libs/blog-bff/banners/api/src/lib/dtos.ts (1 hunks)
  • libs/blog-bff/banners/api/src/lib/mappers.ts (1 hunks)
  • libs/blog-contracts/banners/src/lib/banners.ts (1 hunks)
  • libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts (1 hunks)
  • libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts (2 hunks)
  • libs/blog/ad-banner/ui/src/index.ts (1 hunks)
  • libs/blog/ad-banner/ui/src/lib/newsletter-banner/al-newsletter-banner.component.ts (1 hunks)
  • libs/blog/articles/feature-article/src/lib/article-details/article-details.component.html (1 hunks)
  • libs/blog/articles/feature-article/src/lib/article-details/article-details.component.ts (2 hunks)
  • libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.html (1 hunks)
  • libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts (3 hunks)
  • libs/blog/articles/ui-article-card/src/lib/components/article-horizontal-card/article-horizontal-card.component.html (1 hunks)
  • libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
libs/blog/ad-banner/ui/src/lib/newsletter-banner/al-newsletter-banner.component.ts (2)
libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts (1)
  • Component (32-94)
libs/blog-contracts/banners/src/lib/banners.ts (1)
  • CardBanner (16-20)
libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts (1)
libs/blog-contracts/banners/src/lib/banners.ts (1)
  • Banners (22-26)
libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts (1)
libs/blog-contracts/banners/src/lib/banners.ts (1)
  • Banners (22-26)
libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts (1)
libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts (1)
  • AdBannerStore (19-40)
libs/blog/articles/feature-article/src/lib/article-details/article-details.component.ts (1)
libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts (1)
  • AdBannerStore (19-40)
libs/blog-bff/banners/api/src/lib/mappers.ts (2)
libs/blog-bff/banners/api/src/lib/dtos.ts (2)
  • WPBannerDto (1-21)
  • WPBannerMediaDto (23-29)
libs/blog-contracts/banners/src/lib/banners.ts (1)
  • Banners (22-26)
libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts (5)
libs/blog/ad-banner/ui/src/lib/top-banner.component.ts (1)
  • Component (3-20)
libs/blog/layouts/ui-layouts/src/lib/header/header.component.ts (1)
  • Component (20-75)
libs/blog/ad-banner/ui/src/lib/banner-carousel/al-banner-carousel.component.ts (1)
  • Component (7-36)
libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts (1)
  • AdBannerStore (19-40)
libs/blog/ad-banner/ui/src/lib/ad-image-banner/ad-image-banner-data.interface.ts (1)
  • AdImageBanner (1-5)
🔇 Additional comments (31)
libs/blog/ad-banner/ui/src/index.ts (1)

2-2: Exports updated to support new newsletter banner component

The barrel file has been appropriately updated to export the new al-newsletter-banner.component and reorder exports to maintain logical grouping (data interface before component).

Also applies to: 4-4

libs/blog/articles/ui-article-card/src/lib/components/article-horizontal-card/article-horizontal-card.component.html (1)

3-3: CSS class reordering looks good

The reordering of CSS classes on the article element doesn't change functionality and maintains the same styling.

libs/blog/articles/feature-article/src/lib/article-details/article-details.component.html (1)

65-67: Direct banner check is more efficient

The conditional styles now directly check for the presence of topBanner instead of using a method call, which is more readable and efficient. The optional chaining operator is correctly used for null safety.

libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.html (1)

32-45: Well-implemented conditional newsletter banner rendering

The template now intelligently renders either the new newsletter banner component or falls back to the standard newsletter card based on the availability of banner data. The conditional syntax using @if/else with the as alias is clean and follows Angular best practices.

libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts (2)

4-5: Appropriate import substitution for the new banner structure.

The import changes correctly reflect the transition from a single slider model to a broader Banners interface that can include multiple banner types (slider, topBanner, cardBanner). Adding ConfigService allows for dynamic API URL construction.


9-10: Good use of dependency injection pattern.

Using inject() for the ConfigService is aligned with Angular's recommended patterns, and extracting the apiBaseUrl as a private readonly property follows good encapsulation principles.

libs/blog/articles/feature-article/src/lib/article-details/article-details.component.ts (3)

5-7: Good import update for dependency injection.

Adding the inject import from @angular/core is appropriate for the new injection pattern used in the component.


10-10: Proper import for store access.

The import of AdBannerStore provides access to the centralized banner data management, which is a good architectural approach.


61-61: Direct store injection improves state management.

Replacing a local reactive boolean signal with direct injection of the AdBannerStore service follows Angular best practices for state management. This change aligns with the broader refactoring of the banner system and provides more granular access to the available banner types.

libs/blog/ad-banner/data-access/src/lib/state/ad-banner.store.ts (4)

7-7: Import correctly updated for new banner structure.

The import has been updated from Slider to Banners to reflect the broader banner data model that includes multiple banner types.


11-13: Type definition properly updated for expanded banner model.

The state type has been correctly updated from { slider: Slider | null } to { banners: Banners | null } to accommodate the expanded banner model that includes multiple banner types (slider, topBanner, cardBanner).


15-17: Initial state correctly updated.

The initial state has been appropriately updated to match the new state structure.


25-30: Service method call and state update properly aligned with new structure.

The store now correctly calls getVisibleBanners() instead of getBannerSlider() and updates the state with the new banners property, aligning with the service changes and expanded banner model.

libs/blog/ad-banner/ui/src/lib/newsletter-banner/al-newsletter-banner.component.ts (2)

6-9: Component declaration follows Angular best practices.

Good use of OnPush change detection strategy and proper import of NgOptimizedImage for optimized image handling.


42-44: Input property defined correctly.

Good use of input.required<CardBanner>() to define a required input property with proper typing.

libs/blog/articles/feature-latest-articles/src/lib/feature-latest-articles/feature-latest-articles.component.ts (4)

12-12: Import organization looks good

Clean addition of the AdBannerStore import to support the new banner functionality.


19-19: Appropriate import for newsletter banner component

The AlNewsletterBannerComponent import aligns with the new banner system.


44-49: Component imports updated correctly

Added the necessary imports (CardContentDirective, AlNewsletterBannerComponent) and reordered GradientCardDirective, maintaining a clean imports section.


68-68: Good store injection approach

Correctly injecting the AdBannerStore using Angular's recommended inject pattern.

libs/blog-contracts/banners/src/lib/banners.ts (3)

10-14: New TopBanner interface looks good

The TopBanner interface follows the same structure as the Slider's slide items, maintaining consistency.


16-20: New CardBanner interface looks good

The CardBanner interface is consistent with the TopBanner interface, which is good for maintainability.


22-26: Well-structured Banners aggregation interface

The Banners interface effectively aggregates all banner types, making it easy to manage multiple banner types in a single object.

libs/blog-bff/banners/api/src/lib/dtos.ts (2)

4-10: Good banner type flag and device-specific image handling

The DTO now properly supports different device images and has a clear flag to control visibility.


12-15: Top banner properties follow consistent pattern

The top banner properties follow the same pattern as the slider with desktop/mobile versions and navigation URL.

libs/blog-bff/banners/api/src/lib/mappers.ts (2)

1-8: Updated import and return type

Good update to the import and return type to match the new Banners interface.


10-24: Slider mapping implemented correctly

The conditional inclusion of the slider based on the display flag is well implemented.

libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts (5)

28-39: Verify the purpose of dual banner components

There seems to be redundancy with both <al-top-banner> and the conditional <al-ad-image-banner> for top banners. Based on the overall refactoring, it appears you're transitioning to a dynamic banner system, but it's unclear if both components are intentionally needed.

Also, consider using the nullish coalescing operator instead of non-null assertions for better safety:

@if (topBanner(); as topBanner) {
  <al-ad-image-banner
    [banner]="{
-     url: topBanner.url!,
-     alt: topBanner.alt!,
+     url: topBanner.url ?? '',
+     alt: topBanner.alt ?? '',
      action: {
        type: 'url',
-       url: topBanner.navigateTo!,
+       url: topBanner.navigateTo ?? '',
      },
    }"
  />
}

73-73: LGTM! Appropriate store naming

The rename from sliderStore to bannerStore better reflects the expanded functionality for multiple banner types.


87-89: LGTM! Clean computed property implementation

The implementation of the topBanner computed property is clean and correctly uses optional chaining.


93-94: Address TODO comment

There's a comment indicating this is a temporary solution. Consider creating a ticket to track this technical debt or implement a more permanent solution.

The adBannerVisible computed property is hardcoded to false. Is this intentional or should it be derived from banner data?


114-116: LGTM! Simplified constructor

The constructor has been simplified to only fetch banner data, removing previous viewport scroll handling.

Comment on lines +12 to +13
getVisibleBanners() {
return this._http.get<Banners>(`${this._apiBaseUrl}/banners`);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider caching mechanism to address previous concerns about redundant requests.

The method has been appropriately renamed from getBannerSlider() to getVisibleBanners() to reflect its expanded purpose. The implementation is clean and focused.

However, I noticed in past review comments a concern about making redundant banner requests during prerendering. Consider implementing a caching mechanism or moving to a static JSON asset as previously suggested to optimize performance.


🏁 Script executed:

#!/bin/bash
# Check if there's any caching implementation for banner requests
echo "Searching for caching implementations in banner-related code..."
rg -A 5 -B 5 "cache|shareReplay|replaySubject" --iglob "**/*banner*/**"

Length of output: 1094


🌐 Web query:

What are common strategies for reducing redundant API requests during Angular SSR/prerendering?

💡 Result:

During Angular SSR (Server-Side Rendering) or prerendering, it's common to face redundant API requests because Angular's rendering lifecycle might trigger multiple calls to the same data source. Here are some common strategies to reduce these redundant requests:

  1. State Transfer with TransferState API
    Angular provides a TransferState API that allows you to store the results of API calls on the server and then re-use that data on the client without making the API call again.

    • During SSR, you fetch data and store it in TransferState.
    • On the client side, you read from TransferState first. If data is available, skip the HTTP call.
      This efficiently reduces redundant calls since the client can reuse server-fetched data.
  2. Caching HTTP Requests
    Use an HTTP caching mechanism on the server during SSR. You can implement operators on the HTTP service like shareReplay to cache observable results so that simultaneous or repeated subscriptions don’t trigger multiple HTTP calls.

    • This is useful if multiple components/services request the same data.
    • Combine with TransferState for best results.
  3. Pre-fetching Data at a Higher Level
    Instead of each component making its own API call, lift the data fetching up to a parent or even to the route resolver.

    • Fetch data once per route during SSR.
    • Pass data down to child components via @input bindings, reducing multiple API calls.
  4. Avoid API Calls in ngOnInit or Constructor
    Sometimes API calls placed in lifecycle hooks without coordination cause multiple calls. Refactor to use centralized services that handle data fetching with caching or transfer state.

  5. Using Route Resolvers
    Use Angular route resolvers to fetch data before the route activates. During SSR, these fetches happen once per route transition, preventing redundant requests that happen due to component re-instantiations.

  6. Idempotent APIs and Request Deduplication Logic
    If your backend APIs support idempotent fetches, you can implement deduplication logic on the client-side or at the HTTP interceptor level to cancel or merge multiple identical requests.

  7. Universal-aware Data Services
    Design services that can detect if they are running on the server or client (using isPlatformServer) to conditionally trigger API calls or fetch from caches.

  8. Single Source of Truth using State Management
    Using NGRX or other state management libraries to keep a unified store. API data fetched once during SSR is hydrated into the store and accessed by components without triggering new calls.


Summary: The most effective and widely used approach is combining Angular's TransferState API to pass pre-fetched data from server to client and HTTP caching techniques like shareReplay to prevent duplicate calls during SSR. Route resolvers and centralized data fetching also contribute significantly to this goal.


Implement HTTP caching to prevent redundant banner requests during SSR

This service currently issues a fresh HTTP GET on every call:

  • File: libs/blog/ad-banner/data-access/src/lib/infrastructure/ad-banner.service.ts
    Method: getVisibleBanners() (lines 12–13)

To optimize performance during Angular Universal prerendering, consider one or more of the following:

• Use Angular’s TransferState API
– Fetch banners on the server, store them in TransferState, and read from it on the client to skip the extra HTTP call.
• Cache the Observable with shareReplay(1)
– Wrap the HTTP request in a shared ReplaySubject so multiple subscribers reuse the same response.
• Lift data fetching to a route resolver or parent service
– Fetch once per route and pass data down via @input, avoiding repeated calls in individual components.
• Pre-render a static banners JSON asset
– If banner data changes infrequently, bundle a JSON file in assets/ and load it instead of calling the API at runtime.

See Angular’s TransferState guide for details: https://angular.io/api/platform-browser/TransferState

Comment on lines +10 to +40
template: `
<a
class="relative flex h-full items-center justify-center"
[href]="cardBanner().navigateTo!"
>
<aside class="absolute h-full w-full overflow-hidden rounded-lg">
<img
tabindex="0"
role="button"
class="!relative object-cover shadow-inner blur-xl"
[attr.aria-label]="cardBanner()!.alt!"
[alt]="cardBanner().alt!"
[ngSrc]="cardBanner().url!"
fill
priority
/>
</aside>
<aside>
<img
tabindex="0"
role="button"
class="!relative"
[attr.aria-label]="cardBanner().alt!"
[alt]="cardBanner().alt!"
[ngSrc]="cardBanner().url!"
fill
priority
/>
</aside>
</a>
`,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve template safety and accessibility.

The template has several issues that should be addressed:

  1. Non-null assertions (!) are used extensively, which could lead to runtime errors if properties are undefined.
  2. Duplicate images are loaded for the same content, which is inefficient.
  3. The role="button" on images inside an anchor tag is redundant and potentially confusing for screen readers.

Consider this safer and more efficient approach:

-  template: `
-    <a
-      class="relative flex h-full items-center justify-center"
-      [href]="cardBanner().navigateTo!"
-    >
-      <aside class="absolute h-full w-full overflow-hidden rounded-lg">
-        <img
-          tabindex="0"
-          role="button"
-          class="!relative object-cover shadow-inner blur-xl"
-          [attr.aria-label]="cardBanner()!.alt!"
-          [alt]="cardBanner().alt!"
-          [ngSrc]="cardBanner().url!"
-          fill
-          priority
-        />
-      </aside>
-      <aside>
-        <img
-          tabindex="0"
-          role="button"
-          class="!relative"
-          [attr.aria-label]="cardBanner().alt!"
-          [alt]="cardBanner().alt!"
-          [ngSrc]="cardBanner().url!"
-          fill
-          priority
-        />
-      </aside>
-    </a>
+  template: `
+    <a
+      class="relative flex h-full items-center justify-center"
+      [href]="cardBanner().navigateTo"
+    >
+      <aside class="absolute h-full w-full overflow-hidden rounded-lg">
+        <img
+          *ngIf="cardBanner().url"
+          class="!relative object-cover shadow-inner blur-xl"
+          [attr.aria-label]="cardBanner().alt || 'Newsletter banner'"
+          [alt]="cardBanner().alt || 'Newsletter banner'"
+          [ngSrc]="cardBanner().url"
+          fill
+          priority
+        />
+      </aside>
+      <aside>
+        <img
+          *ngIf="cardBanner().url"
+          class="!relative"
+          [attr.aria-label]="cardBanner().alt || 'Newsletter banner'"
+          [alt]="cardBanner().alt || 'Newsletter banner'"
+          [ngSrc]="cardBanner().url"
+          fill
+          priority
+        />
+      </aside>
+    </a>

Alternatively, consider using CSS for the blurred background effect instead of loading the same image twice:

-  template: `
-    <a
-      class="relative flex h-full items-center justify-center"
-      [href]="cardBanner().navigateTo!"
-    >
-      <aside class="absolute h-full w-full overflow-hidden rounded-lg">
-        <img
-          tabindex="0"
-          role="button"
-          class="!relative object-cover shadow-inner blur-xl"
-          [attr.aria-label]="cardBanner()!.alt!"
-          [alt]="cardBanner().alt!"
-          [ngSrc]="cardBanner().url!"
-          fill
-          priority
-        />
-      </aside>
-      <aside>
-        <img
-          tabindex="0"
-          role="button"
-          class="!relative"
-          [attr.aria-label]="cardBanner().alt!"
-          [alt]="cardBanner().alt!"
-          [ngSrc]="cardBanner().url!"
-          fill
-          priority
-        />
-      </aside>
-    </a>
+  template: `
+    <a
+      class="relative flex h-full items-center justify-center"
+      [href]="cardBanner().navigateTo"
+    >
+      <div *ngIf="cardBanner().url" class="banner-container">
+        <img
+          class="banner-image"
+          [attr.aria-label]="cardBanner().alt || 'Newsletter banner'"
+          [alt]="cardBanner().alt || 'Newsletter banner'"
+          [ngSrc]="cardBanner().url"
+          fill
+          priority
+        />
+      </div>
+    </a>
+  `,
+  styles: [`
+    .banner-container {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+      border-radius: 0.5rem;
+    }
+    .banner-container::before {
+      content: "";
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background-image: inherit;
+      background-size: cover;
+      filter: blur(16px);
+      z-index: 0;
+    }
+    .banner-image {
+      position: relative;
+      z-index: 1;
+    }
+  `]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
template: `
<a
class="relative flex h-full items-center justify-center"
[href]="cardBanner().navigateTo!"
>
<aside class="absolute h-full w-full overflow-hidden rounded-lg">
<img
tabindex="0"
role="button"
class="!relative object-cover shadow-inner blur-xl"
[attr.aria-label]="cardBanner()!.alt!"
[alt]="cardBanner().alt!"
[ngSrc]="cardBanner().url!"
fill
priority
/>
</aside>
<aside>
<img
tabindex="0"
role="button"
class="!relative"
[attr.aria-label]="cardBanner().alt!"
[alt]="cardBanner().alt!"
[ngSrc]="cardBanner().url!"
fill
priority
/>
</aside>
</a>
`,
template: `
<a
class="relative flex h-full items-center justify-center"
[href]="cardBanner().navigateTo"
>
<aside class="absolute h-full w-full overflow-hidden rounded-lg">
<img
*ngIf="cardBanner().url"
class="!relative object-cover shadow-inner blur-xl"
[attr.aria-label]="cardBanner().alt || 'Newsletter banner'"
[alt]="cardBanner().alt || 'Newsletter banner'"
[ngSrc]="cardBanner().url"
fill
priority
/>
</aside>
<aside>
<img
*ngIf="cardBanner().url"
class="!relative"
[attr.aria-label]="cardBanner().alt || 'Newsletter banner'"
[alt]="cardBanner().alt || 'Newsletter banner'"
[ngSrc]="cardBanner().url"
fill
priority
/>
</aside>
</a>
`,

Comment on lines +70 to +78
// todo: remove this once the card banner is implemented
protected readonly cardBanner = computed(() => {
this._bannerStore.banners();
return {
url: 'https://img.freepik.com/free-psd/banner-template-black-friday-clearance_23-2148745448.jpg',
alt: 'Card Banner Example',
navigateTo: '/',
};
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Temporary banner implementation needs improvement

The current implementation calls this._bannerStore.banners() but doesn't use the result, returning a hardcoded object instead.

Consider one of these alternatives:

- // todo: remove this once the card banner is implemented
- protected readonly cardBanner = computed(() => {
-   this._bannerStore.banners();
-   return {
-     url: 'https://img.freepik.com/free-psd/banner-template-black-friday-clearance_23-2148745448.jpg',
-     alt: 'Card Banner Example',
-     navigateTo: '/',
-   };
- });
+ // todo: remove this hardcoded fallback once the card banner is fully implemented
+ protected readonly cardBanner = computed(() => {
+   const banners = this._bannerStore.banners();
+   return banners?.cardBanner || {
+     url: 'https://img.freepik.com/free-psd/banner-template-black-friday-clearance_23-2148745448.jpg',
+     alt: 'Card Banner Example',
+     navigateTo: '/',
+   };
+ });

Or if you want to keep it truly hardcoded for now:

- // todo: remove this once the card banner is implemented
- protected readonly cardBanner = computed(() => {
-   this._bannerStore.banners();
-   return {
-     url: 'https://img.freepik.com/free-psd/banner-template-black-friday-clearance_23-2148745448.jpg',
-     alt: 'Card Banner Example',
-     navigateTo: '/',
-   };
- });
+ // todo: replace with real banner implementation
+ protected readonly cardBanner = computed(() => ({
+   url: 'https://img.freepik.com/free-psd/banner-template-black-friday-clearance_23-2148745448.jpg',
+   alt: 'Card Banner Example',
+   navigateTo: '/',
+ }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// todo: remove this once the card banner is implemented
protected readonly cardBanner = computed(() => {
this._bannerStore.banners();
return {
url: 'https://img.freepik.com/free-psd/banner-template-black-friday-clearance_23-2148745448.jpg',
alt: 'Card Banner Example',
navigateTo: '/',
};
});
// todo: remove this hardcoded fallback once the card banner is fully implemented
protected readonly cardBanner = computed(() => {
const banners = this._bannerStore.banners();
return banners?.cardBanner || {
url: 'https://img.freepik.com/free-psd/banner-template-black-friday-clearance_23-2148745448.jpg',
alt: 'Card Banner Example',
navigateTo: '/',
};
});

Comment on lines +17 to +19
is_card_banner_displayed: boolean;
card_banner_image: number /* mediaId */;
card_banner_url: string /* url to navigate to after click */;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Card banner missing mobile image support

Unlike slider and top banner, the card banner only has a single image property without device-specific versions.

Is this intentional, or should card banners also support device-specific images like the other banner types?


🏁 Script executed:

#!/bin/bash
# Check if other components use device-specific images for card banners
rg -A 3 -B 3 "card_banner_image" --type ts

Length of output: 1306


I’d like to inspect the banner DTO definitions to confirm which types include device-specific images. Let me pull out the related sections for slider, mobile, and desktop in dtos.ts:


🏁 Script executed:

#!/bin/bash
# Show slider banner fields
rg -n "slider" -A5 -B5 libs/blog-bff/banners/api/src/lib/dtos.ts

# Check for mobile-specific image fields
rg -n "mobile" -A5 -B5 libs/blog-bff/banners/api/src/lib/dtos.ts

# Check for desktop-specific image fields
rg -n "desktop" -A5 -B5 libs/blog-bff/banners/api/src/lib/dtos.ts

Length of output: 1786


Ensure Device-Specific Images for Card Banners

The card_banner DTO currently only exposes a single card_banner_image, whereas both the slider and top banners define separate desktop and mobile image fields. To maintain consistency and support mobile layouts, please split the card banner image into desktop and mobile variants (or confirm and document that a single image is intentional).

• In libs/blog-bff/banners/api/src/lib/dtos.ts
Replace:

card_banner_image: number /* mediaId */;

With:

- card_banner_image: number /* mediaId */;
+ card_banner_image_desktop: number /* mediaId */;
+ card_banner_image_mobile: number /* mediaId */;

• In libs/blog-bff/banners/api/src/lib/mappers.ts
Update the cardBanner mapping to source both URLs:

    cardBanner: {
-     url: mediaDto.find((m) => m.id === dto.acf.card_banner_image)?.guid.rendered,
+     desktopUrl: mediaDto.find((m) => m.id === dto.acf.card_banner_image_desktop)
+            ?.guid.rendered,
+    mobileUrl: mediaDto.find((m) => m.id === dto.acf.card_banner_image_mobile)
+            ?.guid.rendered,
      navigateTo: dto.acf.card_banner_url,
    },

If the single-image design is intentional, please add a comment in the DTO or README explaining why card banners differ.

Comment on lines +25 to 45
...(dto.acf.is_top_banner_displayed && {
topBanner: {
url: mediaDto.find(
(media) => media.id === dto.acf.top_banner_image_desktop,
)?.guid.rendered,
alt: mediaDto.find(
(media) => media.id === dto.acf.top_banner_image_desktop,
)?.alt_text,
navigateTo: dto.acf.top_banner_image_url,
},
}),
...(dto.acf.is_card_banner_displayed && {
cardBanner: {
url: mediaDto.find((media) => media.id === dto.acf.card_banner_image)
?.guid.rendered,
alt: mediaDto.find((media) => media.id === dto.acf.card_banner_image)
?.alt_text,
navigateTo: dto.acf.card_banner_url,
},
}),
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Code duplication in media lookups

There's significant duplication in the media lookup logic across all banner types, and mobile images aren't being used.

Consider extracting the media lookup logic into a helper function and using mobile images for responsive design:

 export const toBanner = (
   dto: WPBannerDto,
   mediaDto: WPBannerMediaDto[],
 ): Banners => {
+  const findMedia = (mediaId: number) => mediaDto.find(media => media.id === mediaId);
+
   return {
     ...(dto.acf.is_slider_banner_displayed && {
       slider: {
         slideDisplayTimeMs: +dto.acf.display_time,
         slides: dto.acf.slides.map((slide) => {
-          const media = mediaDto.find(
-            (media) => media.id === slide.slide_image_desktop,
-          )!;
+          const desktopMedia = findMedia(slide.slide_image_desktop);
+          const mobileMedia = findMedia(slide.slide_image_mobile);
           return {
-            url: media?.guid.rendered,
-            alt: media?.alt_text,
+            url: desktopMedia?.guid.rendered,
+            mobileUrl: mobileMedia?.guid.rendered,
+            alt: desktopMedia?.alt_text,
             navigateTo: slide.slide_url,
           };
         }),
       },
     }),
     ...(dto.acf.is_top_banner_displayed && {
       topBanner: {
-        url: mediaDto.find(
-          (media) => media.id === dto.acf.top_banner_image_desktop,
-        )?.guid.rendered,
-        alt: mediaDto.find(
-          (media) => media.id === dto.acf.top_banner_image_desktop,
-        )?.alt_text,
+        url: findMedia(dto.acf.top_banner_image_desktop)?.guid.rendered,
+        mobileUrl: findMedia(dto.acf.top_banner_image_mobile)?.guid.rendered,
+        alt: findMedia(dto.acf.top_banner_image_desktop)?.alt_text,
         navigateTo: dto.acf.top_banner_image_url,
       },
     }),
     ...(dto.acf.is_card_banner_displayed && {
       cardBanner: {
-        url: mediaDto.find((media) => media.id === dto.acf.card_banner_image)
-          ?.guid.rendered,
-        alt: mediaDto.find((media) => media.id === dto.acf.card_banner_image)
-          ?.alt_text,
+        url: findMedia(dto.acf.card_banner_image)?.guid.rendered,
+        alt: findMedia(dto.acf.card_banner_image)?.alt_text,
         navigateTo: dto.acf.card_banner_url,
       },
     }),
   };
 };

Note: You would need to update the banner interfaces to include mobileUrl properties for this change.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants