Angular2 - SEO - how to manipulate the meta description - seo

Search Results in google are displayed via TitleTag and the <meta name="description"..."/> Tag.
The <title>-Tag is editiable via Angular2 how to change page title in angular2 router
What's left is the description.
Is it possibile to write a directive in angular2, that manipulates the meta-tags in the <head> part of my page.
So depending on the selected route, the meta description changes like:
<meta name="description" content="**my description for this route**"/>

Since Angular4, you can use Angular Meta service.
import { Meta } from '#angular/platform-browser';
// [...]
constructor(private meta: Meta) {}
// [...]
this.meta.addTag({ name: 'robots', content: 'noindex' });

It is possible. I implemented it in my app and below I provide the description how it is made.
The basic idea is to use Meta from #angular/platform-browser
To dynamically change particular meta tag you have to:
Remove the old one using removeTag(attrSelector: string) : void
method.
Add the new one using addTag(tag: MetaDefinition, forceCreation?:
boolean) : HTMLMetaElement method.
And you have to do it when the router fires route change event.
Notice: In fact it is also necessary to have default <title>...</title> and <meta name="description"..." content="..."/> in head of index.html so before it is set dynamically there is already some static content.
My app-routing.module.ts content:
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import { NgModule } from '#angular/core';
import { RouterModule, Routes, Router, NavigationEnd, ActivatedRoute } from '#angular/router';
import { StringComparisonComponent } from '../module-string-comparison/string-comparison.component';
import { ClockCalculatorComponent } from '../module-clock-calculator/clock-calculator.component';
import { Title, Meta } from '#angular/platform-browser';
const routes: Routes = [
{
path: '', redirectTo: '/string-comparison', pathMatch: 'full',
data: { title: 'String comparison title', metaDescription: 'String comparison meta description content' }
},
{
path: 'string-comparison', component: StringComparisonComponent,
data: { title: 'String comparison title', metaDescription: 'String comparison meta description content' }
},
{
path: 'clock-time-calculator', component: ClockCalculatorComponent,
data: { title: 'Clock time calculator title', metaDescription: 'Clock time calculator meta description content' }
}
];
#NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private titleService: Title,
private metaService: Meta
){
//Boilerplate code to filter out only important router events and to pull out data object field from each route
this.router.events
.filter(event => event instanceof NavigationEnd)
.map(() => this.activatedRoute)
.map(route => {
while (route.firstChild) route = route.firstChild;
return route;
})
.filter(route => route.outlet === 'primary')
//Data fields are merged so we can use them directly to take title and metaDescription for each route from them
.mergeMap(route => route.data)
//Real action starts there
.subscribe((event) => {
//Changing title
this.titleService.setTitle(event['title']);
//Changing meta with name="description"
var tag = { name: 'description', content: event['metaDescription'] };
let attributeSelector : string = 'name="description"';
this.metaService.removeTag(attributeSelector);
this.metaService.addTag(tag, false);
});
}
}
As it can be seen there is an additional data object field for
each route. It contains title and metaDescription strings
which will be used as title and meta tag content.
In constructor we filter out router events and we subscribe to filtered
router event. Rxjs is used there, but in fact it is not necessary to use it. Regular if statements and loops could be used insead of stream, filter and map.
We also merge our data object field with our event so we can easily
use info like title and metaDescription strings.
We dynamically change <title>...</title> and <meta name="description"..." content="..."/> tags.
Effects:
First component
Second component
In fact I currently use a little bit more sophisticated version of this solution which uses also ngx-translate to show different title and meta description for different languages.
Full code is available in angular2-bootstrap-translate-website-starter project.
The app-routing.module.ts file with ngx-translate solution is exactly there: app-routing.module.ts.
There is also the production app which uses the same solution: http://www.online-utils.com.
For sure it is not the only way and there might be better ways to do it. But I tested this solution and it works.
In fact the solution is very similar to this from corresponding post about changing title: How to change page title in angular2 router.
Angular Meta docs: https://angular.io/docs/ts/latest/api/platform-browser/index/Meta-class.html. In fact they aren't very informative and I had to experiment and look into real .js code to make this dynamic meta change working.

