Skip to main content

SEO and Meta

Explanation

A separate module responsible for meta tags generation - SeoModule from @tramvai/module-seo package.

Data for meta tags can be defined globally, for the all application pages, or only for a specific routes.

All meta have a priority and the data with highest priority will be used.

Meta tags

Meta tags in tramvai application represented as a key: value object, where key is a name of the meta tag from predefined list

Data sources

Meta tags will be generated per every page request, and based on different data sources:

Meta priority

There is a three predefined priority levels, and you can always use custom:

  • 0 - used for default meta, available in constant META_PRIORITY_DEFAULT
  • 10 - used for meta from route config, available in constant META_PRIORITY_ROUTE
  • 20 - highest priority, intended for custom usage in application, usually for dynamic meta, available in constant META_PRIORITY_APP

Usage

Installation

Be sure that you have installed and connected SeoModule (already included in new projects):

npx tramvai add @tramvai/module-seo

And connect in the project

import { createApp } from '@tramvai/core';
import { SeoModule } from '@tramvai/module-seo';

createApp({
name: 'tincoin',
modules: [SeoModule],
});

Default meta

You can set default meta pack by using the method SeoModule.forRoot with metaDefault option:

import { createApp } from '@tramvai/core';
import { SeoModule } from '@tramvai/module-seo';

createApp({
name: 'tincoin',
modules: [
SeoModule.forRoot({
metaDefault: {
title: 'Tramvai application',
},
}),
],
});

Another way is to provide META_DEFAULT_TOKEN token directly:

import { createApp, provide } from '@tramvai/core';
import { SeoModule, META_DEFAULT_TOKEN } from '@tramvai/module-seo';

createApp({
name: 'tincoin',
modules: [SeoModule],
providers: [
provide({
provide: META_DEFAULT_TOKEN,
useValue: {
title: 'Tramvai application',
},
}),
],
});

Route meta

Simplest way to set meta for specific route is to use static seo property of the page component:

import type { PageComponent } from '@tramvai/react';

const CommentsPage: PageComponent = () => <h1>Comments Page</h1>;

CommentsPage.seo = {
metaTags: {
title: 'Comments Page Title',
},
};

export default CommentsPage;

Another way, for manually created routes is to update route config.seo property:

const route = {
name: 'comments',
path: '/comments/',
config: {
pageComponent: '@/pages/comments',
seo: {
metaTags: {
title: 'Comments Page Title',
},
},
},
};

Custom data source

You can set additional data source by using the method SeoModule.forRoot with metaUpdaters option.

Each source is a function that takes a meta and allows you to extend the meta through a updateMeta call. The priority is a positive number, for each specific meta key the value with the highest priority will be used, the value with priority 0 denotes the default value.

This meta update will be executed for every request for application pages. Possible use-case - read information for meta from some services or stores (this information need to be already fetched, meta updaters are syncronyous).

import { createApp } from '@tramvai/core';
import { SeoModule, META_PRIORITY_ROUTE } from '@tramvai/module-seo';

createApp({
name: 'tincoin',
modules: [
SeoModule.forRoot({
metaUpdaters: [
(meta) => {
meta.updateMeta(META_PRIORITY_ROUTE, {
ogTitle: 'Tramvai application',
});
},
],
}),
],
});

Another way is to provide META_UPDATER_TOKEN token directly:

import { createApp, provide } from '@tramvai/core';
import { SeoModule, META_UPDATER_TOKEN, META_PRIORITY_ROUTE } from '@tramvai/module-seo';

createApp({
name: 'tincoin',
modules: [SeoModule],
providers: [
provide({
provide: META_UPDATER_TOKEN,
useValue: (meta) => {
meta.updateMeta(META_PRIORITY_ROUTE, {
ogTitle: 'Tramvai application',
});
},
}),
],
});

Dynamic meta

For example, your meta depends on API response. Actions is a good place to both save response to store and update meta tags by using META_WALK_TOKEN токен:

import { declareAction } from '@tramvai/core';
import { META_WALK_TOKEN, META_PRIORITY_APP } from '@tramvai/module-seo';

declareAction({
name: 'action',
fn() {
this.deps.meta.updateMeta(META_PRIORITY_APP, {
title: 'WoW, such dynamic!',
});
},
deps: {
meta: META_WALK_TOKEN,
},
conditions: {
dynamic: true,
},
});

JSON-LD

It's possible to set up JSON-LD for your routes. You can learn more about this technology here.
There are two ways to set it up:

import type { PageComponent } from '@tramvai/react';

const CommentsPage: PageComponent = () => <h1>Comments Page</h1>;

CommentsPage.seo = {
meta: {
structuredData: {
jsonLd: {
'@context': 'http://schema.org/',
'@type': 'Comments',
},
},
},
};

export default CommentsPage;
const route = {
name: 'comments',
path: '/comments/',
config: {
pageComponent: '@/pages/comments',
seo: {
meta: {
structuredData: {
jsonLd: {
'@context': 'http://schema.org/',
'@type': 'Comments',
},
},
},
},
},
};

Remember, that JSON-LD only generates for server requests, so it won't change on SPA transitions.

