Angular SEO на хостинге Firebase

348
views

Angular SEO pre-renderingРазрабатывая Angular приложение, все мы рано или поздно сталкиваемся с проблемой SEO. Существуют различные пути решения, но если вы хотите высокую производительность, легкость обслуживания, отсутствие необходимости в платных NodeJS серверах, то вас вероятно заинтересует Angular Server Side Pre-rendering.Pre-rendering – это server side процесс, который подготовит SEO оптимизированные странички в виде готовых html файлов. Специальный скрипт открывает заранее подготовленные url страниц, ждет окончания загрузки и выполнения каждой из них, а затем полученную верстку сохраняет в html файлы, которые будут доступны по указанному пути.

Именно таким способом я реализовал SEO в своем проекте methodist.io. Поэтому далее я буду использовать его как пример.

Общая структура собранного проекта будет выглядеть так:

Установка зависимостей

Данный гайд рассчитан, что вы работаете с Angular v6+ проектом.

Установим angular universal. Он будет основой нашего пререндеринга.

ng g universal --client-project firestarter

После выполнения этой команды, проект будет настроен автоматически для работы с SSR. Имя проекта для Angular universal я указал app-server.

Следующий шаг можно пропустить, если вы не используете module lazy loading.

npm i @nguniversal/module-map-ngfactory-loader --save

Обновим файл src/app/app.module.ts

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

@NgModule({
  imports: [
     // ...
    // remove BrowserModule and add:
    BrowserModule.withServerTransition({ appId: 'firestarter' }),
    BrowserTransferStateModule
  ]
})

Еще обновим src/app/app.server.module

import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';


@NgModule({
  imports: [
    // ...
    ServerTransferStateModule
    ModuleMapLoaderModule // <-- нужен для lazy-loaded модулей
  ],
})

Установка и настройка пререндеринга

Теперь будет происходить магия. Для начала установим webpack:

npm i -D webpack-cli fs-extra http-server ts-loader

# если вы используете firebase
npm i -D ws xmlhttprequest

Все эти пакеты нужны для того, чтобы скомпилированный сервер node.js при запуске загружал роуты из файла static.paths.ts в корне проекта:

export const ROUTES = [
    '/',
    '/profile',
    '/terms',
    '/privacy'
];

и генерировал страницы внутри папки dist.

Создадим конфиг webpack.prerender.config.js в корне проекта:

const path = require('path');
const webpack = require('webpack');

const APP_NAME = 'methodist'; //Имя вашего проекта

module.exports = {
  entry: {  prerender: './prerender.ts' },
  resolve: { extensions: ['.js', '.ts'] },
  mode: 'development',
  devtool: 'hidden-source-map',
  target: 'node',
  externals: [/(node_modules|main\..*\.js)/],
  output: {
    path: path.join(__dirname, `dist/${APP_NAME}`),
    filename: '[name].js'
  },
  module: {
    rules: [
      { test: /\.ts$/, loader: 'ts-loader' }
    ]
  },
  plugins: [
    new webpack.ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    ),
    new webpack.NormalModuleReplacementPlugin(
      /@swimlane(\\|\/)dragula(\\|\/)dragula.js/,
      path.join(__dirname, 'src/server-mocks/dragula/dragula.js')    
    ),
  ]
}

Ну и теперь самое важное, скрипт пререндеринга и сохранение полученных html:

import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';

import { enableProdMode } from '@angular/core';
// С включенным prod mode сервер быстрее генерирует страницы
enableProdMode();

import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { renderModuleFactory } from '@angular/platform-server';
import { ROUTES } from './static.paths';

(global as any).XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;

const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/methodist-server/main');

const BROWSER_FOLDER = join(process.cwd(), './dist/methodist'); //Путь к SPA билду

// Укажем начальную страницу сбилдженного Angular приложения
const index = readFileSync(join('./dist/methodist', 'index.html'), 'utf8');

let previousRender = Promise.resolve();