I have developed and just released #ngx-meta/core plugin, which manipulates the meta tags at the route level, and allows setting the meta tags programmatically within the component constructor.
You can find detailed instructions at #ngx-meta/core github repository. Also, source files might be helpful to introduce a custom logic.

There is currently no out-of-the-box solution only an open issue to implement it https://github.com/angular/angular/issues/7438.
You can of course implement something like the title service yourself, just use the TitleService as template
A Meta service similar to Title service is in the works (currently only a pull request).

Related

How to use vue component across multiple node projects?

I'm trying to build a website builder within the drag-and-drop abilities via using Vue3. So, the user will be playing with the canvas and generate a config structure that going to post the backend. Furthermore, the server-side will generate static HTML according to this config.
Eventually, the config will be like the below and it works perfectly. The config only can have HTML tags and attributes currently. Backend uses h() function to generate dom tree.
My question is: can I use .vue component that will generate on the server side as well? For example, the client-side has a Container.vue file that includes some interactions, styles, etc. How can the backend recognize/resolve this vue file?
UPDATE:
Basically, I want to use the Vue component that exists on the Client side on the backend side to generate HTML strings same as exactly client side. (including styles, interactions etc).
Currently, I'm able to generate HTML string via the below config but want to extend/support Vue component itself.
Note: client and server are completely different projects. Currently, server takes config and runs createSSRApp, renderToString methods.
Here is the gist of how server would handle the API:
https://gist.github.com/yulafezmesi/162eafcf7f0dcb3cb83fb822568a6126
{
id: "1",
tagName: "main",
root: true,
type: "container",
properties: {
class: "h-full",
style: {
width: "800px",
transform: "translateZ(0)",
},
},
children: [
{
id: "9",
type: "image",
tagName: "figure",
interactive: true,
properties: {
class: "absolute w-28",
style: {
translate: "63px 132px",
},
},
},
],
}
This might get you started: https://vuejs.org/guide/scaling-up/ssr.html#rendering-an-app
From the docs:
// this runs in Node.js on the server.
import { createSSRApp } from 'vue'
// Vue's server-rendering API is exposed under `vue/server-renderer`.
import { renderToString } from 'vue/server-renderer'
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button #click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
console.log(html)
})
I guess extract the template from request or by reading the submitted Vue file and use that as the template parameter value

How do I properly import multiple components dynamically and use them in Nuxt?

