12

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.

asked Jun 5, 2017 at 10:56
6
  • Have you tried github.com/aspnet/JavaScriptServices yet? Commented Jun 9, 2017 at 7:02
  • @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. Commented Jun 9, 2017 at 7:09
  • Alright, why do you think you need cshtml view? Why not simply return prerenderResult.Html from action? Commented Jun 9, 2017 at 7:14
  • @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/… Commented Jun 9, 2017 at 7:17
  • 1
    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? Commented Jun 16, 2017 at 6:37

2 Answers 2

5
+150

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.

answered Jun 9, 2017 at 12:54
13
  • @StepUp, I have updated my answer, I home it makes more sense now. Commented Jun 11, 2017 at 9:07
  • In my view, there is no difference between your answer and Dávid Molnár. Commented Jun 11, 2017 at 19:37
  • @StepUp, fair enough. Anyway, you asked for an example of server-side rendering an there is an example. In what way does not it work for you? Commented Jun 12, 2017 at 4:39
  • cause I have many html pages to be posted and if I use this way it would be really bad to render each page in a such way. Commented Jun 13, 2017 at 11:58
  • 1
    @StepUp, I have found repo github.com/thinktecture/nodejs-aspnetcore-webapi that shows how same API can be build with both nodejs and webapi and consumed by angular ui. In urlService.ts you simply specify what url you want to use to access API. So again it two independent servers will we deployed - one to serve html, js, css, etc with node and one - asp.net web api. You can host both behind reverse proxy so that you don't need to configure CORS. Commented Jun 15, 2017 at 13:38
2

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.

answered Jun 9, 2017 at 11:48
6
  • How can I direct to use this url by SEO? For example, a browser address of my page is http://myappl.com/#/hello, but SEO should seeks another address(http://myappl.com/api/prerenderer)? Commented Jun 9, 2017 at 11:55
  • @StepUp, sounds like your question does not reflect what you want. Try explain better what you want to achieve. There is not way to make it work with hashes in URL because browser don't sent part of url after # to server. All server will get from http://myappl.com/#/hello is http://myappl.com/. Commented Jun 9, 2017 at 12:18
  • @AndriiLitvinov What I want is to make my application to be crawlable or link sharable by Facebook, Twitter, Google. To do it, I am using metatags constructor( private metaService: Meta) {} in mypage.component.ts, however the page is not crawlable and sharable by Facebook. Twitter or other social nets. Commented Jun 9, 2017 at 12:25
  • Now I understand what you want. You'll need to dynamically create the HTML page for http://myappl.com. You are now serving the HTML file generated by Webpack/Angular. It's a static HTML file. You need to return a dynamic HTML file with the contents of the prerenderResult.Html + your HTML. Normally you would do this in a .cshtml file. But you do not have one.... As a dirty workaround you could concatenate the <html>... with the prerenderResult.Html, but that's not really nice. Commented Jun 9, 2017 at 12:30
  • 1
    @StepUp, I don't see how it will be possible to achieve if you cannot change URL and Facebook cannot run scripts. Would be nice to see a good answer. I will also try to investigate a bit more on my spare time. Commented Jun 9, 2017 at 12:45

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.