How to include arbitrary tags in the head of HTML documents

I’m trying to put an <esi:include src=…/> tag into my <head> section, so I defined this expander first:

html_write:expand(esi(Handler)) -->
     { http_link_to_id(Handler, [], HREF) },
    "<esi:include src=\"", [HREF], "\"/>".

Then I modify the ‘sheets’ page style to include this in the header

user:head(sheets, Head) -->
    html([ \html_receive(head),
           Head,
           esi(shared_head_esi),
           meta(two)
         ]).

I render the page like this:

sheets_homepage(Request) :-
    reply_html_page(
        sheets,
        title('Sheets'),
        [ \header_fragment(Request),
          \body_fragment(Request),
          \footer_fragment(Request)]).

The result is that the esi-tag is put into the body-tag and wraps everything in there, which doesn’t make sense. (BTW: the expander always creates <esi:...></esi:...> tag pairs but I just need a single <esi: .../> tag. Luckily the upstream processor seems to handle it. How do I generate single tags?)

I experimented a bit and it seems to me that the library throws out the tags it doesn’t want in the head!?! If so, how do I overwrite this behavior? How else could this be done?

I tried debugging but it does’t enter into the expansion process (presumably it’s written in C?).

The expand/2 seems popular. It isn’t intended for that purpose. The way to get things into the HTML head is to use html_post//2, I think this might do:

html_post(head, 'esi:include'(src(URL), []))

Not sure whether : is passed correctly. Is it even legal without using the old XHTML? If that fails you can opt for (not tested)

html_post(head, \["<esi:include src=\"...\"/>"])

The \[...] construct passes arbitrary strings to the output and can be used as an emergency to generate invalid HTML.

Next, to generate uniform pages, have a look at reply_html_page/3. That allows specifying a page style and a hook that provides all the page decoration depending on this style.

I have rewritten my code to use html_post(head…), but it still puts the <esi:include>... at the beginning of body:

esi(URL) -->
    html_post(head, 'esi:include'(src(URL), [])).

sheets_homepage(Request) :-
    http_link_to_id(shared_head_esi, [], URL),
    reply_html_page(
        sheets,
        [ \esi(URL),
          title('Sheets')
        ],
        [ \header_fragment(Request),
          \body_fragment(Request),
          \footer_fragment(Request)]).

This results in

<html><head><title>Sheets</title>
</head><body><esi:include src="/esi/shared_head"></esi:include>
<header>
<div>LOGO</div>
<div>abc</div>
<footer>baz</footer>
</body></html>

I also tried with

esiraw(URL) -->
    { format(atom(ESI), "<esi:include src=\"~s\"/>", [URL]) },
    html_post(head, \[ESI]).

which give me the same result.

Where am I supposed to call the http_post? I also called it from the user:head hook which didn’t output the tag at all. Then I also tried with a different topic for the receive:

user:head(sheets, Head) -->
    { http_link_to_id(shared_head_esi, [], URL) },
    html_post(esi, 'esi:include'(src(URL), [])),
    html(Head).

sheets_homepage(Request) :-
    reply_html_page(
        sheets,
        [  title('Sheets'),
          \html_receive(esi)
        ], [...

This also puts it into the <body>.

If I read this correctly, you are posting from the head to the head. That apparently goes wrong. If you use the 2nd argument of reply_html_page/3, just putting a term 'esi:include'(src(URL), []) in the same list as the title element should be fine. If you use the user:head route, you just need to insert the element there, so I think this should work:

user:head(sheets, Head) -->
    { http_link_to_id(shared_head_esi, [], URL) },
    html('esi:include'(src(URL), [])),
    html(Head).

Note that this is all written in Prolog, but library(http/html_write) sets generate_debug_info to false as tracing inside gets really confusing. You can comment that line in the library and debug as normal.

Unfortunately nothing I do works. I have prepared a minimal test case:

:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- http_handler(root(.), sheets_homepage, []).

esi_server(Port) :-
    http_server(http_dispatch, [port(Port)]).

sheets_homepage(_Request) :-
    reply_html_page(
        sheets,
        [ title('Sheets'),
          html(meta(foo)), % This is included correctly
          % These are never included, they get pushed to the body.
          'esi:include'(src('/shared_head_esi'), []),
          html('esi:include'(src('/shared_head_esi'), []))
        ], [ span(foo) ]).

Note that the meta tag written by the library

<meta http-equiv="content-type" content="text/html; charset=UTF-8">

also gets pushed into the body if any “invalid” head tag is introduced. It’s like the library encounters an unknown header tag and then says to itself: “Yup that’s it, lets move on to the body”.

It occured to me that the raw-include might work here but now the meta tag by the library is correctly put in the head. But the esi tag now wraps the complete body!?!

esiraw(URL) -->
    { format(atom(ESI), "<esi:include src=\"~s\"/>", [URL]) },
    html( \[ESI]).

sheets_homepage(_Request) :-
    reply_html_page(
        sheets,
        [ title('Sheets'),
          \esiraw('/shared_head_esi')
        ], [ span(foo) ]).

This works for me:

sheets_homepage(_Request) :-
    reply_html_page(
        sheets,
        [ title('Sheets'),
          meta([], []),
          'esi:include'(src('/shared_head_esi'), [])
        ], [ span(foo) ]).

Note that html is a predicate. You don’t want that in the head, as that would create an html element.

OK, this is embarrassing!

All this time I was looking at the DOM in the the browser inspector and THAT moves the invalid <head> tags into the body. :man_facepalming: :rofl: Lesson: use “View source” during development!

Thank you for helping me through this.