I need to implement dynamic pages in a Nuxt + CMS bundle.
I send the URL to the server and if such a page exists I receive the data.
The data contains a list of components that I need to use, the number of components can be different.
I need to dynamically import these components and use them on the page.
I don't fully understand how I can properly import these components and use them.
I know that I can use the global registration of components, but in this case I am interested in dynamic imports.
Here is a demo that describes the approximate logic of my application.
https://codesandbox.io/s/dank-water-zvwmu?file=%2Fpages%2F_.vue
Here is a github issue that may be useful for you: https://github.com/nuxt/components/issues/227#issuecomment-902013353
I've used something like this before
<nuxt-dynamic :name="icon"></nuxt-dynamic>
to load dynamic SVG depending of the icon prop thanks to dynamic.
Since now, it is baked-in you should be able to do
<component :is="componentId" />
but it looks like it is costly in terms of performance.
This is of course based on Nuxt components and auto-importing them.
Also, if you want to import those from anywhere you wish, you can follow my answer here.
I used this solution. I get all the necessary data in the asyncData hook and then import the components in the created () hook
https://codesandbox.io/s/codesandbox-nuxt-uidc7?file=/pages/index.vue
asyncData({ route, redirect }) {
const dataFromServer = [
{
path: "/about",
componentName: 'myComponent'
},
];
const componentData = dataFromServer.find(
(data) => data.path === route.path
);
return { componentData };
},
data() {
return {
selectedRouteData: null,
componentData: {},
importedComponents: []
};
},
created() {
this.importComponent();
},
methods: {
async importComponent() {
const comp = await import(`~/folder/${this.componentData.componentName}.vue`);
this.importedComponents.push(comp.default);
}

How can I add vue-router link as ag-grid-vue column?

ag-grid-vue documentation from ag-grid website clearly says:
You can provide Vue Router links within the Grid, but you need to
ensure that you provide a Router to the Grid Component being created.
with sample code:
// create a new VueRouter, or make the "root" Router available
import VueRouter from "vue-router";
const router = new VueRouter();
// pass a valid Router object to the Vue grid components to be used within the grid
components: {
'ag-grid-vue': AgGridVue,
'link-component': {
router,
template: '<router-link to="/master-detail">Jump to Master/Detail</router-link>'
}
},
// You can now use Vue Router links within you Vue Components within the Grid
{
headerName: "Link Example",
cellRendererFramework: 'link-component',
width: 200
}
What's missing here is how to make the "root" Router available. I've been looking into various sources and see many people have the same problem, but none got a clear answer.
https://github.com/ag-grid/ag-grid-vue/issues/1
https://github.com/ag-grid/ag-grid-vue/issues/23
https://github.com/ag-grid/ag-grid-vue-example/issues/3
https://forum.vuejs.org/t/vue-cant-find-a-simple-inline-component-in-ag-grid-vue/21788/10
Does ag-grid-vue still work with vue-router, then how, or is this just outdated documentation? Some people claim it worked for them so I assume it worked at one point.
I am not looking for cool answer at this point. I just want to know if it is possible. I tried passing router using window or created() and none worked so far.
Thank you!
the approach suggested by #thirtydot works well. The only downside was the user cannot right-click, but I found you can just define href link. So when you left-click, event listener makes use of router. When you right-click and open in new tab, browser takes href link.
You still need to make your root router available. Below code sample assumes you have the code inside the vue-router-aware Vue component that consumes ag-grid, hence this.$router points to the root router.
{
headerName: 'ID',
field: 'id',
cellRenderer: (params) => {
const route = {
name: "route-name",
params: { id: params.value }
};
const link = document.createElement("a");
link.href = this.$router.resolve(route).href;
link.innerText = params.value;
link.addEventListener("click", e => {
e.preventDefault();
this.$router.push(route);
});
return link;
}
}

Vuejs + vue-router, pass data between views like http post

I'm try to pass data between Vuejs views with vue-router.
//View1.vue
route: {
data: function (transition) {
transition.next({
message: "this is it!!"
});
}
}
I call next wiew with a click action button with:
//View1.vue
methods:{
showResult: function(){
this.$router.go('/View2');
}
}
but the data are not filled in the next view:
//View2.vue
<template>
<p>Message: {{ message }}</p>
</template>
Does somebody knows what's wrong with my usage of vue-router? I don't think I need to pass through services for this, right?
Working examples on jsfiddle (or jsbin, etc) are welcome :D
If View2 is a child component you can pass it using props:
//View1.vue
<view2-component :passedData='message'></view2-component>
Alternatively, I believe if you set data on the $route object from View1, since that object is shared between all vue instances, I believe it will be available application-wide.
//View1.vue
this.$router.myProps.message = message
But arguably the better way to share data is use a POJO - plain old javascript object and bind it to both views. To do this you typically need a shared state object and you can if you wish use Vuex for this although it is a little more complicated than a POJO.
I know this has already been answered, but if someone is here looking for a way to pass data to a route from a router, I use Meta Data.
Not sure if this is what the questioner meant or not but I think it is?
I personally prefer this to props just because I am more used to using it.
It allows for data to be easily passed and received without having to modify children.
Anyway here is a snippit and link!
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Homepage',
meta: {
logo:{
"/imgs/Normal-Logo.png"
}
}
},
{
path: '/admin',
name: 'Admin',
meta: {
logo:{
"/imgs/Admin-Logo.png"
}
}
},
]
})
In any children who want to use vars:
<logo :src="this.$route.meta.logo"/>
Ref:
https://router.vuejs.org/guide/advanced/meta.html

