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 Answers 2
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.
-
@StepUp, I have updated my answer, I home it makes more sense now.Andrii Litvinov– Andrii Litvinov2017年06月11日 09:07:00 +00:00Commented Jun 11, 2017 at 9:07
-
In my view, there is no difference between your answer and Dávid Molnár.StepUp– StepUp2017年06月11日 19:37:56 +00:00Commented 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?Andrii Litvinov– Andrii Litvinov2017年06月12日 04:39:41 +00:00Commented 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.StepUp– StepUp2017年06月13日 11:58:24 +00:00Commented 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.Andrii Litvinov– Andrii Litvinov2017年06月15日 13:38:07 +00:00Commented Jun 15, 2017 at 13:38
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.
-
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
)?StepUp– StepUp2017年06月09日 11:55:49 +00:00Commented 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 fromhttp://myappl.com/#/hello
ishttp://myappl.com/
.Andrii Litvinov– Andrii Litvinov2017年06月09日 12:18:16 +00:00Commented 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) {}
inmypage.component.ts
, however the page is not crawlable and sharable by Facebook. Twitter or other social nets.StepUp– StepUp2017年06月09日 12:25:41 +00:00Commented 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 theprerenderResult.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 theprerenderResult.Html
, but that's not really nice.Dávid Molnár– Dávid Molnár2017年06月09日 12:30:52 +00:00Commented 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.Andrii Litvinov– Andrii Litvinov2017年06月09日 12:45:50 +00:00Commented Jun 9, 2017 at 12:45
Explore related questions
See similar questions with these tags.
ViewData
to send html to.cshtml
view in this tutorial, but I am using just.html
views, not.cshtml
views.prerenderResult.Html
from action?For example, open Views/Home/Index.cshtml, and add markup like the following:
and thisNow go to your Views/_ViewImports.cshtml file, and add the following line: @addTagHelper "*, Microsoft.AspNetCore.SpaServices"
here: github.com/aspnet/JavaScriptServices/tree/dev/src/…http://myappl.com/#/hello
instead ofhttp://myappl.com/#/hellopage
?