AnalogJs: SSR para Angular

Felipe Norato
8 min readJul 18, 2023

--

Já tem um tempo que a comunidade Angular vem sofrendo com SSR, eu particularmente trabalhei pouco em applicações que dependiam muito dessa tecnologia. Porém quando tive contato com SSR utilizei duas ferramentas o Scully e o Angular Universal, onde tudo parecia meio complicado e não tão funcional para SSR.

Há poucos meses fui apresentado ao AnalogJS, uma lib nova, ainda em beta, que promete resolver vários problemas de SSR no ecossistema Angular.

Neste artigo eu não entrarei nos detalhes de como funciona o SSR, motores de busca, e sim na minha experiência com essa lib, baseada na live do Brandon Roberts, criador da lib.

Como de costume deixo também o repositório do teste.

Instalação

A instalação do AnalogJS possui um schematics, para gerar um projeto com toda a estrutura pronta para o projeto. Nada de ter que fazer várias configurações para dar um start na aplicação.

É importante ressaltar sobre a compatibilidade do nodejs, hoje no momento que escrevo esse artigo, o suporte é para as versões 16.17.0, 18.13.0 ou superior. Portanto eu fortemente recomendo o uso de um gerenciador de versões do node, eu uso o nvm e me resolvo muito bem com ele. A equipe está alerta quanto a isso, e me responderam super rápido num domingo a noite. 👏🏻

Enfim, o comando para gerar o projeto é esse:

npm create analog@latest

Onde, assim como o nx e o angular/cli, você irá configurar suas preferências respondendo as perguntas do cli. Neste commit pode-se ver a estrutura gerada para o projeto.

Estrutura

Aproveitando a maravilha dos standalone components do Angular, as páginas do Analog são standalone componentes e export default.

Mas por que, export default? A não obrigatoriedade do uso de módulos no Angular possibilitou uma flexibilidade significativa na sintaxe, incluindo, para AnalogJS definir as rotas baseadas em arquivos.

Isso quer dizer que, na pasta pages do projeto o nome dos arquivos dos componentes já é a definição das rotas!

A única coisa que muda aqui é o padrão de nome de componentes, como iremos, nesta pasta pages, tratar de pages (que sim, são componentes). mas ao invés de foobar.component.ts iremos definir como foobar.page.ts para a página foobar. Só isso!

As páginas são export default standalone componentes com o posfixo page.ts

import { Component } from '@angular/core';

@Component({
selector: 'app-about',
standalone: true,
template: ` <h1>About Page</h1> `,
})
export default class AboutPageComponent {}

E sim, podemos usar componentes normais dentro das páginas, só pelamor, não faz um export default dos componentes, por que isso vira uma bagunça.

Route params

Graças ao Angular 16 que introduziu o ComponentInputBinding, onde você pode pegar via input os parametros da rota, o AnalogJs é possível usar a mesma estratégia.

Então ao definir no arquivo app.config.ts o provider withComponentInputBinding, as páginas serão capaz de extrair os parâmetros da rota.

// src/app/app.config.ts

import { withComponentInputBinding } from '@angular/router';

export const appConfig: ApplicationConfig = {
providers: [
provideFileRouter(withComponentInputBinding()), // atualizar esta linha
provideHttpClient(),
provideClientHydration(),
//
],
};

Ótimo, mas como eu defino que uma rota possui parâmetro, eu não tenho um route modules…

Definimos os parâmetro da rota da mesma maneira que defino uma rota… pelo nome do arquivo.

Se eu quero a rota blog/:slug, eu crio um arquivo blog.[slug].page.ts, e definindo um Input como slug, o parâmetro da rota será injetado!

// src/app/pages/blog.[slug].page.ts

import { Component, Input } from '@angular/core';

@Component({
selector: 'app-blog-post',
standalone: true,
template: `
<h1>Blog Post</h1>
<h2>{{ slug }}</h2>
`,
})
export default class BlogPostPageComponent {
@Input() slug?: string;
}

Página de erro

Nas configurações de rota do Angular podemos definir tanto a página de erro quanto um redirect para outra página quando a rota não existe. Usando o AnalogJs temos as duas maneiras de fazer.

Rota de erro

No Angular para criar uma rota de erro, podemos fazer algo parecido com isso

// Definição no Angular

const routes: Route = [
// outras rotas
{
path: '**',
component: PageNotFoundComponent
}
];

Enquanto no AnalogJs, criamos uma página com o nome […page-not-found].page.ts.

// src/app/pages/[...page-not-found].page.ts