Aurelia: How navigate between child routes

I'm trying to navigate from one child route to another, but I continually get Route not found. My primary question: how to navigate between child views?
Below is the code, and I'll have additional questions below, too.
App Mode-View
App Class:
export class App {
configureRouter(config, router) {
config.title = 'My Site';
config.map([
{ route: ['','job-view'], name: 'jobView', moduleId: './job-view', nav: true, title:'Jobs'},
{ route: ['services'], name: 'services', moduleId: './services', nav: true, title:'Services'}
]);
this.router = router;
this.router.refreshNavigation();
}
}
Q.2: Why do we need to save router here if it's always accessible from aurelia-router?
App Page:
<template>
<require from='./nav-bar'></require>
<nav-bar router.bind="router"></nav-bar>
<div class="container">
<router-view></router-view>
</div>
</template>
Ok, so now that we have our root page view and nav defined, let's define the job-view MV.
JobView Class:
export class JobView {
configureRouter(config, router) {
config.map([
{ route: ['','jobs'], name: 'jobs', moduleId: './jobs', nav: false, title:'Jobs', settings:{icon:'glyphicon glyphicon-align-justify'} },
{ route: ['job/:id'], name: 'job', moduleId: './job', nav: false, title:'Job Details'}
]);
this.router = router; //WHY SAVE THIS?
this.router.refreshNavigation();
}
}
JobView Page:
<template>
<router-view></router-view>
</template>
Now, here are the child views. My assumption that is that routing that occurs should be relative to job-view. That's what I want, ideally.
Jobs Class (a bunch of code removed for brevity):
import {Router} from 'aurelia-router';
#inject(Router)
export class Jobs {
constructor(router) {
this.router = router;
}
toJob(id) {
// this.router.navigateToRoute("job", {id:id}); // ERROR - ROUTE NOT FOUND
this.router.navigate("#/job-view/job/".concat(id)); // THIS WORKS
}
}
Q.3: I've seen both router.navigateToRoute and router.navigate referenced, but no indication when to use either or what the difference is, and the document doesn't seen to explain. Which should be used? Docs
Jobs Page:
Details for jobs.html are irrelevant, so not listing them here.
Finally, the job view:
Job Class:
Nothing relevant for job.js, so not listing code. At most I may perform navigation back to jobs, but that's handled below in the page.
Job Page:
<!-- a bunch of html //-->
<!-- HOW TO USE ROUTER IN HTML, NOT BELOW URL HREF? //-->
Print Jobs
<!-- a bunch of html //-->
Q.4: Again, I'd like routing to relative, even in the HTML page. The above won't work, so what should I use?
There was a proposed answer in a similar question, but injecting job-view into job and using job-view's saved router didn't work either.
By the way, if I manually navigate to http://localhost:3000/#/job-view/job/3 the page loads fine, so it's clear something with the router.
Note to mod:
A similar question was ask at How to access child router in Aurelia? but it wasn't answered with a solution that works.
I will try to answer you questions one by one below.
I will start from Q2
Q.2: Why do we need to save router here if it's always accessible from
aurelia-router?
So in your App Mode-View App Class you are referencing router property in your view: <nav-bar router.bind="router"></nav-bar>, that's why you need to declare the property to use it then. In the second view you are not so you don't need it :-)
The property router is also added when you need do something with the router in main.ts / main.js - the starting point of you application. This is because the router is configured for the first time there, and injection will not work in constructor, so you need to save this property to get it in configureRouter(..) method (note: this was a case before beta 1, I don't know if it's still there now).
In your code you have a call for this.router.refreshNavigation(); this will ensure that your router is updated with new information regarding current location of the routing.
Q.3: I've seen both router.navigateToRoute and router.navigate referenced, but no indication when to use either or what the difference is, and the document doesn't seen to explain. Which should be used? Docs
The method router.navigate(fragment: string, options?: any) uses an URL fragment not a route name to navigate, so e.g. router.navigate('#/app/child', {...<options - not params od the URL>...}). This method must be used to navigate absolutely between routers, and to access parent URL etc.
If you only are navigating around the current router you will always use router.navigateToRoute(route: string, params?: any, options?: any). This method is using a route name, not URL, co we just put there a name of route in the custom routing map (custom means the current child routing map, or current main routing regarding the URL location we are on the page). Here you can pass URL params in more convenient way, as you can see. You can use a params object instead of concatenating the URL with params.
Q.4: Again, I'd like routing to relative, even in the HTML page. The above won't work, so what should I use?
In Aurelia we are not using href attribute of the a tag directly for navigation. As already answered by Brandon, you have to use route-href attribute, which is probably nowhere documented just appears around on forums and portals. This is equivalent of the router.navigateToRoute(route: string, params?: any, options?: any), so you cannot use it to navigate between routers in such case you can use custom attribute or just use click.triger="navTo('#/app/child')", where the navTo() method is implemented in your View-Model and looks like this:
public navTo(routeName: string) {
// Assuming you are injecting router somewhere in the constructor
this.router.navigateToRoute(routeName);
}
And finally your topic question:
Q.1: How navigate between child routes
Probably now you know the answer, just use: router.navigate(fragment: string, options?: any) with absolute URL.
Below example custom attribute to solve this:
import {inject} from "aurelia-dependency-injection";
import {Router} from "aurelia-router";
import {customAttribute} from "aurelia-framework";
#customAttribute('nav-href')
#inject(Element, Router)
export class NavHref {
private value: string = '';
constructor(private element: Element, private router: Router) {
let $this = this;
element.addEventListener('click', () => {
if ($this.value === 'back') {
$this.router.navigateBack();
} else {
// expression
$this.router.navigate($this.value);
}
});
}
public valueChanged(newValue: string, oldValue: string) {
this.value = newValue;
}
}
First you need to import it in your HTML, I named my file nav.href.ts:
<require from="common/nav.href"></require>
Then just use it in you HTML code:
<a nav-href="#/home/any-location">My link to any location</a>
Hope this will help you, cheers :-)
The way I've configured child routers in my Aurelia apps is in this fashion:
app.js
export class App {
configureRouter(config, router) {
config.map([
{ route: ['','home'], name: 'home', moduleId: 'home', nav: true },
{ route: 'work', name: 'work', moduleId: 'work/work-section', nav: true },
]);
this.router = router;
}
}
work/work-section.js
export class WorkSection {
configureRouter(config, router) {
config.map([
{ route: '', moduleId: './work-list', nav: false },
{ route: ':slug', name: 'workDetail', moduleId: './work-detail', nav: false }
]);
this.router = router;
};
}
The corresponding "work-section.html" is simply a Router View:
<template>
<router-view></router-view>
</template>
In this use case, I have my main app.js which defines a child router, "work", which sits in a subdirectory under src.
When the route /work is activated, the child router, "work-section" takes over, activating the "work-list" route, as the path segments end there: /work
"work-list.js" retrieves items from a REST API then passes the data to the view.
From there, I'm able to use route binding to get to a "work detail" in the "work-list.html" view:
<div repeat.for="sample of item.samples">
<a route-href="route: workDetail; params.bind: { slug: sample.slug }">
${sample.title}
</a>
</div>
Hope that helps you out. I'm not 100% certain if you're asking how to do a redirect, or how to nav to a child route from the view, so please correct me if I'm wrong and I'll do my best to update my answer for you.