How-to

How to set a custom meta tag?

Custom meta tag description, usage example:

import { createApp, provide } from '@tramvai/core';
import { SeoModule, META_UPDATER_TOKEN, META_PRIORITY_ROUTE } from '@tramvai/module-seo';

createApp({
name: 'tincoin',
modules: [SeoModule],
providers: [
provide({
provide: META_UPDATER_TOKEN,
useValue: (meta) => {
meta.updateMeta(META_PRIORITY_ROUTE, {
metaCustom: {
tag: 'meta',
attributes: {
name: 'metaCustomNameAttribute',
content: 'metaCustomContent',
},
},
});
},
}),
],
});

And result will be - <meta name="metaCustomNameAttribute" content="metaCustomContent" data-meta-dynamic="true">

How-to remove meta tag?

Just return null as value for highest priority:

import { createApp, provide } from '@tramvai/core';
import { SeoModule, META_UPDATER_TOKEN, META_PRIORITY_ROUTE } from '@tramvai/module-seo';

createApp({
name: 'tincoin',
modules: [SeoModule],
providers: [
provide({
provide: META_UPDATER_TOKEN,
useValue: (meta) => {
meta.updateMeta(30, {
keywords: null,
});
},
}),
],
});

References

Meta tags list

Predefined list for easy adding meta tags:

  • title - <title> tag
  • description - <meta name="description"> tag
  • keywords - <meta name="keywords"> tag
  • canonical - <link rel="canonical"> tag
  • viewport - <meta name="viewport"> tag
  • ogTitle - <meta property="og:title"> tag
  • ogDescription - <meta property="og:description"> tag
  • ogSiteName - <meta property="og:site_name"> tag
  • ogUrl - <meta property="og:url"> tag
  • ogType - <meta property="og:type"> tag
  • ogImage - <meta property="og:image"> tag
  • ogImageSecure - <meta property="og:image:secure_url"> tag
  • ogImageType - <meta property="og:image:type"> tag
  • ogImageAlt - <meta property="og:image:alt"> tag
  • ogImageWidth - <meta property="og:image:width"> tag
  • ogImageHeight - <meta property="og:image:height"> tag
  • ogLocale - <meta property="og:locale"> tag
  • twitterTitle - <meta name="twitter:title"> tag
  • twitterDescription - <meta name="twitter:description"> tag
  • twitterCard - <meta name="twitter:card"> tag
  • twitterSite - <meta name="twitter:site"> tag
  • twitterCreator - <meta name="twitter:creator"> tag
  • twitterImage - <meta name="twitter:image"> tag
  • twitterImageAlt - <meta name="twitter:image:alt"> tag
  • robots - function with parameters (type: 'all' | 'noindex' | 'nofollow' | 'none') which returns <meta name="robots" content="none">, <meta name="robots" content="noindex, nofollow"> or <meta name="robots" content="noarchive"> tag

Custom meta tags

If you need to add meta tag which is not in the predefined list, you need to provide object with specific description:

type CustomTag = {
tag: string;
attributes?: {
[key: string]: any;
};
innerHtml?: string;
};

Testing

Testing work with META_UPDATER_TOKEN and META_DEFAULT_TOKEN

If you have a module or providers that define META_UPDATER_TOKEN or META_DEFAULT_TOKEN then it is convenient to use special utilities to test them separately:

import { Module, provide } from '@tramvai/core';
import { testMetaUpdater } from '@tramvai/module-seo/tests';
import { META_PRIORITY_APP, META_DEFAULT_TOKEN, META_UPDATER_TOKEN } from '@tramvai/module-seo';

describe('testMetaUpdater', () => {
it('modules', async () => {
const metaUpdater = jest.fn<
ReturnType<typeof META_UPDATER_TOKEN>,
Parameters<typeof META_UPDATER_TOKEN>
>((walker) => {
walker.updateMeta(META_PRIORITY_APP, {
title: 'test title',
});
});
@Module({
providers: [
provide({
provide: META_UPDATER_TOKEN,
multi: true,
useValue: metaUpdater,
}),
],
})
class CustomModule {}
const { renderMeta } = testMetaUpdater({
modules: [CustomModule],
});

const { render, metaWalk } = renderMeta();

expect(metaWalk.get('title').value).toBe('test title');
expect(render).toMatch('<title data-meta-dynamic="true">test title</title>');
});

it('providers', async () => {
const { renderMeta } = testMetaUpdater({
providers: [
provide({
provide: META_DEFAULT_TOKEN,
useValue: {
title: 'default title',
},
}),
],
});

const { render } = renderMeta();

expect(render).toMatch('<title data-meta-dynamic="true">default title</title>');
});
});

Known issues

Googlebot will parse everything inside your HTML document, including inline scripts, and even JSON located within those scripts. If Googlebot encounters a link, it might extract and attempt to access these URLs to index them.

When you are developing a Tramvai application, the most common case where this could happen is with the initial state. This initial state is passed from your server to the client inside an inline script with the __TRAMVAI_STATE__ id. Such initial states can also contain links. More information about state, you can read in State Management.

Unfortunately, there is no straightforward solution at this moment.

Links: