Skip to content

[ssr] breaks when using expressions in <title> and <html> tags #2441

New issue

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

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

Already on GitHub? Sign in to your account

Closed
2 tasks done
daKmoR opened this issue Jan 24, 2022 · 17 comments · Fixed by #2459
Closed
2 tasks done

[ssr] breaks when using expressions in <title> and <html> tags #2441

daKmoR opened this issue Jan 24, 2022 · 17 comments · Fixed by #2459
Assignees

Comments

@daKmoR
Copy link
Contributor

daKmoR commented Jan 24, 2022

Description

A clear and concise description of what the bug is.

Steps to Reproduce

  1. Write this code in test.js
import { Readable } from "stream";
import { render } from "@lit-labs/ssr/lib/render-with-global-dom-shim.js";
import { html } from 'lit-html';

const template = ({ title }) => html`
  <html>
    <head>
      <!-- remove the next line and it works -->
      <title>${title}</title>
    </head>
    <body>
      <p>${title}</p>
    </body>
  </html>
`;

const ssrResult = render(template({ title: "hello" }));



/***** HELPERS */
const stream = Readable.from(ssrResult);
const ssrString = await streamToString(stream);

console.log(ssrString);

function streamToString(stream) {
  const chunks = [];
  return new Promise((resolve, reject) => {
    stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
    stream.on("error", (err) => reject(err));
    stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
  });
}
  1. execute it via node test.js

Expected Results

the ssr output

Actual Results

$ node test.js
node:internal/process/esm_loader:94
    internalBinding('errors').triggerUncaughtException(
                              ^

Error: unexpected final partIndex: 1 !== 2
    at renderTemplateResult (file:///html/test-ssr/node_modules/@lit-labs/ssr/lib/render-lit-html.js:514:15)
    at renderTemplateResult.next (<anonymous>)
    at renderValue (file:///html/test-ssr/node_modules/@lit-labs/ssr/lib/render-lit-html.js:354:16)
    at renderValue.next (<anonymous>)
    at render (file:///html/test-ssr/node_modules/@lit-labs/ssr/lib/render-lit-html.js:336:12)
    at render.next (<anonymous>)
    at next (node:internal/streams/from:86:20)
    at Readable.readable._read (node:internal/streams/from:53:7)
    at Readable.read (node:internal/streams/readable:487:10)
    at flow (node:internal/streams/readable:1012:34)

Browsers Affected

  • Node 17
  • Node 16
@aomarks aomarks moved this to Todo in Lit Project Board Jan 24, 2022
@aomarks aomarks self-assigned this Jan 24, 2022
@aomarks
Copy link
Member

aomarks commented Jan 24, 2022

Thanks for the report, will look into this soon.

@daKmoR
Copy link
Contributor Author

daKmoR commented Jan 26, 2022

@aomarks attributes for the html tag is another case that breaks

const template = ({ lang }) => html`
  <html lang="${lang}">
  [...]
`;  

Do you want it as a separate issue or is this good enough?

@aomarks
Copy link
Member

aomarks commented Jan 26, 2022

Do you want it as a separate issue or is this good enough?

This one is fine, I'll update the title.

@aomarks aomarks changed the title [lit-ssr] breaks when using expressions in a <title> tag [lit-ssr] breaks when using expressions in <title> and <html> tags Jan 26, 2022
@aomarks aomarks changed the title [lit-ssr] breaks when using expressions in <title> and <html> tags [ssr] breaks when using expressions in <title> and <html> tags Jan 26, 2022
@justinfagnani
Copy link
Collaborator

For <title> this is because the tag isn't listed in our raw text elements: https://github.com/lit/lit/blob/main/packages/lit-html/src/lit-html.ts#L238

Raw text elements won't parse out comment-like strings, they <!-- ... --> just becomes text content, so we can't find markers and the part list gets corrupted. https://html.spec.whatwg.org/multipage/syntax.html#raw-text-elements

My hunch for the <html> tag is that something else is going wrong with the parse5 entrypoint we're using and we may be throwing the element away.

@justinfagnani
Copy link
Collaborator

@aomarks I can fix <title> real quick

@aomarks
Copy link
Member

aomarks commented Jan 26, 2022

@aomarks I can fix <title> real quick

Sure, I think @daKmoR might have started on a PR as well?

@daKmoR
Copy link
Contributor Author

daKmoR commented Jan 27, 2022

I will only list stuff I find for now 😬

another breaking template

const template = ({ foo }) => html`
  [...]
  <body foo="${foo}">
  [...]
`;  

probably a similar case to html tag?

Repository owner moved this from Todo to Done in Lit Project Board Jan 27, 2022
@justinfagnani
Copy link
Collaborator

Reopening to track the <html> issue.

@justinfagnani justinfagnani reopened this Jan 27, 2022
@justinfagnani
Copy link
Collaborator

I'm not entirely sure that we can support bindings to <html> and <body> in actual lit-html templates. lit-html can't really create those nodes - they exist before lit-html gets a chance to run - and we don't have a general way to transpose bindings onto existing elements. One the server you might be better with plain template strings. I've generally done something like this in tests and demos:

import {render} from '../../lib/render-with-global-dom-shim.js';
import {template} from './app.js';

export function* renderPage(data) {
  yield `
    <html lang=${data.lang}>
      <body>
  `;
  yield * render(template(data));
  yield `
      </body>
    </html>
  `;
}

@bennypowers
Copy link
Collaborator

should <html> attrs be a special case in ssr-and-hydrate-only?

@daKmoR
Copy link
Contributor Author

daKmoR commented Jan 27, 2022

I think needing to use a different template "system" for different parts of the dom seems very counterintuitive...

would lead to user documentation like this:

User Docs

Generally, you are required to use the template literal html

import { html } from 'lit-html';

pageTemplate.addBodyTopContent(html`
   <p>stuff</p>
`);

However, if you want to add something to the html outside of the body you must use plain html

// warning using html` here will break
pageTemplate.addHeaderContent(`
  <meta content="website" property="og:type">
`);

Notes

that seems like a hard sell 😅

Especially as it will require you to manually join arrays, prevents you from using directives and generally, you can not reuse any of the existing knowledge of all the other templates...

We could provide a string template literal import { stringHtml } from 'foo'; but even then you won't get code highlighting...

tricky situation - I wonder if that is "impossible" in next, nuxt, ... as well? 🤔

@justinfagnani
Copy link
Collaborator

I understand that it would be frustrating, but I'm guessing that there might be some very tricky issues to solve here. lit-ssr is designed to SSR templates that would have been client-rendered. You wouldn't have rendered <html>, <head>, and <body> with lit-html because those elements are implicitly created by the browser before code runs. We'll try to solve this, but it can't come at the cost of lots of new code in the core library.

@daKmoR
Copy link
Contributor Author

daKmoR commented Jan 28, 2022

ok, I have a rather simple workaround... (at least for a static site generator) which I think would be ok as an end-user API

so for tags like html, body, ... you do NOT use them in the template but instead, you use <*-server-only>.

so a page template would look something like this

render(data) {
  return html`
    <!DOCTYPE html>
    <html-server-only lang="${data.lang}">
      <head>...</head>
      <body-server-only class=${classMap(data.cssClasses)}>
        ${data.content()}
      </body-server-only>
    </html-server-only>
  `;
}

with that, the lit ssr rendering works as expected and before we actually save the file to disk we remove the -server-only from the tags.

this probably means you can NOT hydrate templates if they or their sub-templates are using a <*-server-only> tag - which is imho reasonable and could be explained once in a generic section.

that is good enough for me for now 🤗

if lit could update a full html page if new data or new templates get rendered... then it would enable SPA like navigation by sending a new template result and rendering it... might be interesting but probably a different issue/discussion

@aomarks aomarks moved this from Done to In Progress in Lit Project Board Jan 28, 2022
@aomarks aomarks moved this from ⏱ In Progress to 📋 Triaged in Lit Project Board Jun 10, 2022
@aomarks aomarks removed their assignment Jan 18, 2023
@rictic
Copy link
Collaborator

rictic commented Jun 28, 2023

Augustine and I have been talking about this, and I think there's value in having server-only templates that can render the full page but that don't emit parts for hydration (i.e. we won't emit any Lit comments), and that do support rendering the entire document. We're looking into some approaches for this.

@justinfagnani
Copy link
Collaborator

We have a customer at a discussion site who wants SSR-only templates too, to replace their custom server template system.

@AndrewJakubowicz
Copy link
Contributor

lit-labs/ssr@3.2.0 added server only templates which support <title> and <html> for templates that only render during SSR.

Circling back to the example given in the issue description, the server only template version would be:

import {render, html} from '@lit-labs/ssr';

const template = ({ title, lang }) => html`
  <!DOCTYPE html>
  <html lang="${lang}">
    <head>
      <!-- Works within a server only template -->
      <title>${title}</title>
    </head>
    <body>
      <p>${title}</p>
    </body>
  </html>
`;

This template is rendered by Lit SSR as plain HTML (without any Lit marker comments). Therefore it cannot be hydrated or re-rendered on the client. This is by design as it's not possible to hydrate <!doctype html> or <title>.
One final note, server templates can contain client templates (e.g. html imported from 'lit'), but client templates cannot contain server templates.

Server only template documentation

Try them out, and please file issues (or re-open this one) if issues persist or new ones arise! Thank you!

@niedzielski
Copy link

I think you can wrap title (and also script tags) in an unsafeStatic():

  // import { html, unsafeStatic } from 'lit/static-html.js' // get the unstatic version of html
  let hello = 'hello'
  return html`
    ${unsafeStatic(
      `<title>${hello}</title>`,
    )}
  `

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

7 participants