diff --git a/projects/ppwcode/ng-common/src/lib/pipes/sanitize-html.pipe.spec.ts b/projects/ppwcode/ng-common/src/lib/pipes/sanitize-html.pipe.spec.ts
new file mode 100644
index 00000000..136c40e6
--- /dev/null
+++ b/projects/ppwcode/ng-common/src/lib/pipes/sanitize-html.pipe.spec.ts
@@ -0,0 +1,50 @@
+import { SecurityContext } from '@angular/core'
+import { TestBed } from '@angular/core/testing'
+import { DomSanitizer } from '@angular/platform-browser'
+import { PpwSanitizeHtmlPipe } from './sanitize-html.pipe'
+
+describe('PpwSanitizeHtmlPipe', () => {
+ let pipe: PpwSanitizeHtmlPipe
+ let sanitizer: DomSanitizer
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [PpwSanitizeHtmlPipe]
+ })
+
+ pipe = TestBed.inject(PpwSanitizeHtmlPipe)
+ sanitizer = TestBed.inject(DomSanitizer)
+ })
+
+ it('sanitizes HTML before trusting it', () => {
+ const sanitizeSpy = vi.spyOn(sanitizer, 'sanitize')
+ const bypassSpy = vi.spyOn(sanitizer, 'bypassSecurityTrustHtml')
+ const html = '
'
+
+ pipe.transform(html)
+
+ expect(sanitizeSpy).toHaveBeenCalledWith(SecurityContext.HTML, html)
+ expect(bypassSpy).toHaveBeenCalledWith('')
+ })
+
+ it('strips unsafe HTML content from the result', () => {
+ const html = ''
+
+ const result = pipe.transform(html)
+
+ expect(result).toEqual({
+ changingThisBreaksApplicationSecurity: ''
+ })
+ })
+
+ it('returns trusted empty HTML for null input', () => {
+ const bypassSpy = vi.spyOn(sanitizer, 'bypassSecurityTrustHtml')
+
+ const result = pipe.transform(null)
+
+ expect(bypassSpy).toHaveBeenCalledWith('')
+ expect(result).toEqual({
+ changingThisBreaksApplicationSecurity: ''
+ })
+ })
+})
diff --git a/projects/ppwcode/ng-common/src/lib/pipes/sanitize-html.pipe.ts b/projects/ppwcode/ng-common/src/lib/pipes/sanitize-html.pipe.ts
new file mode 100644
index 00000000..5f5ec20c
--- /dev/null
+++ b/projects/ppwcode/ng-common/src/lib/pipes/sanitize-html.pipe.ts
@@ -0,0 +1,34 @@
+import { inject, Pipe, PipeTransform, SecurityContext } from '@angular/core'
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
+
+/**
+ * Sanitizes an HTML string with Angular's built-in HTML sanitizer and returns the
+ * sanitized result as `SafeHtml`.
+ *
+ * The pipe first removes unsafe markup such as scripts and dangerous attributes
+ * by calling `DomSanitizer.sanitize(SecurityContext.HTML, value)`. It then wraps
+ * the sanitized output with `bypassSecurityTrustHtml` so Angular can bind the
+ * result to `[innerHTML]` without sanitizing it a second time.
+ *
+ * Use this pipe when the source value may contain user-provided or otherwise
+ * untrusted HTML and you want to render the remaining safe markup in the view.
+ * Typical usage is:
+ *
+ * ``
+ *
+ * Do not use this pipe to preserve unsafe markup. Any content removed by the
+ * sanitizer is intentionally discarded before the result is marked as trusted.
+ */
+@Pipe({
+ name: 'ppwSanitizeHtml',
+ standalone: true
+})
+export class PpwSanitizeHtmlPipe implements PipeTransform {
+ private readonly sanitizer = inject(DomSanitizer)
+
+ /** Returns trusted HTML that has already been sanitized for safe rendering. */
+ transform(value: string | null): SafeHtml {
+ const sanitizedValue = this.sanitizer.sanitize(SecurityContext.HTML, value) ?? ''
+ return this.sanitizer.bypassSecurityTrustHtml(sanitizedValue)
+ }
+}
diff --git a/projects/ppwcode/ng-common/src/public-api.ts b/projects/ppwcode/ng-common/src/public-api.ts
index d4b17482..d0711c5e 100644
--- a/projects/ppwcode/ng-common/src/public-api.ts
+++ b/projects/ppwcode/ng-common/src/public-api.ts
@@ -14,6 +14,7 @@ export * from './lib/mixins/handle-subscriptions'
export * from './lib/mixins/responsive-observers'
export * from './lib/mixins/track-pending'
export * from './lib/pipes/api-translate.pipe'
+export * from './lib/pipes/sanitize-html.pipe'
export * from './lib/rxjs-operators/truthy-first'
export * from './lib/rxjs-operators/truthy-filter'
export * from './lib/storage/local-storage'