import { Component } from '@angular/core';

@Component({
selector: 'app-error',
standalone: true,
template: ` <h1>Error</h1> `,
})
export default class PageNotFoundPageComponent {}

Redirect

Agora caso seja necessário fazer um redirect ao entrar numa rota, como por exemplo, da /foo para /foo/home.

Basta definir a rota da página, ou seja, criar o arquivo correspondente aquela rota. E em seguida, definir um routeMeta, com o redirectTo. Bem parecido como é feito no Angular.

// src/app/pages/foo.page.ts

import { RouteMeta } from '@analogjs/router';

export const routeMeta: RouteMeta = {
redirectTo: '/foo/home',
pathMatch: 'full',
};

Já que falei sobre o routeMeta, é bom falar um pouco mais. Essa constante é necessariamente um “partial” do Route do Angular (omitindo algumas props).

Porém o mais importante é o que a gente pode fazer, que é definir Guards, Resolvers, adicionar data, title, meta, e, os já mencionados redirects.

Lembrando que já podemos definir inline guards e resolvers.

export const routeMeta: RouteMeta = {
canActivate: [() => true],
};

Definindo rotas filhas

Como já foi falado, as rotas da aplicação AnalogJs é baseado em rotas, e também podemos aplicar todos os conceitos de rotas filhas do Angular.

Vou separar em 3 exemplos onde, vou compara com a rota do Angular

Rota simples

Sim, me faltou um nome mais técnico, porém com o exemplo voce vai entender.

// Definição no Angular

const routes: Route = [
// outras rotas
{
path: 'aqui-sem-pagina/aqui-tambem/aqui-tem-pagina-finalmente',
component: AquiTerminaComponent
}
];typ

Para este exemplo é apenas necessário criar um arquivo com o nome aqui-sem-pagina.aqui-tambem.aqui-tem-pagina-finalmente.page.ts.

Simples, só substituir as barras de separação de path por ponto.

Rotas irmãs

As rotas irmãs são as que não possuiem um componente hierarquicamente superior neste domínio, ou seja, a rota pai é vazia, assim se tornando irmã. Fez sentido? Então vamos ao exemplo.

// Definição no Angular

const routes: Route = [
// outras rotas
{
path: 'algum-dominio',
children: [
{
path: '',
component: DominioBarraPathComponent
},
{
path: 'sub-page',
component: DominioSubPagePathComponent
},
{
path: ':outra',
component: DominioComParametroPathComponent
},

]
}
];

Para este caso é necessário criar uma pasta com o nome algum-dominio e dentro da pasta criar os arquivos:

/pages
/algum-dominio
index.page.ts
sub-page.page.ts
[outra].page.ts

Rotas irmãs com wrapper

Este exemplo é nada mais que criar um componente hierarquicamente superior, que possui um router-outlet, assim servindo de wrapper para os componentes filhos.

// Definição no Angular

const routes: Route = [
// outras rotas
{
path: 'outro-dominio',
component: OutroDominioWrapperComponent,
children: [
{
path: '',
component: OutroDominioBarraPathComponent
},
{
path: 'sub-page',
component: OutroDominioSubPagePathComponent
},
{
path: ':outra',
component: OutroDominioComParametroPathComponent
},

]
}
];

O que muda aqui é a necessidade criar um component para esse wrapper que deve ficar um nível acima, conforme a estrutura de pastas abaixo.

/pages
outro-dominio.page.ts
/outro-dominio
index.page.ts
sub-page.page.ts
[outra].page.ts

Este componente wrapper é da mesma forma que iriamos fazer com um componente Angular.

