11
votes

I've developed a web application built using ASP.NET Core Web API and Angular 4. My module bundler is Web Pack 2.

I would like to make my application crawlable or link sharable by Facebook, Twitter, Google. The url must be the same when some user tries to post my news at Facebook. For example, Jon wants to share a page with url - http://myappl.com/#/hellopage at Facebook, then Jon inserts this link into Facebook: http://myappl.com/#/hellopage.

I've seen this tutorial of Angular Universal server side rendering without tag helper and would like to make server side rendering. As I use ASP.NET Core Web API and my Angular 4 application does not have any .cshtml views, so I cannot send data from controller to view through ViewData["SpaHtml"] from my controller:

ViewData["SpaHtml"] = prerenderResult.Html;

In addition, I see this google tutorial of Angular Universal, but they use NodeJS server, not ASP.NET Core.

I would like to use server side prerendering. I am adding metatags through this way:

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" }]);

and when I inspect this element in a browser it looks like this:

<head>    
    <meta property="og:title" content="Foo data. This is test data!:)">    
    <meta property="og:description" content="Foo data. This is test data!:)">
    <meta name="og:url" content="http://foourl.com">
    <meta property="og:image" content="http://www.freeimageslive.co.uk/files
/images004/Italy_Venice_Canal_Grande.jpg"">    
</head>

I am bootstrapping the application usual way:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

and my webpack.config.js config looks like this:

var path = require('path');

var webpack = require('webpack');

var ProvidePlugin = require('webpack/lib/ProvidePlugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var CleanWebpackPlugin = require('clean-webpack-plugin');
var WebpackNotifierPlugin = require('webpack-notifier');

var isProd = (process.env.NODE_ENV === 'production');

function getPlugins() {
    var plugins = [];

    // Always expose NODE_ENV to webpack, you can now use `process.env.NODE_ENV`
    // inside your code for any environment checks; UglifyJS will automatically
    // drop any unreachable code.
    plugins.push(new webpack.DefinePlugin({
        'process.env': {
            'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
        }
    }));

    plugins.push(new webpack.ProvidePlugin({
        jQuery: 'jquery',
        $: 'jquery',
        jquery: 'jquery'
    }));
    plugins.push(new CleanWebpackPlugin(
        [
            './wwwroot/js',
            './wwwroot/fonts',
            './wwwroot/assets'
        ]
    ));

    return plugins;
}


module.exports = {

    devtool: 'source-map',

    entry: {
        app: './persons-app/main.ts' // 
    },

    output: {
        path: "./wwwroot/",
        filename: 'js/[name]-[hash:8].bundle.js',
        publicPath: "/"
    },

    resolve: {
        extensions: ['.ts', '.js', '.json', '.css', '.scss', '.html']
    },

    devServer: {
        historyApiFallback: true,
        stats: 'minimal',
        outputPath: path.join(__dirname, 'wwwroot/')
    },

    module: {
        rules: [{
                test: /\.ts$/,
                exclude: /node_modules/,
                loader: 'tslint-loader',
                enforce: 'pre'
            },
            {
                test: /\.ts$/,
                loaders: [
                    'awesome-typescript-loader',
                    'angular2-template-loader',

                    'angular-router-loader',

                    'source-map-loader'
                ]
            },
            {
                test: /\.js/,
                loader: 'babel',
                exclude: /(node_modules|bower_components)/
            },
            {
                test: /\.(png|jpg|gif|ico)$/,
                exclude: /node_modules/,
                loader: "file?name=img/[name].[ext]"
            },
            {
                test: /\.css$/,
                exclude: /node_modules/,                
                use: ['to-string-loader', 'style-loader', 'css-loader'],
            },
            {
                test: /\.scss$/,
                exclude: /node_modules/,
                loaders: ["style", "css", "sass"]
            },
            {
                test: /\.html$/,
                loader: 'raw'
            },
            {
                test: /\.(eot|svg|ttf|woff|woff2|otf)$/,
                loader: 'file?name=fonts/[name].[ext]'
            }
        ],
        exprContextCritical: false
    },
    plugins: getPlugins()

};

Is it possible to do server side rendering without ViewData? Is there an alternative way to make server side rendering in ASP.NET Core Web API and Angular 2?

I have uploaded an example to a github repository.

2
@AndriiLitvinov yeah, it is necessary to use ViewData to send html to .cshtml view in this tutorial, but I am using just .html views, not .cshtml views.StepUp
Alright, why do you think you need cshtml view? Why not simply return prerenderResult.Html from action?Andrii Litvinov
@AndriiLitvinov I've read this: For example, open Views/Home/Index.cshtml, and add markup like the following: and this Now go to your Views/_ViewImports.cshtml file, and add the following line: @addTagHelper "*, Microsoft.AspNetCore.SpaServices" here: github.com/aspnet/JavaScriptServices/tree/dev/src/…StepUp
in the bold part, you say "The url must be the same", but then, example urld differ. Did you mean http://myappl.com/#/hello instead of http://myappl.com/#/hellopage?quetzalcoatl

2 Answers

4
votes

There is an option in Angular to use HTML5 style urls (without hashes): LocationStrategy and browser URL styles. You should opt this URL style. And for each URL that you want to be shared o Facebook you need to render the entire page as shown in the tutorial you referenced. Having full URL on server you are able to render corresponding view and return HTML.

Code provided by @DávidMolnár might work very well for the purpose, but I haven't tried yet.

UPDATE:

First of all, to make server prerendering work you should not use useHash: true which prevents sending route information to the server.

In the demo ASP.NET Core + Angular 2 universal app that was mentioned in GitHub issue you referenced, ASP.NET Core MVC Controller and View are used only to server prerendered HTML from Angular in a more convenient way. For the remaining part of application only WebAPI is used from .NET Core world everything else is Angular and related web technologies.

It is convenient to use Razor view, but if you are strictly against it you can hardcode HTML into controller action directly:

[Produces("text/html")]
public async Task<string> Index()
{
    var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>();
    var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>();

    var applicationBasePath = hostEnv.ContentRootPath;
    var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>();
    var unencodedPathAndQuery = requestFeature.RawTarget;
    var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}";

    TransferData transferData = new TransferData();
    transferData.request = AbstractHttpContextRequestInfo(Request);
    transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)";

    var prerenderResult = await Prerenderer.RenderToString(
        "/",
        nodeServices,
        new JavaScriptModuleExport(applicationBasePath + "/Client/dist/main-server"),
        unencodedAbsoluteUrl,
        unencodedPathAndQuery,
        transferData,
        30000,
        Request.PathBase.ToString()
    );

    string html = prerenderResult.Html; // our <app> from Angular
    var title = prerenderResult.Globals["title"]; // set our <title> from Angular
    var styles = prerenderResult.Globals["styles"]; // put styles in the correct place
    var meta = prerenderResult.Globals["meta"]; // set our <meta> SEO tags
    var links = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags

    return $@"<!DOCTYPE html>
<html>
<head>
<base href=""/"" />
<title>{title}</title>

<meta charset=""utf-8"" />
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"" />
{meta}
{links}

<link rel=""stylesheet"" href=""https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/0.8.2/css/flag-icon.min.css"" />

{styles}

</head>
<body>
{html}

<!-- remove if you're not going to use SignalR -->
<script src=""https://code.jquery.com/jquery-2.2.4.min.js""
        integrity=""sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=""
        crossorigin=""anonymous""></script>

<script src=""http://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.0.min.js""></script>

<script src=""/dist/main-browser.js""></script>
</body>
</html>";   
}

Please note that the fallback URL is used to process all routes in HomeController and render corresponding angular route:

builder.UseMvc(routes =>
{
  routes.MapSpaFallbackRoute(
      name: "spa-fallback",
      defaults: new { controller = "Home", action = "Index" });
});

To make it easier to start consider to take that demo project and modify it to fit with your application.

UPDATE 2:

If you don't need to use anything from ASP.NET MVC like Razor with NodeServices it feels more natural to me to host Universal Angular app with server prerendering on Node.js server. And host ASP.NET Web Api independently so that Angular UI can access API on different server. I think it is quite common approach to host static files (and utilize server prerendering in case) independently fro API.

Here is a starter repo of Universal Angular hosted on Node.js: https://github.com/angular/universal-starter.

And here is an example of how UI and web API can be hosted on different servers: https://github.com/thinktecture/nodejs-aspnetcore-webapi. Notice how API URL is configured in urlService.ts.

Also you could consider to hide both UI and API server behind reverse proxy so that both can be accessed through same public domain and host and you don't have to deal with CORS to make it work in a browser.

2
votes

Based on your linked tutorials you could return the HTML directly from the controller.

The prerendered page will be available at http://<host>:

[Route("")]
public class PrerenderController : Controller
{
    [HttpGet]
    [Produces("text/html")]
    public async Task<string> Get()
    {
        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()
        );
        return @"<html>..." + prerenderResult.Html + @"</html>";
    }
}

Note the Produces attribute, which allows to return HTML content. See this question.