Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Add simpler API for invoking prerendering without using tag helper #607

Closed
SteveSandersonMS opened this issue Jan 26, 2017 · 14 comments
Closed

Comments

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Jan 26, 2017

Although the asp-prerender-module tag helper is usually the most convenient and natural way to invoke prerendering, there are some cases where you'd want to do it from controller code, or from a code block in your Razor page. For example, you might want to receive back additional custom data values from prerendering, and then maybe use them to set the page title or similar. Or you might want to change how the globals values are serialized and delivered to the client.

Currently it is possible to do this by invoking Prerenderer.RenderToString directly. However it's inconvenient, because you have to duplicate some non-obvious logic from the tag helper. For example, to run prerendering from controller code, currently you need something like this:

var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>();
var unencodedPathAndQuery = requestFeature.RawTarget;
var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}";
var prerenderResult = await Prerenderer.RenderToString(
    hostEnv.ContentRootPath,
    nodeServices,
    new JavaScriptModuleExport("ClientApp/dist/main-server"),
    unencodedAbsoluteUrl,
    unencodedPathAndQuery,
    /* custom data parameter */ null,
    /* timeout milliseconds */ 15*1000,
    Request.PathBase.ToString()
);

ViewData["SpaHtml"] = prerenderResult.Html;
ViewData["Title"] = prerenderResult.Globals["pageTitle"];

We should consider migrating this logic out of PrerenderTagHelper and into some new DI service class that PrerenderTagHelper starts using. Then developers will be able to grab instances of that new service class and do custom prerendering much more easily. Ideally, you'd be able to do something like this directly in a Razor view:

@inject Microsoft.AspNetCore.SpaServices.Prerendering.IPrerenderer prerenderer
@{
    var prerenderResult = await prerenderer.RenderToString("ClientApp/dist/main-server");
}
...
<title>@prerenderResult.Globals["pageTitle"]</title>
<app>@Html.Raw(prerenderResult.Html)</app>

There could be an options param for overriding things like the request URL, timeout, etc., but by default it would use values from the context automatically like asp-prerender-module already does.

@MarkPieszak
Copy link
Contributor

Thank you for looking into this @SteveSandersonMS ! 💯 💯
It seemed like a change that would have to happen with the underlying Prerenderer, tried everything on my end with no luck.

Also for those wondering what the use-case for this is:

Since we're allowing .NET (MVC) to have control over the overall <html>, when we prerender applications through prerenderer.RenderToString, although we can pass variables through globals onto the window., we're still unable to effect the html that's output from the MVC View.

Being able to manipulate <title> and <meta> tags is crucial for SEO when it comes to server-rendered pages (ie: social-media deeplinking, link previews, etc)

Your example looks like it would be perfect for this!

@inject Microsoft.AspNetCore.SpaServices.Prerendering.IPrerenderer prerenderer
@{
    var prerenderResult = await prerenderer.RenderToString("ClientApp/dist/main-server");
}
...
<html>
  <head>
    <title>@prerenderResult.Globals["pageTitle"]</title>

    <!-- meta tags -->
    <meta name="description" content='@prerenderResult.Globals["meta"]["description"]'>
    <meta name="keywords" content='@prerenderResult.Globals["meta"]["keywords"]'>

    <!-- and so on, for the remaining meta / FB: / og: / twitter: meta tags -->
    <meta name="fb:app_id" content='@prerenderResult.Globals["meta"]["fb"]["appid"]'>

  </head>
  <body>
    <app>@Html.Raw(prerenderResult.Html)</app>
  </body>
</html>

One last question, would this be an alternative to the way we're currently doing things, or would all the templates need to be changed in this way? I feel that ideally they might as well all change, because eventually everyone will get to the point where they realize they need to update the Title / etc.


(On the Angular side) I have a way of extracting the title/meta data from the serialization, but I'm going to look into making it even easier & cleaner.

Ideally it'd be great to have Universal do something like this, I brought it up that normal meta manipulation won't work with .NET for the reasons above, so I'm hoping I can somehow convince them to add this in there.

    platform.serializeModule(AppModule)).then( result => {
        resolve({ 
           html: result.html, 
           globals: result.meta // <--
        });
    };

@hheexx
Copy link

hheexx commented Feb 22, 2017

@MarkPieszak Is there any reason not to switch to code you provided in last post?

Only thing that comes to my mind is asp-append-version attribute that is used for cache busting.

@MarkPieszak
Copy link
Contributor

MarkPieszak commented Feb 22, 2017

The code will actually be slightly similar to the above :)

Of course some of those APIs have changed in Angular 4.x since I wrote that, but so far I have it working, and am able to extract out styles (to be placed in the correct location), Title, and only place in the correct Html from the serialized application (instead of the entire <html><head>... etc)