// src/app/pages/outro-dominio.page.ts
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
selector: 'app-outro-dominio',
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
<h1>Wrapper para esse domínio</h1>
<a [routerLink]="['/outro-dominio']"> Home</a>
<a [routerLink]="['/outro-dominio/sub-page']"> Child Route</a>
<a [routerLink]="['/outro-dominio/any-slug-aqui']"> Child Route com slug</a>
<router-outlet />
`,
})
export default class ParentPageComponent {}

Conteúdo em Markdown

Outra funcionalidade que pode ser muito usada é criar páginas a partir de arquivos markdown, que já é build-in do AnalogJs.

Só temos que voltar naquele arquivo de configurações que adicionamos o provider dos inputs e adicionar o provider de conteúdo markdown

import { provideContent, withMarkdownRenderer } from '@analogjs/content';
import { provideFileRouter } from '@analogjs/router';
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { withComponentInputBinding } from '@angular/router';

export const appConfig: ApplicationConfig = {
providers: [
provideFileRouter(withComponentInputBinding()),
provideHttpClient(),
provideClientHydration(),
provideContent(withMarkdownRenderer()), // adicionar esta linha
],
};

Agora o app já está pronto para exibir os conteúdos em markdown. E podemos exibir o conteúdo específico de um arquivo, só criando um arquivo na pasta pages, como por exemplo

/pages
any-content.md

Este arquivo any-content.md, poderá ser acessado pela rota /any-content. Não é necessário criar component, ou adicionar o posfixo page. Qualquer arquivo .md na pasta pages será interpretado como uma página

Trabalhando de forma dinâmica com os arquivos

Agora se for necessário renderizar esse markdown dentro de um outra página de forma mais estruturada, ou de forma dinâmica. A solução é criar a pasta content, dentro da pasta src, e nesta pasta criar os arquivos markdown.

Estes arquivos podemos colocar meta atributos, como o slug e o título:

---
title: Post out
slug: post-out
---

#Post Out

Assim podemos listar os arquivos:

//src/app/pages/posts/index.page.ts

import { injectContentFiles } from '@analogjs/content';
import { NgFor } from '@angular/common';
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Post } from '../../shared/posts';

@Component({
selector: 'app-posts-template',
standalone: true,
imports: [NgFor, RouterLink],
template: `
<ul>
<li *ngFor="let post of posts">
<a routerLink="/posts/{{ post.slug }}"> {{ post.attributes.title }}</a>
</li>
</ul>
`,
})
export default class PostsPageComponent {
posts = injectContentFiles<Post>();
}

E renderizar um arquivo:

//src/app/pages/posts/[slug].page.ts

import { MarkdownComponent, injectContent } from '@analogjs/content';
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { Post } from 'src/app/shared/posts';

@Component({
selector: 'app-post-content',
standalone: true,
imports: [AsyncPipe, MarkdownComponent],
template: `
<h1>Blog Post</h1>
<analog-markdown [content]="(post$ | async)?.content" />
`,
})
export default class PostContentPageComponent {
post$ = injectContent<Post>('slug');
}

Separando em sub-pastas

Caso seja necessário dividir em sub pastas esses arquivos, tanto a forma de listar quanto de renderizar muda um pouco

Para listar

// src/app/pages/posts/index.page.ts

import { injectContentFiles } from '@analogjs/content';
import { NgFor } from '@angular/common';
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Post } from '../../shared/posts';

@Component({
selector: 'app-posts-template',
standalone: true,
imports: [NgFor, RouterLink],
template: `
<ul>
<li *ngFor="let post of posts">
<a routerLink="/posts/{{ post.slug }}"> {{ post.attributes.title }}</a>
</li>
</ul>
`,
})
export default class PostsPageComponent {
posts = injectContentFiles<Post>((contentFile) =>
contentFile.filename.includes('/content/posts')
);
}

Para renderizar

// src/app/pages/posts/[slug].page.ts

import { MarkdownComponent, injectContent } from '@analogjs/content';
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { Post } from 'src/app/shared/posts';

@Component({
selector: 'app-post-content',
standalone: true,
imports: [AsyncPipe, MarkdownComponent],
template: `
<h1>Blog Post</h1>
<analog-markdown [content]="(post$ | async)?.content" />
`,
})
export default class PostContentPageComponent {
post$ = injectContent<Post>({
param: 'slug',
subdirectory: 'posts',
});
}

Build

O build é realizado pelo ng build, pois já está tudo configurado. E este build irá gerar as páginas estáticas, com um servidor que pode ser executado para renderizar as páginas.

node dist/analog/server/index.mjs

Considerações

Como eu havia dito, eu tenho pouca experiência com SSR, então não conheço profundamente alguns conceitos, porém, acredito que pude ajudar com o que pude captar do vídeo e da documentação do AnalogJs.

O AnalogJs é uma lib que ainda está em beta, passa por muitas instabilidades, porém eu vejo ter um futuro muito interessante. É fácil de usar, a equipe que está contribuindo é muito boa e já vi evoluções no tempo que testei e escrevi esse artigo, como por exemplo o suporte ao NX, que pretendo fazer alguns testes.

Dúvidas, sugestões, e comentário… fiquem à vontade.

--

--

Felipe Norato
Felipe Norato

Written by Felipe Norato

A person who likes to solve people’s lives using Code and sometimes play Guitar. Lover of TV Shows and Movies, as well as beautiful and performative code.

No responses yet