WordPress y Angular
Hace unos meses wordpress mando una actualización de su sistema y ahora acepta peticiones api rest, estas tratan de sustituir el odioso xml y dar la oportunidad de unificar varios sistemas con peticiones rest.
La idea principal es que wordpress en este momento es lo que mas encuentras en la web, asi que al ser muy usada han creado plugin de todo tipo, pero al ser wordpress algo cerrado no se podía exportar fácilmente esa base de datos para proyectos más a la medida.
aqui un grafico del mercado que abarca wordpress, si desean tener más información hagan click en la imagen para la página web que realizo esto.
WordPress y Angular consumiendo api rest
Una vez estudiado la api de wordpress se me ocurrió crear una sección en mi pagina web para mostrar mis publicaciones en unprogramador, esta web está hecha en angular.
la ventaja de wordpress es que su interfaz así como su funcionalidad está muy hecha para gente que no tiene experiencia, y esto hace que nos olvidemos un poco sobre esa planeación.
Api Rest WordPress
Para esta parte wordpress analizó que su estructura interna, y nos creó los accesos directo a las base de datos, estas son las entradas
Posts | /wp/v2/posts |
Post Revisions | /wp/v2/revisions |
Categories | /wp/v2/categories |
Tags | /wp/v2/tags |
Pages | /wp/v2/pages |
Comments | /wp/v2/comments |
Taxonomies | /wp/v2/taxonomies |
Media | /wp/v2/media |
Users | /wp/v2/users |
Post Types | /wp/v2/types |
Post Statuses | /wp/v2/statuses |
Settings | /wp/v2/settings |
con esto ya podemos llamar a nuestro wordpress y capturar los datos, por default todas las llamadas están habilitadas solamente para peticiones get.
WordPress usa una estructura (no es al 100%) en el json llamada HAL, que básicamente es una forma de ordenar ese json para que nadie se confunda.
para hacer una llamada exitosa hay que sobreponer lo siguiente, /wp-json/ con esto vamos a poder hacer nuestra peticiones un ejemplo, los post de unprogramador.com
para visualizarlo mejor recuerden usar postman.
Consumiendo api rest con Angular
como hemos visto en este sitio (curso de angular), angular es muy flexible para programar, así que la idea es que nosotros podamos hacer un front end sin la necesidad de aprender php + plantillas de wordpress.
ya estan publicado los concepto básico para poder consumir un web services
Modelos de datos WordPress y Angular
antes que nada tenemos que crear nuestro modelo de datos la estructura es la siguiente (esta puede cambiar dependiendo si tienen algunos plugin)
[ { "id": 780, "date": "2018-03-14T15:38:42", "date_gmt": "2018-03-14T21:38:42", "guid": { "rendered": "https://unprogramador.com/?p=780" }, "modified": "2018-03-14T15:42:09", "modified_gmt": "2018-03-14T21:42:09", "slug": "metadatos-para-paginas-web-seo", "status": "publish", "type": "post", "link": "https://unprogramador.com/metadatos-para-paginas-web-seo/", "title": { "rendered": "Metadatos sociales para páginas web (seo)" }, "content": { "rendered": " html del contenido", "protected": false }, "excerpt": { "rendered": "<p>Usar metadatos en nuestra página web ayuda a que nos podamos posicionar mejor en los buscadores (seo), pero aparte de esto tenemos más posibilidades visuales así como los primeros pasos de hacer nuestra web app.</p>\n", "protected": false }, "author": 1, "featured_media": 792, "comment_status": "open", "ping_status": "open", "sticky": false, "template": "", "format": "standard", "meta": { "amp_status": "" }, "categories": [ 5, 186 ], "tags": [ 327, 326, 328, 329, 323, 324, 330, 331, 332, 325, 333, 334, 283, 316 ], "jetpack-related-posts": [ { "id": 681, "url": "https://unprogramador.com/propiedad-position-en-css/", "url_meta": { "origin": 780, "position": 0 }, "title": "Propiedad Position en CSS", "date": "30/01/2018", "format": false, "excerpt": "Esta propiedad siempre puede provocar confusión ya se ha durante el aprendizaje o el desarrollo de un sitio web. La Propiedad Position en CSS nos permite cambiar el comportamiento y la posición de un elemento, sin embargo, no todos sabemos el uso correcto de las opciones. Así que veremos cuales…", "rel": "nofollow", "context": "En \"Breakpoint\"", "img": { "src": "https://i1.wp.com/unprogramador.com/wp-content/uploads/2017/03/379080_168a_2.jpg?fit=750%2C422&ssl=1&resize=350%2C200", "width": 350, "height": 200 }, "classes": [] }, { "id": 766, "url": "https://unprogramador.com/adaptar-pantallas-pagina-web-para-movil/", "url_meta": { "origin": 780, "position": 1 }, "title": "Adaptar pantallas en página web para android, iPad y iPhone", "date": "11/03/2018", "format": false, "excerpt": "En la actualidad no podemos olvidar adaptar pantallas en nuestras páginas web, esto puede ser la diferencia que tengamos visitas o no, en este post intentamos explicar como lograr esto con javascript nativo y no morir en el intento.", "rel": "nofollow", "context": "En \"Líneas de código\"", "img": { "src": "https://i2.wp.com/unprogramador.com/wp-content/uploads/2017/04/desarrollo-web.jpg?fit=1200%2C800&ssl=1&resize=350%2C200", "width": 350, "height": 200 }, "classes": [] }, { "id": 543, "url": "https://unprogramador.com/crear-componente-router-angular-4/", "url_meta": { "origin": 780, "position": 2 }, "title": "Angular 4 - Crear Componente router", "date": "04/11/2017", "format": false, "excerpt": "Explicamos que es y para que sirve el componente router en angular 4, y como hacer para cambiar secciones sin refrescar tu página web.", "rel": "nofollow", "context": "En \"Angular\"", "img": { "src": "https://i2.wp.com/unprogramador.com/wp-content/uploads/2017/05/angulardev.jpg?fit=1200%2C628&ssl=1&resize=350%2C200", "width": 350, "height": 200 }, "classes": [] } ], "_links": { "self": [ { "href": "https://unprogramador.com/wp-json/wp/v2/posts/780" } ], "collection": [ { "href": "https://unprogramador.com/wp-json/wp/v2/posts" } ], "about": [ { "href": "https://unprogramador.com/wp-json/wp/v2/types/post" } ], "author": [ { "embeddable": true, "href": "https://unprogramador.com/wp-json/wp/v2/users/1" } ], "replies": [ { "embeddable": true, "href": "https://unprogramador.com/wp-json/wp/v2/comments?post=780" } ], "version-history": [ { "href": "https://unprogramador.com/wp-json/wp/v2/posts/780/revisions" } ], "wp:featuredmedia": [ { "embeddable": true, "href": "https://unprogramador.com/wp-json/wp/v2/media/792" } ], "wp:attachment": [ { "href": "https://unprogramador.com/wp-json/wp/v2/media?parent=780" } ], "wp:term": [ { "taxonomy": "category", "embeddable": true, "href": "https://unprogramador.com/wp-json/wp/v2/categories?post=780" }, { "taxonomy": "post_tag", "embeddable": true, "href": "https://unprogramador.com/wp-json/wp/v2/tags?post=780" } ], "curies": [ { "name": "wp", "href": "https://api.w.org/{rel}", "templated": true } ] } } ]
este es el json que arroja una sola publicación, y de aquí debemos crear nuestro modelo para que los datos sean fácil de usar, me di la tarea de crear esta model.
export interface Guid { rendered: string; } export interface Title { rendered: string; } export interface Content { rendered: string; protected: boolean; } export interface Excerpt { rendered: string; protected: boolean; } export interface Meta { amp_status: string; } export interface UrlMeta { origin: number; position: number; } export interface Img { src: string; width: number; height: number; } export interface JetpackRelatedPost { id: number; url: string; url_meta: UrlMeta; title: string; date: string; format: boolean; excerpt: string; rel: string; context: string; img: Img; classes: any[]; } export interface Self { href: string; } export interface Collection { href: string; } export interface About { href: string; } export interface Author { embeddable: boolean; href: string; } export interface Reply { embeddable: boolean; href: string; } export interface VersionHistory { href: string; } export interface WpFeaturedmedia { embeddable: boolean; href: string; } export interface WpAttachment { href: string; } export interface WpTerm { taxonomy: string; embeddable: boolean; href: string; } export interface Cury { name: string; href: string; templated: boolean; } export interface Links { self: Self[]; collection: Collection[]; about: About[]; author: Author[]; replies: Reply[]; version_history: VersionHistory[]; wp_featuredmedia: WpFeaturedmedia[]; wp_attachment: WpAttachment[]; wp_term: WpTerm[]; curies: Cury[]; } export class WordPress { id: number; date: Date; date_gmt: Date; guid: Guid; modified: Date; modified_gmt: Date; slug: string; status: string; type: string; link: string; title: Title; content: Content; excerpt: Excerpt; author: number; featured_media: number; comment_status: string; ping_status: string; sticky: boolean; template: string; format: string; meta: Meta; categories: number[]; tags: number[]; jetpack_related_posts: JetpackRelatedPost[]; _links: Links; }
ya con este modelos podemos consultar la api y tener un orden.
Creando un services para consumir api
recuerden que estas tareas que hagan procesos de datos siempre hay que separarlo de nuestra vista para esto vamos a crear nuestro service en angular con angular-cli.
ng g service services/wordpress
en este services lo que vamos a hacer es un llamado por get usando la librería httpClient, para regresar el json y transformarlo a nuestro modelo.
getPostWordpress(): Observable<Wordpress[]>{ return this.http.get<Wordpress[]>(environment.posts); }
con esto ya tenemos la petición.
Algo que tengo que aclarar es que los post de la api rest de wordpress no vienen con imagenes, si no que hay que hacer otro llamado a la ruta de media.
para esto hay un valor que se llama featured_media que es la que enlaza el número id de la imagen principal, teniendo esta problemática, mi primera opción era un array que guardara los link, pero tuve un error ya que los link no se guardaba de la forma como venía la info de los post.
y practicamente las imagenes eran al azar y no concordaba con la informacion, asi que utilice un valor tipo map que nos da typescript (como c# o java) con la siguiente estructura
map<tipo variable id, tipo valor>
let mapImg = new Map<number,string>();
getListMedia(wp: any):Observable<any>{ wp.forEach(e => { this.http.get<WordpressImg>(environment.media + e.featured_media).subscribe( resp => this.mapImg.set(e.featured_media, resp.source_url)); }); return of(this.mapImg); }
con esto pasamos el valor de la respuesta post y extraemos el id de la imagen, consultamos y guardamos las referencias.
Clase completa de services
import { Injectable } from '@angular/core'; import {Wordpress} from '../models/wordpress' import {WordpressImg} from '../models/wordpressImg'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import {HttpClient, HttpHeaders} from '@angular/common/http'; import 'rxjs/add/operator/map'; import {environment} from '../../environments/environment'; @Injectable() export class WordPressService { mapImg = new Map<number,string>(); constructor(private http: HttpClient) { } getPostWordpress(): Observable<Wordpress[]>{ return this.http.get<Wordpress[]>(environment.posts); } getListMedia(wp: any):Observable<any>{ wp.forEach(e => { this.http.get<WordpressImg>(environment.media + e.featured_media).subscribe( resp => this.mapImg.set(e.featured_media, resp.source_url)); }); return of(this.mapImg); } }
Modelo para api de media
export interface Guid { rendered: string; } export interface Title { rendered: string; } export interface Meta { amp_status: string; } export interface Description { rendered: string; } export interface Caption { rendered: string; } export interface Thumbnail { file: string; width: number; height: number; mime_type: string; source_url: string; } export interface Medium { file: string; width: number; height: number; mime_type: string; source_url: string; } export interface MediumLarge { file: string; width: number; height: number; mime_type: string; source_url: string; } export interface Large { file: string; width: number; height: number; mime_type: string; source_url: string; } export interface PostThumbnail { file: string; width: number; height: number; mime_type: string; source_url: string; } export interface Full { file: string; width: number; height: number; mime_type: string; source_url: string; } export interface Sizes { thumbnail: Thumbnail; medium: Medium; medium_large: MediumLarge; large: Large; post_thumbnail: PostThumbnail; full: Full; } export interface ImageMeta { aperture: string; credit: string; camera: string; caption: string; created_timestamp: string; copyright: string; focal_length: string; iso: string; shutter_speed: string; title: string; orientation: string; keywords: any[]; } export interface MediaDetails { width: number; height: number; file: string; sizes: Sizes; image_meta: ImageMeta; } export interface Self { href: string; } export interface Collection { href: string; } export interface About { href: string; } export interface Author { embeddable: boolean; href: string; } export interface Reply { embeddable: boolean; href: string; } export interface Links { self: Self[]; collection: Collection[]; about: About[]; author: Author[]; replies: Reply[]; } export interface WordPressImg { id: number; date: Date; date_gmt: Date; guid: Guid; modified: Date; modified_gmt: Date; slug: string; status: string; type: string; link: string; title: Title; author: number; comment_status: string; ping_status: string; template: string; meta: Meta; description: Description; caption: Caption; alt_text: string; media_type: string; mime_type: string; media_details: MediaDetails; post: number; source_url: string; _links: Links; }
Clase principal
una vez teniendo el services tenemos que unificar wordpress y angular, dejare el codigo de esta sección.
html
<section> <div class="content"> <img class="loading" src="../../assets/img/loading.gif" *ngIf="infoWordpress == null" /> <div class="postcard" *ngFor="let info of infoWordpress; let i = index;"> <a href="{{info.link}}" target="_blank"> <img src="{{mapImg.get(info.featured_media)}}" /> <div class="post-info"> <h2 class="post-info_title">{{info.title.rendered | quitarSlash}}</h2> <div class="info"> <span class="post-info_date"> {{info.date | date}} </span> <div class="post-info_describe"> {{info.excerpt.rendered | quitarTag}} </div> </div> </div> </a> </div> </div> <div class="masurl"> <a href="{{yoUrl}}" target="_blank"> <p><b>Más contenidos publicados en unprogramador.com</b></p> </a> </div> </section>
TS
import { Component, OnInit } from '@angular/core'; import {ContactForm} from '../models/contactForm'; import {WordpressService} from '../services/wordpress.service'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import {Wordpress} from '../models/wordpress'; import {HttpClient, HttpHeaders} from '@angular/common/http'; import {environment} from '../../environments/environment'; @Component({ selector: 'app-publish', templateUrl: './publish.component.html', styleUrls: ['./publish.component.css'] }) export class PublishComponent implements OnInit { infoWordpress:Array<Wordpress>; imgs: Array<string> ; yoUrl:string = environment.yoUp; mapImg = new Map<number,string>(); constructor(private wServices:WordpressService) { } ngOnInit() { this.getPost(); } getPost(){ this.wServices.getPostWordpress().subscribe( result => { this.infoWordpress = result; this.getMedia(result); }, err=>{ }); } getMedia(info:any){ this.wServices.getListMedia(info).subscribe(dat => this.mapImg = dat); } }
css
.content { display: grid; grid-gap: 5%; grid-template-columns: repeat(2, 40%); height: auto; width: 100%; align-content: space-between; justify-content: space-around; } a { text-decoration: none; color: black; } .loading { position: absolute; top: 45%; left: 45%; } .postcard { margin-top: 2vh; position: relative; width: 100%; min-height: 200px; border: 1px solid rgba(0, 0, 0, 0.2); animation-name: iz; animation-duration: 800ms; animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); } .postcard:nth-child(2n) { animation-name: dr; } @keyframes iz { from { margin-left: -50vh; } to { margin-left: 0; } } @keyframes dr { from { margin-left: 150vh; } to { margin-left: 0; } } .postcard img { width: 100%; height: auto; } .info { display: none; opacity: 0; transition-delay: 200ms; transition-duration: 100ms; transition-timing-function: linear; } .post-info { width: 35%; height: 100%; position: absolute; right: 0; top: 0; background-color: #f3f3f3; transition-duration: 400ms; transition-timing-function: linear; } @keyframes minimiza { from { width: 100%; } to { width: 30%; } } .postcard:hover .post-info { width: 100%; } .postcard:hover .info { opacity: 1; display: block; transition-delay: 0; animation-name: puff; animation-duration: 700ms; animation-timing-function: linear; } @keyframes puff { 0% { display: none; opacity: 0; } 70% { display: block; opacity: 0; } 100% { display: block; opacity: 1; } } .postcard:hover .post-info_title { margin-top: 5%; } .post-info_title { width: 80%; margin-left: auto; margin-right: auto; font-size: 120%; margin-top: 40%; } .post-info_date { color: rgba(0, 0, 0, 0.4); font-size: x-small; } .post-info_describe { position: absolute; bottom: 5%; text-align: justify; width: 80%; left: 10%; } .masurl { display: block; width: 70%; margin-left: auto; margin-right: auto; padding: 12px 20px; margin-top: 50px; margin-bottom: 50px; text-align: center; border: 1px solid rgba(0, 0, 0, 0.2); transition-duration: 500ms; transition-timing-function: linear; } .masurl:hover { border: 1px solid rgba(0, 0, 0, 0.8); background-color: #f3f3f3; -webkit-box-shadow: -11px 11px 23px -2px rgba(143, 140, 143, 0.37); -moz-box-shadow: -11px 11px 23px -2px rgba(143, 140, 143, 0.37); box-shadow: -11px 11px 23px -2px rgba(143, 140, 143, 0.37); } @media(max-width: 1024px) { .content { grid-gap: 5%; grid-template-columns: 45%; } .post-info { width: 50%; } .postcard { min-height: 250px; } .post-info_describe { bottom: 2%; width: 90%; left: 5%; } .post-info_title { margin-top: 20%; } } @media(max-width: 800px) { .content { grid-gap: 5%; grid-template-columns: 90%; } .post-info { width: 50%; } .postcard { min-height: 300px; } .post-info_describe { bottom: 2%; width: 90%; left: 5%; } .post-info_title { margin-top: 20%; } }
Conclusión de api rest wordpress y angular
Con esta actualización de wordpress tenemos la posibilidad de olvidarnos un poco del back end y dejarlo para wordpress, su facilidad y flexibilidad nos ayuda para que nos centramos en la parte del front pero sin entrar con php.
No es la mejor opción, ya que puede ser un poco lenta la respuesta, pero es buena solución si queremos extender algún proyecto que esté en wordpress.
El ejemplo está en esta dirección
Este es el repositorio del sitio completo.