We're still in beta, so I'm trying to work everything out and communicate back to the Core team to make sure everything works smoothly. II should be updating it in my repository once everything is ironed out more, I'm also going to eventually abstract out some of the logic so it's much cleaner in the boot-server file. I'd imagine I'll make a PR with the similar new APis here (to this repo) once 4.x officially lands.

We're getting there though! 😄 @hheexx

@hheexx
Copy link

hheexx commented Feb 22, 2017

Thanks @MarkPieszak ,
I'm willing to help if I can.
I have a finished website that is blocked by SEO. Sorry for making pressure but it's very urgent for me :)

I hope you can commit something around RC time :)

@MarkPieszak
Copy link
Contributor

Not a problem totally understand!
You need Title & Meta tag support as well right?
If we can't get it within this repo, I'll at least have it within mine soon so you can patch your project with it. No worries 👍 @hheexx

@HaraldMuehlhoffCC
Copy link

HaraldMuehlhoffCC commented Mar 2, 2017

Just on a side-note:

I'm using the code at the top of this thread in my HomeController.

I had to add

            var redirectUrl = prerenderResult.RedirectUrl;

            if (redirectUrl != null)
                return LocalRedirect(redirectUrl);

to make redirects in my react-router work:

        <Redirect from='/de/selbstbedienung' to='/de/distribution/selbstbedienung' />

I also had to add code to make the react-cookie work again:

(HomeController)

if (prerenderResult.Globals != null)
            {
                [...]
                ViewData["CookieData"] = prerenderResult.Globals["cookieData"];
            }

(Index.cshtml)

<script>
    window.cookieData = @Html.Raw(ViewData["CookieData"]);
</script>

<div id="react-app">@Html.Raw(ViewData["SpaHtml"])</div>

The code in boot-client (ReactReduxSPA) also doesn't take care of the cookie options, e.g. path.

@NiceStepUp
Copy link

NiceStepUp commented Jun 5, 2017

I've developed an web application built using ASP.NET Core Web API and Angular 4. My module bundler is Web Pack 2.
So I cannot use this piece of code: ViewData["SpaHtml"] = prerenderResult.Html; from my controller. I do not have any cshtml views., so I cannot send data from controller to view through ViewData["SpaHtml"].

But I would like to use server side prerendering. I would like to add metatags into head:

import { Meta } from '@angular/platform-browser';

constructor(
    private metaService: Meta) {
}

let newText = "Foo data. This is test data!:)";
    //metatags to publish this page at social nets
    this.metaService.addTags([
        // Open Graph data
        { property: 'og:title', content: newText },
        { property: 'og:description', content: newText },        { 
        { property: "og:url", content: window.location.href },        
        { property: 'og:image', content: "http://usiter.com/uploads/20111118/zhivotnie+koshki+kartinka+s+malenkim+kotyonkom+35121656913.jpg" }]);

Is it possible to do without "ViewData"?`

@hheexx
Copy link

hheexx commented Jun 5, 2017

You receive rendered html string from node.js.
You can do whatever you want to it and you can return it directly from controller like:
return Content(htmlStr)

@NiceStepUp
Copy link

@hheexx ok, how can I add meta tags from controller to html page?

@hheexx
Copy link

hheexx commented Jun 5, 2017

html page is in a string so you need to find a way to find the right spot and insert meta in.

@mryarbles
Copy link

Why is this so poorly documented? I can't find anything online explaining exactly how to use Prerenderer directly except this github issue.

@MarkPieszak
Copy link
Contributor

@NiceStepUp
Copy link

NiceStepUp commented Jun 7, 2017

@hheexx I have a way to send data to plain html. And this data correctly is added to metatags:

import { Meta } from '@angular/platform-browser';

constructor(
private metaService: Meta) {
}

let newText = "Foo data. This is test data!:)";
//metatags to publish this page at social nets
this.metaService.addTags([
// Open Graph data
{ property: 'og:title', content: newText },
{ property: 'og:description', content: newText }, { 
{ property: "og:url", content: window.location.href }, 
{ property: 'og:image', content: "http://www.freeimageslive.co.uk/files
/images004/Italy_Venice_Canal_Grande.jpg" }]);

However, Search engines, Facebook, Twitter does not show my images when I try to post news at Facebook, Twitter or other social nets. I've explored why it happens and the reason is that SEO does not run JavaScript code. So when I post I do not see images.

I cannot use tag helper and ViewData as I've created ASP.NET Core Web API project and I do not have any ".cshtml" page.

@SteveSandersonMS
Copy link
Member Author

There's now a new API for this. It looks extremely similar to the proposal in this issue. Sample usage: 94fc84a#diff-ac9fabb76766dea5d503c5a5387e88e4

Further docs will come later. This will be included in version 2.0.0 of the SpaServices package.

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

No branches or pull requests

6 participants