Tối ưu hoá công cụ tìm kiếm cho ứng dụng AngularJS 4

Những ứng dụng Single Page (SPAs) thật tuyệt vời! Chúng load nhanh và cung cấp cho bạn nhiều kiểm soát về cách bạn muốn ứng dụng chạy. Chúng được parsed bởi trình duyệt và do đó bạn có thể kiểm soát được các DOM elements một cách thần thánh. Tuy nhiên, SPAs không thân thiện với công cụ tìm kiếm (not SEO friendly) bởi vì chúng thay đổi các meta tags và content bằng JavaScript và thay đổi này thường không đuợc bot của các công cụ tìm kiếm biết đến.

Ví dụ, các ứng dụng Angular 4 tải nội dung HTML trống đầu tiên trước khi lấy nội dung HTML cho trang được bằng XMLHttpRequest. Vì một số công cụ tìm kiếm không thể phân tích cú pháp JavaScript khi thu thập thông tin trang web, họ sẽ chỉ nhìn thấy nội dung trống đầu tiên.

Mặc dù Google cho biết bot tìm kiếm của họ có khả năng render JavaScript, điều này vẫn còn mơ hồ, hơn hết chúng ta vẫn nên thận trọng để giải quyết vấn đề này. Ngoài ra còn có các công cụ tìm kiếm khác không xử lý JavaScript. Bài viết này sẽ giới thiệu cách làm cho ứng dụng Angular 4 thân thiện với công cụ tìm kiếm giúp cho website của bạn có thứ hạng cao hơn trong kết quả tìm kiếm.

⚠️ Lưu ý: Đây không phải là Bài hướng dẫn về Angular 4 nên không bao gồm các chi tiết cụ thể về Angular 4 framework.

Bắt đầu làm cho ứng dụng Angular 4 của chúng ta thân thiện với công cụ tìm kiếm

Trước khi bắt đầu, chúng ta hãy xây dựng một ứng dụng đơn giản sử dụng Angular. Ứng dụng sẽ là một trang liệt kê một loạt chủ đề trên trang chủ. Chúng ta sẽ không kết nối với bất kỳ nguồn dữ liệu nào, thay vào đó sẽ hard code dữ liệu vào component.

Tạo một ứng dụng Angular 4 đơn giản

Chúng ta sẽ sử dụng ng-cli để tạo một ứng dụng Angular gọi là Blogist.

Tạo một ứng dụng mới sử dụng ng-cli

Chúng ta sẽ sử dụng ng new command để tạo ứng dụng Angular 4.

$ ng new Blogist

⚠️ Lưu ý: Bạn cần phải sử dụng phiên bản Angular CLI mới nhất để ứng dụng này hoạt động đúng. Phiên bản mới nhất là 1.3.x vào thời điểm viết bài này.

Tiếp theo, chúng ta sẽ tạo ra một component mà sau đó có thể thêm code logic vào. Chúng ta sẽ sử dụng ng g component command cho điều này:

$ ng g component ./blog/posts

Thêm mock data vào PostComponent của chúng ta

Vì lý do ngắn gọn, chúng ta sẽ không kết nối với một API bên ngoài. Thay vào đó, chúng ta sẽ chỉ tạo ra một số dữ liệu mô phỏng và sử dụng dữ liệu đó trong ứng dụng của chúng ta.

