Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

SSR outputs <!--container--> instead of HTML as per template syntax #60871

Copy link
Copy link
@davidbusuttil

Description

@davidbusuttil
Issue body actions

Which @angular/* package(s) are the source of the bug?

Don't known / other

Is this a regression?

No

Description

Introduction

I am using Firestore to store data for a CMS.

The data is stored in a field called blocks.

The CMS is divided into two components: Block and Inline.

Block Component

block.component.ts

import { ChangeDetectionStrategy, Component, input, ViewEncapsulation } from "@angular/core";
import { NgClass } from "@angular/common";

import { InlineComponent } from "../inline/inline.component";

import { JoinArrayPipe } from "../../pipes/join-array/join-array.pipe";
import { BlockFlowPipe } from "../../pipes/block-flow/block-flow.pipe";
import { BlockFlowRootPipe } from "../../pipes/block-flow-root/block-flow-root.pipe";
import { SectionPipe } from "../../pipes/section/section.pipe";
import { ArticlePipe } from "../../pipes/article/article.pipe";
import { AsidePipe } from "../../pipes/aside/aside.pipe";
import { MeshPipe } from "../../pipes/mesh/mesh.pipe";
import { HeadingPipe } from "../../pipes/heading/heading.pipe";
import { ParagraphPipe } from "../../pipes/paragraph/paragraph.pipe";
import { AddressPipe } from "../../pipes/address/address.pipe";
import { ListPipe } from "../../pipes/list/list.pipe";
import { MenuPipe } from "../../pipes/menu/menu.pipe";
import { DescriptionPipe } from "../../pipes/description/description.pipe";
import { SeparatorPipe } from "../../pipes/separator/separator.pipe";

import { Block } from "../../types/block/block";

@Component({
  selector: "[contentBlock]",
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [
    NgClass,
    InlineComponent,
    JoinArrayPipe,
    BlockFlowPipe,
    BlockFlowRootPipe,
    SectionPipe,
    ArticlePipe,
    AsidePipe,
    MeshPipe,
    HeadingPipe,
    ParagraphPipe,
    AddressPipe,
    ListPipe,
    MenuPipe,
    DescriptionPipe,
    SeparatorPipe,
  ],
  templateUrl: "./block.component.html",
})
export class BlockComponent {
  readonly blocks = input.required<Block[]>({ alias: "contentBlock" });
}

block.component.html

@for (block of blocks(); track $index) {
  @switch (block["@type"]) {
    @case ("block-flow") {
      @let blockFlow = block | blockFlow;
      <div
        class="block-flow"
        [ngClass]="blockFlow.ngClass"
        [attr.id]="blockFlow.id"
        [attr.lang]="blockFlow.lang"
        [attr.title]="blockFlow.title"
        [contentBlock]="blockFlow.blocks"
      ></div>
    }
    @case ("block-flow-root") {
      @let blockFlowRoot = block | blockFlowRoot;
      <div
        class="block-flow"
        [ngClass]="blockFlowRoot.ngClass"
        [attr.id]="blockFlowRoot.id"
        [attr.lang]="blockFlowRoot.lang"
        [attr.title]="blockFlowRoot.title"
        [contentBlock]="blockFlowRoot.blocks"
      ></div>
    }
    @case ("section") {
      @let section = block | section;
      <section
        class="section"
        [ngClass]="section.ngClass"
        [attr.id]="section.id"
        [attr.lang]="section.lang"
        [attr.title]="section.title"
        [contentBlock]="section.blocks"
      ></section>
    }
    @case ("article") {
      @let article = block | article;
      <article
        class="article"
        [ngClass]="article.ngClass"
        [attr.id]="article.id"
        [attr.lang]="article.lang"
        [attr.title]="article.title"
        [contentBlock]="article.blocks"
      ></article>
    }
    @case ("aside") {
      @let aside = block | aside;
      <aside
        class="aside"
        [ngClass]="aside.ngClass"
        [attr.id]="aside.id"
        [attr.lang]="aside.lang"
        [attr.title]="aside.title"
        [contentBlock]="aside.blocks"
      ></aside>
    }
    @case ("mesh") {
      @let mesh = block | mesh;
      <div class="mesh" [ngClass]="mesh.ngClass" [attr.id]="mesh.id" [attr.lang]="mesh.lang" [attr.title]="mesh.title">
        @for (meshItem of mesh.meshItems; track $index) {
          <div
            class="mesh-item"
            [ngClass]="meshItem.ngClass"
            [attr.id]="meshItem.id"
            [attr.lang]="meshItem.lang"
            [attr.title]="meshItem.title"
            [contentBlock]="meshItem.blocks"
          ></div>
        }
      </div>
    }
    @case ("heading") {
      @let heading = block | heading;
      @switch (heading.level) {
        @case (1) {
          <h1
            class="heading"
            [ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: heading.ngClass"
            [attr.id]="heading.id"
            [attr.lang]="heading.lang"
            [attr.title]="heading.title"
            [contentInline]="heading.inlines"
          ></h1>
        }
        @case (2) {
          <h2
            class="heading"
            [ngClass]="['x-small:size:small', 'small:size:medium', 'medium:size:large'] | joinArray: heading.ngClass"
            [attr.id]="heading.id"
            [attr.lang]="heading.lang"
            [attr.title]="heading.title"
            [contentInline]="heading.inlines"
          ></h2>
        }
        @case (3) {
          <h3
            class="heading"
            [ngClass]="['x-small:size:x-small', 'small:size:small', 'medium:size:medium'] | joinArray: heading.ngClass"
            [attr.id]="heading.id"
            [attr.lang]="heading.lang"
            [attr.title]="heading.title"
            [contentInline]="heading.inlines"
          ></h3>
        }
      }
    }
    @case ("paragraph") {
      @let paragraph = block | paragraph;
      <p
        class="paragraph"
        [ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: paragraph.ngClass"
        [attr.id]="paragraph.id"
        [attr.lang]="paragraph.lang"
        [attr.title]="paragraph.title"
        [contentInline]="paragraph.inlines"
      ></p>
    }
    @case ("address") {
      @let address = block | address;
      <p
        class="address"
        [ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: address.ngClass"
        [attr.id]="address.id"
        [attr.lang]="address.lang"
        [attr.title]="address.title"
        [contentInline]="address.inlines"
      ></p>
    }
    @case ("list") {
      @let list = block | list;
      @switch (list.type) {
        @case ("ordered") {
          <ol
            class="list"
            [ngClass]="
              ['x-small:size:medium', 'small:size:large', 'medium:size:x-large', 'x-small:type:ordered']
                | joinArray: list.ngClass
            "
            [attr.id]="list.id"
            [attr.lang]="list.lang"
            [attr.title]="list.title"
          >
            @for (listItem of list.listItems; track $index) {
              <li
                class="list-item"
                [ngClass]="listItem.ngClass"
                [attr.id]="listItem.id"
                [attr.lang]="listItem.lang"
                [attr.title]="listItem.title"
                [contentInline]="listItem.inlines"
              ></li>
            }
          </ol>
        }
        @case ("unordered") {
          <ul
            class="list"
            [ngClass]="
              ['x-small:size:medium', 'small:size:large', 'medium:size:x-large', 'x-small:type:unordered']
                | joinArray: list.ngClass
            "
            [attr.id]="list.id"
            [attr.lang]="list.lang"
            [attr.title]="list.title"
          >
            @for (listItem of list.listItems; track $index) {
              <li
                class="list-item"
                [ngClass]="listItem.ngClass"
                [attr.id]="listItem.id"
                [attr.lang]="listItem.lang"
                [attr.title]="listItem.title"
                [contentInline]="listItem.inlines"
              ></li>
            }
          </ul>
        }
      }
    }
    @case ("menu") {
      @let menu = block | menu;
      <ul
        class="menu"
        [ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: menu.ngClass"
        [attr.id]="menu.id"
        [attr.lang]="menu.lang"
        [attr.title]="menu.title"
      >
        @for (menuItem of menu.menuItems; track $index) {
          <li
            class="menu-item"
            [ngClass]="menuItem.ngClass"
            [attr.id]="menuItem.id"
            [attr.lang]="menuItem.lang"
            [attr.title]="menuItem.title"
            [contentInline]="menuItem.inlines"
          ></li>
        }
      </ul>
    }
    @case ("description") {
      @let description = block | description;
      <dl
        class="description"
        [ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: description.ngClass"
        [attr.id]="description.id"
        [attr.lang]="description.lang"
        [attr.title]="description.title"
      >
        @for (descriptionItem of description.descriptionItems; track $index) {
          @switch (descriptionItem.type) {
            @case ("term") {
              <dt
                class="description-term"
                [ngClass]="descriptionItem.ngClass"
                [attr.id]="descriptionItem.id"
                [attr.lang]="descriptionItem.lang"
                [attr.title]="descriptionItem.title"
                [contentInline]="descriptionItem.inlines"
              ></dt>
            }
            @case ("detail") {
              <dd
                class="description-detail"
                [ngClass]="descriptionItem.ngClass"
                [attr.id]="descriptionItem.id"
                [attr.lang]="descriptionItem.lang"
                [attr.title]="descriptionItem.title"
                [contentInline]="descriptionItem.inlines"
              ></dd>
            }
          }
        }
      </dl>
    }
    @case ("separator") {
      @let separator = block | separator;
      <hr class="separator" [ngClass]="separator.ngClass" />
    }
  }
}
<ng-content />

Inline Component

inline.component.ts

import { ChangeDetectionStrategy, Component, input, ViewEncapsulation } from "@angular/core";
import { NgClass, NgOptimizedImage } from "@angular/common";
import { RouterLink } from "@angular/router";

import { JoinArrayPipe } from "../../pipes/join-array/join-array.pipe";
import { InlineFlowPipe } from "../../pipes/inline-flow/inline-flow.pipe";
import { InlineFlowRootPipe } from "../../pipes/inline-flow-root/inline-flow-root.pipe";
import { HiddenPipe } from "../../pipes/hidden/hidden.pipe";
import { StrongPipe } from "../../pipes/strong/strong.pipe";
import { EmphasisPipe } from "../../pipes/emphasis/emphasis.pipe";
import { AbbreviationPipe } from "../../pipes/abbreviation/abbreviation.pipe";
import { TimePipe } from "../../pipes/time/time.pipe";
import { SubscriptPipe } from "../../pipes/subscript/subscript.pipe";
import { SuperscriptPipe } from "../../pipes/superscript/superscript.pipe";
import { LinkPipe } from "../../pipes/link/link.pipe";
import { AnchorPipe } from "../../pipes/anchor/anchor.pipe";
import { ButtonPipe } from "../../pipes/button/button.pipe";
import { ImagePipe } from "../../pipes/image/image.pipe";
import { LogoPipe } from "../../pipes/logo/logo.pipe";
import { IconPipe } from "../../pipes/icon/icon.pipe";
import { LinebreakPipe } from "../../pipes/linebreak/linebreak.pipe";

import { Inline } from "../../types/inline/inline";

@Component({
  selector: "[contentInline]",
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [
    NgClass,
    NgOptimizedImage,
    RouterLink,
    JoinArrayPipe,
    InlineFlowPipe,
    InlineFlowRootPipe,
    HiddenPipe,
    StrongPipe,
    EmphasisPipe,
    AbbreviationPipe,
    TimePipe,
    SubscriptPipe,
    SuperscriptPipe,
    LinkPipe,
    AnchorPipe,
    ButtonPipe,
    ImagePipe,
    LogoPipe,
    IconPipe,
    LinebreakPipe,
  ],
  templateUrl: "./inline.component.html",
})
export class InlineComponent {
  readonly inlines = input.required<Inline[]>({ alias: "contentInline" });
}

inline.component.html

@for (inline of inlines(); track $index) {
  @switch (inline["@type"]) {
    @case ("inline-flow") {
      @let inlineFlow = inline | inlineFlow;
      <span
        class="inline-flow"
        [ngClass]="inlineFlow.ngClass"
        [attr.id]="inlineFlow.id"
        [attr.lang]="inlineFlow.lang"
        [attr.title]="inlineFlow.title"
        [contentInline]="inlineFlow.inlines ?? []"
        >{{ inlineFlow.text }}</span
      >
    }
    @case ("inline-flow-root") {
      @let inlineFlowRoot = inline | inlineFlowRoot;
      <span
        class="inline-flow-root"
        [ngClass]="inlineFlowRoot.ngClass"
        [attr.id]="inlineFlowRoot.id"
        [attr.lang]="inlineFlowRoot.lang"
        [attr.title]="inlineFlowRoot.title"
        [contentInline]="inlineFlowRoot.inlines ?? []"
        >{{ inlineFlowRoot.text }}</span
      >
    }
    @case ("hidden") {
      @let hidden = inline | hidden;
      <span
        class="hidden"
        [ngClass]="hidden.ngClass"
        [attr.id]="hidden.id"
        [attr.lang]="hidden.lang"
        [attr.title]="hidden.title"
        [contentInline]="hidden.inlines ?? []"
        >{{ hidden.text }}</span
      >
    }
    @case ("strong") {
      @let strong = inline | strong;
      <strong
        class="strong"
        [ngClass]="strong.ngClass"
        [attr.id]="strong.id"
        [attr.lang]="strong.lang"
        [attr.title]="strong.title"
        [contentInline]="strong.inlines ?? []"
        >{{ strong.text }}</strong
      >
    }
    @case ("emphasis") {
      @let emphasis = inline | emphasis;
      <em
        class="emphasis"
        [ngClass]="emphasis.ngClass"
        [attr.id]="emphasis.id"
        [attr.lang]="emphasis.lang"
        [attr.title]="emphasis.title"
        [contentInline]="emphasis.inlines ?? []"
        >{{ emphasis.text }}</em
      >
    }
    @case ("abbreviation") {
      @let abbreviation = inline | abbreviation;
      <abbr
        class="abbreviation"
        [ngClass]="abbreviation.ngClass"
        [attr.id]="abbreviation.id"
        [attr.lang]="abbreviation.lang"
        [attr.title]="abbreviation.title"
        [contentInline]="abbreviation.inlines ?? []"
        >{{ abbreviation.text }}</abbr
      >
    }
    @case ("time") {
      @let time = inline | time;
      <time
        class="time"
        [ngClass]="time.ngClass"
        [attr.dateTime]="time.dateTime"
        [attr.id]="time.id"
        [attr.lang]="time.lang"
        [attr.title]="time.title"
        [contentInline]="time.inlines ?? []"
        >{{ time.text }}</time
      >
    }
    @case ("subscript") {
      @let subscript = inline | subscript;
      <sub
        class="subscript"
        [ngClass]="subscript.ngClass"
        [attr.id]="subscript.id"
        [attr.lang]="subscript.lang"
        [attr.title]="subscript.title"
        [contentInline]="subscript.inlines ?? []"
        >{{ subscript.text }}</sub
      >
    }
    @case ("superscript") {
      @let superscript = inline | superscript;
      <sup
        class="superscript"
        [ngClass]="superscript.ngClass"
        [attr.id]="superscript.id"
        [attr.lang]="superscript.lang"
        [attr.title]="superscript.title"
        [contentInline]="superscript.inlines ?? []"
        >{{ superscript.text }}</sup
      >
    }
    @case ("link") {
      @let link = inline | link;
      @switch (link.type) {
        @case ("router") {
          <a
            class="link"
            [ngClass]="['x-small:color:primary'] | joinArray: link.ngClass"
            [routerLink]="link.routerLink"
            [fragment]="link.fragment"
            [queryParams]="link.queryParams"
            [queryParamsHandling]="link.queryParamsHandling"
            [attr.id]="link.id"
            [attr.lang]="link.lang"
            [attr.title]="link.title"
            [contentInline]="link.inlines ?? []"
            >{{ link.text }}</a
          >
        }
        @case ("href") {
          <a
            class="link"
            [ngClass]="['x-small:color:primary'] | joinArray: link.ngClass"
            [attr.href]="link.href"
            [attr.target]="link.target"
            [attr.id]="link.id"
            [attr.lang]="link.lang"
            [attr.title]="link.title"
            [contentInline]="link.inlines ?? []"
            >{{ link.text }}</a
          >
        }
      }
    }
    @case ("anchor") {
      @let anchor = inline | anchor;
      @switch (anchor.type) {
        @case ("router") {
          <a
            class="anchor"
            [ngClass]="['x-small:color:primary'] | joinArray: anchor.ngClass"
            [routerLink]="anchor.routerLink"
            [fragment]="anchor.fragment"
            [queryParams]="anchor.queryParams"
            [queryParamsHandling]="anchor.queryParamsHandling"
            [attr.id]="anchor.id"
            [attr.lang]="anchor.lang"
            [attr.title]="anchor.title"
            [contentInline]="anchor.inlines ?? []"
            >{{ anchor.text }}</a
          >
        }
        @case ("href") {
          <a
            class="anchor"
            [ngClass]="['x-small:color:primary'] | joinArray: anchor.ngClass"
            [attr.href]="anchor.href"
            [attr.target]="anchor.target"
            [attr.id]="anchor.id"
            [attr.lang]="anchor.lang"
            [attr.title]="anchor.title"
            [contentInline]="anchor.inlines ?? []"
            >{{ anchor.text }}</a
          >
        }
      }
    }
    @case ("button") {
      @let button = inline | button;
      @switch (button.type) {
        @case ("router") {
          <a
            class="button"
            [ngClass]="['x-small:color:2-inverse', 'x-small:background-color:primary'] | joinArray: button.ngClass"
            [routerLink]="button.routerLink"
            [fragment]="button.fragment"
            [queryParams]="button.queryParams"
            [queryParamsHandling]="button.queryParamsHandling"
            [attr.id]="button.id"
            [attr.lang]="button.lang"
            [attr.title]="button.title"
            [contentInline]="button.inlines ?? []"
            >{{ button.text }}</a
          >
        }
        @case ("href") {
          <a
            class="button"
            [ngClass]="['x-small:color:2-inverse', 'x-small:background-color:primary'] | joinArray: button.ngClass"
            [attr.href]="button.href"
            [attr.target]="button.target"
            [attr.id]="button.id"
            [attr.lang]="button.lang"
            [attr.title]="button.title"
            [contentInline]="button.inlines ?? []"
            >{{ button.text }}</a
          >
        }
      }
    }
    @case ("image") {
      @let image = inline | image;
      <img
        class="image"
        [ngClass]="image.ngClass"
        [ngSrc]="image.ngSrc"
        [width]="image.width"
        [height]="image.height"
        [priority]="image.priority"
        [placeholder]="image.placeholder"
        [attr.alt]="image.alt"
        [attr.id]="image.id"
        [attr.lang]="image.lang"
        [attr.title]="image.title"
      />
    }
    @case ("logo") {
      @let logo = inline | logo;
      <svg
        class="logo"
        [ngClass]="logo.ngClass"
        [attr.viewBox]="logo.viewBox"
        [attr.xmlns]="'http://www.w3.org/2000/svg'"
        [attr.aria-hidden]="true"
      >
        @for (logoItem of logo.logoItems; track $index) {
          <path [attr.d]="logoItem.d" [attr.fill]="logoItem.fill" />
        }
      </svg>
    }
    @case ("icon") {
      @let icon = inline | icon;
      <svg
        class="icon"
        [ngClass]="icon.ngClass"
        [attr.viewBox]="icon.viewBox"
        [attr.xmlns]="'http://www.w3.org/2000/svg'"
        [attr.aria-hidden]="true"
      >
        @for (iconItem of icon.iconItems; track $index) {
          <path [attr.d]="iconItem.d" [attr.fill]="iconItem.fill" />
        }
      </svg>
    }
    @case ("linebreak") {
      @let linebreak = inline | linebreak;
      <br class="linebreak" [ngClass]="linebreak.ngClass" />
    }
  }
}
<ng-content />

Generating the Content

When the blocks are retrieved from Firestore, they are simply loaded as follows (example):

<article class="article" [contentBlock]="document.blocks"></article>

The Issue

After a number of iterations, SSR stops rendering the HTML, and instead, it outputs a <!--container--> instead, which is simply used for hydration.

What Should Happen

SSR should wait until all HTML is rendered and avoid outputing <!--container--> as this is not good for SEO.

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw


Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 19.2.7
Node: 22.14.0
Package Manager: npm 10.9.2
OS: win32 x64

Angular: 19.2.6
... common, compiler, compiler-cli, core, platform-browser
... platform-browser-dynamic, platform-server, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1902.7
@angular-devkit/build-angular   19.2.7
@angular-devkit/core            19.2.7
@angular-devkit/schematics      19.2.7
@angular/cli                    19.2.7
@angular/fire                   19.1.0
@angular/ssr                    19.2.7
@schematics/angular             19.2.7
rxjs                            7.8.2
typescript                      5.8.3

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: commonIssues related to APIs in the @angular/common packageIssues related to APIs in the @angular/common packagearea: serverIssues related to server-side renderingIssues related to server-side renderingcommon: image directiveneeds reproductionThis issue needs a reproduction in order for the team to investigate furtherThis issue needs a reproduction in order for the team to investigate further

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.