// Цикл по роутам страниц для пререндеринга
ROUTES.forEach(route => {
    const fullPath = join(BROWSER_FOLDER, route);

    if (!existsSync(fullPath)) {
        mkdirSync(fullPath);
    }

    // Я записываю полученную верстку в seo.html файлы по нужным роутам
    previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, {
        document: index,
        url: route,
        extraProviders: [
            provideModuleMap(LAZY_MODULE_MAP)
        ]
    })).then(html => {
        const sourceMapRe = /\/\*# sourceMappingURL=.*\*\//g;
        writeFileSync(join(fullPath, 'seo.html'), html.replace(sourceMapRe, ''));
    });
});

Если верстку сохранять в index.html, то необходимо будет выключить SPA функционал в firebase хостинге. Для меня такое решение не подходило, так как есть динамические страницы, для которых я не делаю pre-rendering, но их можно открыть по прямой ссылке.

Поэтому я решил, что должен быть в проекте стандартный spa.html в корне собранного проекта, который будет работать для всех роутов кроме тех, которые указаны для пререндеринга. А пути, которые должны открывать сгенерированные html файлы, я задам в настройках firebase хостинга.

Обратите внимание, что главный дефолтный html файл я называю не index.html, а spa.html. Так как если в корне firebase хостинга есть index.html с включенным SPA behavior, то открываться будет он, а не указанный роут в конфиге (у index.html самый высокий приоритет).

Вот как выглядит мой конфиг хостинга firebase.json :

{
  "hosting": {
    "public": "dist/methodist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/",
        "destination": "/seo.html"
      },
      {
        "source": "/profile",
        "destination": "/profile/seo.html"
      },
      {
        "source": "/terms",
        "destination": "/terms/seo.html"
      },
      {
        "source": "/privacy",
        "destination": "/privacy/seo.html"
      },
      {
        "source": "**",
        "destination": "/spa.html"
      }
    ],
    "headers": [
      {
        "source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)",
        "headers": [
          {
            "key": "Access-Control-Allow-Origin",
            "value": "*"
          }
        ]
      },
      {
        "source": "**/*.@(js|css)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "max-age=604800"
          }
        ]
      },
      {
        "source": "**/*.@(jpg|jpeg|gif|png)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "max-age=604800"
          }
        ]
      },
      {
        "source": "404.html",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "max-age=300"
          }
        ]
      }
    ],
    "trailingSlash": false
  },
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint"
    ],
    "source": "functions"
  }
}

Запуск pre-rendering процесса

Уже почти все готово, осталось автоматизировать:

  • сборку приложения
  • выполнение пререндеринга
  • подготовку папки dist к деплою

Для этого мы обновим наш package.json и добавим вот такие команды:

...
"scripts": {
    ...
    "build": "ng build --prod --aot=true --source-map=false",
    "webpack:prerender": "webpack --config webpack.prerender.config.js",
    "build:prerender": "node dist/methodist/prerender.js",
    "serve:prerender": "http-server ./dist/methodist -o",
    "rename-index": "mv ./dist/methodist/index.html ./dist/methodist/spa.html",
    "build:all": "npm run build && ng run methodist:server && npm run webpack:prerender && npm run build:prerender && rm -f ./dist/methodist/prerender.js ./dist/methodist/prerender.js.map && npm run rename-index",
    ...
},
...

Теперь если прописать в консоли npm run build:all, то должен собраться ваш проект и сервер для пререндеринга, затем выполнится сам пререндеринг.

При успешно выполненных командах вы получите SPA с SEO оптимизированными страницами, который полностью готов для деплоя на хостинг.

Заключение

SSR Pre-rendering – отличный подход, который позволит поисковикам и соц. сетям индексировать важные для вашего проекта страницы. При этом вам не понадобится оплачивать отдельный сервер с node.js для Angular Universal или использовать медленный rendertron. Реальный пример работы вы можете посмотреть на methodist.io.

Подписывайтесь

Для получения уведомлений о новых публикациях подписывайтесь на мой блог или страницы в соц. сетях: Twitter, Facebook.

Подписаться на блог по эл. почте