Mở file ./src/app/blog/posts.component.ts, chúng ta sẽ thêm một số logic code để đảm bảo rằng nó hoạt động như chúng ta muốn. Đầu tiên, chúng ta hãy mã hóa một số dữ liệu vào tệp. Thêm một method mới được gọi là postsData cho component.

    private postsData() {
        return [
            {
                "title": "Making Angular.js realtime with Websockets by marble",
                "pubDate": "2017-08-23 14:41:52",
                "link": "https://blog.pusher.com/making-angular-js-realtime-with-pusher/#comment-10372",
                "guid": "http://blog.pusher.com/?p=682#comment-10372",
                "author": "marble",
                "thumbnail": "",
                "description": "always a big fan of linking to bloggers that I enjoy but dont get a great deal of link enjoy from",
                "content": "<p>always a big fan of linking to bloggers that I enjoy but dont get a great deal of link enjoy from</p>",
                "enclosure": [],
                "categories": []
            },
            {
                "title": "Making Angular.js realtime with Websockets by strapless strap on",
                "pubDate": "2017-08-23 05:05:08",
                "link": "https://blog.pusher.com/making-angular-js-realtime-with-pusher/#comment-10371",
                "guid": "http://blog.pusher.com/?p=682#comment-10371",
                "author": "strapless strap on",
                "thumbnail": "",
                "description": "very couple of internet websites that transpire to be detailed beneath, from our point of view are undoubtedly properly worth checking out",
                "content": "<p>very couple of internet websites that transpire to be detailed beneath, from our point of view are undoubtedly properly worth checking out</p>",
                "enclosure": [],
                "categories": []
            },
            {
                "title": "Making Angular.js realtime with Websockets by bondage restraints",
                "pubDate": "2017-08-22 17:09:17",
                "link": "https://blog.pusher.com/making-angular-js-realtime-with-pusher/#comment-10370",
                "guid": "http://blog.pusher.com/?p=682#comment-10370",
                "author": "bondage restraints",
                "thumbnail": "",
                "description": "very couple of web sites that occur to be in depth below, from our point of view are undoubtedly properly worth checking out",
                "content": "<p>very couple of web sites that occur to be in depth below, from our point of view are undoubtedly properly worth checking out</p>",
                "enclosure": [],
                "categories": []
            }
        ];
    }

Để sử dụng mock data đã đuợc tạo phía trên, chúng ta thay constructor method của PostsComponent class bằng đoạn code dưới đây:

    public posts;

    constructor() {
        this.posts = this.postsData();
    }

Trong đoạn code phía trên, chúng ta chỉ đơn giản gán cho thuộc tính posts bằng giá trị mà postsData method trả về, đây chính là cách chúng ta giả lập API response.

Tạo View cho PostsComponent

Bây giờ chúng ta đã có mock posts data. Chúng ta sẽ tạo một view hiển thị tất cả posts từ mock data.

Mở view ./app/blog/posts.component.html và thêm vào đoạn code dưới đây:

    <div class="jumbotron">
        <h1>Blogist</h1>
        <p>This is the best resource for the best web development posts.</p>
    </div>
    <div class="row">
        <div class="col-xs-12 col-md-12">
            <ul class="list-group">
                <li class="list-group-item" *ngFor="let post of posts">
                    <h4>{{post.title}}</h4>
                </li>
            </ul>
        </div>
    </div>

Đoạn code trên chỉ lấy về posts data sau đó lặp và hiển thị title của post.

Tiếp theo, mở file index.html và trong thẻ <head> thay nội dung với code dưới đây. Nó chỉ đơn giản sử dụng Bootstrap và thêm navigation bar:

    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Blogist</title>
      <base href="/">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="icon" type="image/x-icon" href="favicon.ico">
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
    </head>
    <body>
      <nav class="navbar navbar-default">
        <div class="container-fluid">
          <div class="navbar-header">
            <a class="navbar-brand" href="#">Blogist</a>
          </div>
          <ul class="nav navbar-nav">
            <li class="active"><a href="#">Posts</a></li>
            <li><a href="#">Web Development</a></li>
            <li><a href="#">Graphic Design</a></li>
          </ul>
        </div>
      </nav>
      <div class="container">
        <app-root>Loading...</app-root>
      </div>
    </body>
    </html>
    ```
    
###    Đăng ký PostsComponent trong module application module

Tiếp theo chúng ta sẽ đăng ký PostsComponent thành module ứng dụng

💡 Chú ý sử dụng **ng g component** command sẽ tự động đăng ký PostsComponent trong application module. Vì vậy, bạn có thể không cần phải làm lại. Nếu nó đã được thực hiện cho bạn, bạn có thể bỏ qua bước này.

Nếu nó chưa được đăng ký tự động, hãy mở tệp `./src/app/app.module.ts` và import `PostsComponent`:

```Javascript
    import { PostsComponent } from './blog/posts.component';

Sau đó, trong mảng NgModule declarations, thêm PostsComponent:

    @NgModule({
      declarations: [
        ...
        PostsComponent,
      ],
      ...
    })

Hiển thị ứng dụng Angular

Sau khi đăng ký Posts component, chúng ta sẽ include nó trong ./src/app/app.component.html file để posts component được hiển thị. Mở ./src/app/app.component.html file và thêm vào dòng code dưới đây:

 <app-posts></app-posts>

Đó là tất cả!

Bây giờ bạn chạy ng serve và mở URL đuợc cung cấp. Bạn sẽ có thể nhìn page với tất cả các posts:

Tuyệt vời, đó là chính xác những gì chúng ta mong đợi. Tuy nhiên, khi bạn view source của URL, bạn sẽ nhận thấy toàn bộ nội của page bị thiếu và chỉ <app-root>loading… </ app-root> hiển thị.

Đó là do cách Angular hoạt động. Nó sẽ tải parent template đầu tiên, sau đó sau đó tải chính nó.

Sau đó nó sẽ bắt đầu quá trình thao tác DOM sẽ chèn nội dung của mỗi trang tiếp theo trong thẻ <app-root>.

Do đó, khi bot công cụ tìm kiếm request trang này, nó sẽ nhận được HTML <app-root><app-root>Loading…</app-root> </ app-root> ở trên và nội dung của trang đã khiến cho nội dung cần SEO không đến đưọc công cụ tìm kiếm.

Tối ưu hoá công cụ tìm kiếm cho ứng dụng AngularJS 4

Bây giờ chúng ta đã xây dựng ứng dụng sample, chúng ta có thể thấy ngay nó không thân thiện với SEO. Vì vậy, chúng ta sẽ sử dụng Angular universal platform để pre-render templates phía server-side sau đó serve khi page đã đuợc tải

💡 Universal Angular project bao base platform API và các công cụ xung quanh cho phép các nhà phát triển render phía server-side (hoặc pre-rendering) trong các ứng dụng Angular..

Để bắt đầu, chúng ta sẽ cài đặt angular/platform-server package và angular/animations package. Cả hai đều cần require để platform server hoạt động chính xác . Platform server sẽ cung cấp server-side rendering.

Chạy command dưới đây trong terminal để cài đặt dependencies require cho server-side rendering của ứng dụng Angular:

    $ npm install --save @angular/platform-server @angular/animations

Sau khi các packages đã cài đặt thành công NPM, mở ./src/app.modules.ts và thay đổi BrowserModule declaration như dưới đây:

    @NgModule({
      ...
      imports: [
        BrowserModule.withServerTransition({appId: 'blogist'})
      ],
      ...
    })   

Trong đoạn code phía trên, chúng ta thêm withServerTransition method vào BrowserModule và truyền vào appId trùng với tên ứng dụng blogist. Thêm vào như này nhằm ‘cấu hình một a browser-based application chuyển từ một server-rendered app, nếu có hiển thị trên page’.

Tiếp theo chúng ta sẽ tạo application server module. Tạo một file mới ./src/app/app-server.module.ts

    import { NgModule } from '@angular/core';
    import { AppModule } from './app.module';
    import { AppComponent } from './app.component';
    import { ServerModule } from '@angular/platform-server';

    @NgModule({
      imports: [
        ServerModule,
        AppModule,
      ],
      bootstrap: [
        AppComponent
      ]
    })
    export class AppServerModule { }

Đây là một Angular module cơ bản sẽ hoạt động như server module. Điều lớn nhất cần chú ý là ở trên chúng ta import AppModule trong server module để nó sẽ là một phần của AppServerModule. Module này sẽ nơi chúng ta boostrap ứng dụng từ server.

Thêm title và meta tags vào ứng dụng Angular

Một điều cuối cùng chúng ta thêm vào ứng dụng là hỗ trợ meta tags và title trên mỗi page. Với Angular universal, làm điều này rất dễ dàng.

Mở ./src/app/blog/posts.component.ts file và làm như duới đây:

Import Meta và Title từ @angular/platform-browser package:

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

Bây giờ trong constructor method, thêm những dòng code này:

    constructor(meta: Meta, title: Title) {
      this.posts = this.postsData();

      // Sets the <title></title>
      title.setTitle('Blogist');

      // Sets the <meta> tag for the page
      meta.addTags([
        { name: 'author', content: 'Blogist' },
        { name: 'description', content: 'This is a description.' },
      ]);
    }
    ```
    
Đoạn code trên cho phép set title cho mỗi page bạn tạo và chúng sẽ trở nên pre-rendered sử dụng Angular Universal. Điều nay cho phép bạn kiểm soát tốt hơn meta tags và title của mỗi page khác nhau.

### Tạo một Express server để làm cho ứng dụng Angular SEO friendly

Hãy tạo mộtt Express server. Điều này đơn giản sẽ cho phép server-side rendering.

Tạo một file `./src/server.ts` và thêm vào nội dung dưới đây:

```Javascript
    import 'reflect-metadata';
    import 'zone.js/dist/zone-node';
    import { renderModuleFactory } from '@angular/platform-server'
    import { enableProdMode } from '@angular/core'
    import * as express from 'express';
    import { join } from 'path';
    import { readFileSync } from 'fs';
    import { AppServerModuleNgFactory } from '../dist/ngfactory/src/app/app-server.module.ngfactory'

    enableProdMode()

    const PORT     = process.env.PORT || 4000
    const DIST_DIR = join(__dirname, '..', 'dist')
    const app = express();
    const template = readFileSync(join(DIST_DIR, 'index.html')).toString()

    app.engine('html', (_, options, callback) => {
      const newOptions = { document: template, url: options.req.url };

      renderModuleFactory(AppServerModuleNgFactory, newOptions)
        .then(html => callback(null, html))
    })

    app.set('views', 'src')
    app.set('view engine', 'html')

    app.get('*.*', express.static(DIST_DIR))
    app.get('*', (req, res) => {
      res.render('index', { req })
    })

    app.listen(PORT, () => {
      console.log(`App listening on http://localhost:${PORT}!`)
    });
    
    ```
Trong file này, chúng ta đã import tất cả các packages chúng ta cần để chạy Express server của chúng ta. Đặc biệt, chúng ta `import AppServerModuleNgFactory`, một file không tồn tại những sẽ được generate trong build process.

Tiếp theo, `enableProdMode()` đơn giản là  enable production mode trên ứng dụng của chúng ta. Chúng ta cũng sử dụng [renderModuleFactory](https://angular.io/api/platform-server/renderModuleFactory) để parse HTML và render page đã được loaded trên server-side. Mọi thứ khác trong đoạn code này đều liên quan tới Express.

Tiếp theo chúng ta mở `./src/tsconfig.app.json` file và thêm `server.ts` vào exclude section.

```Javascript
      "exclude": [
        "server.ts",
        ...
      ]

💡 Thuộc tính exclude liệt kê một danh sách các files sẽ bị loại trừ khỏi quá trình biên dịch.

Mở file ./tsconfig.json và thêm đoạn code bên dưới vào dưới cùng sau thuộc tính compilerOptions:

        ...
        "lib": [
          "es2016",
          "dom"
        ]
      },
      "angularCompilerOptions": {
          "genDir": "./dist/ngfactory",
          "entryModule": "./src/app/app.module#AppModule"
      }
    }

💡 genDir là nơi chủ yếu mọi thứ được generated. entryModule chấp nhận đường dẫn của module bootstrapped. #AppModule ở cuối đường dẫn là tên của các layers được published.

Bước cuối cùng cần làm là cập nhật thuộc tính scripts trong ./package.json file. Bạn nên thay thế hoặc bổ sung vào những keys có sẵn trong thuộc tính scripts:

    {
      ...
      "scripts": {
        "prestart": "ng build --prod && ./node_modules/.bin/ngc",
        "start": "ts-node src/server.ts"
      },
      ...
    }

Chúng ta có những commands đã được đăng ký cho start và prestart scripts trong ./package.json file. Bởi vì chúng ta đã thêm vào prestart, nó sẽ chạy tự động truớc khi start được.

Test SEO friendly trong ứng dụng Angular 4

Sau khi bạn đã thực hiện xong những thay đổi này cho ứng dụng, vào terminal và chạy lệnh sau đây:

    $ npm run start

Thao tác này sẽ chạy prestart script chứa các lệnh ng build --prod && ./node_modules/.bin/ngc và chạy start script ts-node src / server.ts. Một khi các lệnh được hoàn thành, bạn sẽ thấy out gần với hình ảnh này trên terminal:

Khi bạn truy cập vào trang bây giờ bạn vẫn sẽ thấy output tương tự như bạn đã thấy trước đây. Tuy nhiên, khi bạn xem source, bạn sẽ thấy HTML hoàn chỉnh.

Kết luận

Trong bài này, chúng ta đã khám phá làm thế nào để làm cho Angular 4 Single Page Application (SPA) SEO Friendly bằng cách sử dụng Angular 4 Universal. Hy vọng rằng bạn đã học được một vài điều và nỗi lo về việc tối ưu hóa SEO xấu sẽ không ngăn bạn sử dụng Angular 4 cho các ứng dụng của bạn nữa.

Bài viết đuợc dịch từ nguồn: https://blog.pusher.com/make-angular-4-app-seo-friendly/