From e4096e89266fb1c206089f739c0d2bcb15ad69c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marl=C3=A8ne=20SIMONDANT?= <msimondant@grandlyon.com> Date: Wed, 2 Feb 2022 14:16:42 +0000 Subject: [PATCH] feat(pages): display pages from ghost (eg. : about page and accessibility page) --- scripts/ghost/migrations/init/pages.json | 61 ++++++++++++++++ scripts/init-ghost.js | 86 ++++++++++++++++++---- src/app.module.ts | 2 + src/pages/pages.controller.spec.ts | 91 ++++++++++++++++++++++++ src/pages/pages.controller.ts | 31 ++++++++ src/pages/pages.module.ts | 8 +++ src/pages/schemas/page.schema.ts | 39 ++++++++++ src/posts/posts.service.ts | 11 +-- src/shared/utils.ts | 17 +++++ 9 files changed, 322 insertions(+), 24 deletions(-) create mode 100644 scripts/ghost/migrations/init/pages.json create mode 100644 src/pages/pages.controller.spec.ts create mode 100644 src/pages/pages.controller.ts create mode 100644 src/pages/pages.module.ts create mode 100644 src/pages/schemas/page.schema.ts create mode 100644 src/shared/utils.ts diff --git a/scripts/ghost/migrations/init/pages.json b/scripts/ghost/migrations/init/pages.json new file mode 100644 index 000000000..08e28b9cd --- /dev/null +++ b/scripts/ghost/migrations/init/pages.json @@ -0,0 +1,61 @@ +[ + { + "slug": "qui-sommes-nous", + "id": "61efc44b26db9e00015c4f32", + "title": "Qui sommes-nous ?", + "html": "<p>La numérisation accélérée des différents services privés et publics ainsi que la crise sanitaire que nous traversons a renforcé une fracture numérique déjà forte pour un nombre important de citoyens.</p><p>Au printemps 2019, la Métropole de Lyon s'est saisie des enjeux autour de l'inclusion numérique en initiant la structuration d'un réseau des acteurs de la médiation numérique sur son territoire. Son objectif est de mettre en relation les acteurs qui œuvrent au quotidien pour limiter cette fracture numérique, nombreux sur le territoire de la Métropole : associations, centres sociaux, structures informations jeunesses, grands opérateurs de services publics, collectivités...</p><p>Des ateliers de travail ont été organisés en 2019 pour identifier les besoins de ces acteurs et 9 offres de services ont été identifiées :</p><h3 id=\"recenser-et-partager-des-ressources-existantes-optimisation\">Recenser et partager des ressources existantes (optimisation)</h3><figure class=\"kg-card kg-image-card\"><img src=\"http://localhost:4200/assets/img/about_illustration_1.jpg\" class=\"kg-image\" alt=\"illustration des besoins\" loading=\"lazy\"></figure><h3 id=\"co-construire-de-nouvelles-ressources-d%C3%A9veloppement\">Co-construire de nouvelles ressources (développement)</h3><figure class=\"kg-card kg-image-card\"><img src=\"http://localhost:4200/assets/img/about_illustration_2.jpg\" class=\"kg-image\" alt=\"illustration des besoins\" loading=\"lazy\"></figure><p>Cet espace vise à centraliser et mettre en commun les ressources développées dans le cadre du réseau par ses acteurs.</p><p>N'hésitez pas à contribuer à cet espace en partageant vos ressources</p><!--kg-card-begin: markdown--><div style=\"display:flex; margin:0;padding:5px;align-items:center;\"\n</div><!--kg-card-end: markdown--><figure class=\"kg-card kg-image-card\"><img src=\"http://localhost:2368/content/images/2022/01/logo_europe.png\" class=\"kg-image\" alt loading=\"lazy\"></figure><figure class=\"kg-card kg-image-card\"><img src=\"http://localhost:2368/content/images/2022/01/logo_region.png\" class=\"kg-image\" alt=\"logo de l'union européenne\" loading=\"lazy\" width=\"220\" height=\"98\"></figure>", + "feature_image": null, + "featured": false, + "visibility": "public", + "created_at": "2022-01-25T09:35:07.000+00:00", + "updated_at": "2022-01-25T09:35:07.000+00:00", + "published_at": "2022-01-25T09:35:07.000+00:00", + "status": "published", + "custom_excerpt": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "custom_template": null, + "canonical_url": null, + "url": "http://localhost:2368/qui-sommes-nous/", + "excerpt": "La numérisation accélérée des différents services privés et publics ainsi que la\ncrise sanitaire que nous traversons a renforcé une fracture numérique déjà forte\npour un nombre important de citoyens.\n\nAu printemps 2019, la Métropole de Lyon s'est saisie des enjeux autour de\nl'inclusion numérique en initiant la structuration d'un réseau des acteurs de la\nmédiation numérique sur son territoire. Son objectif est de mettre en relation\nles acteurs qui œuvrent au quotidien pour limiter cette fracture ", + "page": true, + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null, + "meta_title": null, + "meta_description": null, + "frontmatter": null + }, + { + "slug": "accessibilite", + "id": "61effea226db9e00015c4f41", + "title": "Accessibilité", + "html": "<p>Page accessibilité. Contenu en cours de rédaction. </p>", + "feature_image": null, + "featured": false, + "visibility": "public", + "created_at": "2022-01-25T13:44:02.000+00:00", + "updated_at": "2022-01-25T13:44:02.000+00:00", + "published_at": "2022-01-25T13:44:02.000+00:00", + "status": "published", + "custom_excerpt": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "custom_template": null, + "canonical_url": null, + "url": "http://localhost:2368/accessibilite/", + "excerpt": "Page accessibilité. Contenu a venir.", + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null, + "meta_title": null, + "meta_description": null, + "frontmatter": null + } +] diff --git a/scripts/init-ghost.js b/scripts/init-ghost.js index 4ecc42cb5..8c0ba5feb 100644 --- a/scripts/init-ghost.js +++ b/scripts/init-ghost.js @@ -5,6 +5,8 @@ const tagsData = require('./ghost/migrations/init/tags.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires const postsData = require('./ghost/migrations/init/posts.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires +const pagesData = require('./ghost/migrations/init/pages.json'); +// eslint-disable-next-line @typescript-eslint/no-var-requires const path = require('path'); // eslint-disable-next-line @typescript-eslint/no-var-requires const GhostAdminAPI = require('@tryghost/admin-api'); @@ -21,7 +23,7 @@ var api = new GhostAdminAPI({ async function deleteTags(existingTags) { return await Promise.all( _.forEach(existingTags, async (tag) => { - api.tags + await api.tags .delete(_.pick(tag, ['id'])) .then((res) => { return null; @@ -33,7 +35,7 @@ async function deleteTags(existingTags) { async function deletePosts(existingPosts) { return await Promise.all( _.forEach(existingPosts, async (tag) => { - api.posts + await api.posts .delete(_.pick(tag, ['id'])) .then((res) => { return null; @@ -42,6 +44,18 @@ async function deletePosts(existingPosts) { }) ); } +async function deletePages(existingPages) { + return await Promise.all( + _.forEach(existingPages, async (id) => { + await api.pages + .delete(_.pick(id, ['id'])) + .then((res) => { + return null; + }) + .catch((error) => console.error(error)); + }) + ); +} async function createTags(deleteOnly) { // Get existing tags @@ -100,22 +114,22 @@ function processImagesInHTML(html) { }); } -async function uploadPostImage(imagePath) -{ +async function uploadPostImage(imagePath) { let imagePromise = api.images.upload({ - ref: imagePath, - file: path.resolve(imagePath) - }); + ref: imagePath, + file: path.resolve(imagePath), + }); - return Promise.resolve(imagePromise).then((url) => { + return Promise.resolve(imagePromise) + .then((url) => { return url.url; - }).catch((error) => { - console.error(error); - return null }) + .catch((error) => { + console.error(error); + return null; + }); } - async function createPosts(deleteOnly) { api.posts .browse({ limit: 'all' }) @@ -152,10 +166,52 @@ async function createPosts(deleteOnly) { .catch((error) => console.error(error)); } +async function createPages(deleteOnly) { + api.pages + .browse({ limit: 'all' }) + .then(async (existingPages) => { + // remove 'meta' key + delete existingPages['meta']; + if (existingPages.length > 0) { + console.log(`-- Dropping ${existingPages.length} pages... --`); + // Delete existing pages + await deletePages(existingPages).then(() => { + console.log('-- Pages dropped --'); + }); + } else { + console.log('-- No pages to drop --'); + } + // wait complete delete of pages, if not page slugs are appended by _2 + await new Promise((r) => setTimeout(r, 1000)); + + // Creating new pages + if (!deleteOnly) { + console.log(`-- Creating ${pagesData.length} pages --`); + _.forEach(pagesData, async (page) => { + //upload de l'image en featured_image + if (page.feature_image) { + page.feature_image = await uploadPostImage(page.feature_image); + } + api.pages + .add(page, { source: 'html' }) + .then((res) => { + console.log(`-- Page \`${res.title}\` created --`); + }) + .catch((error) => console.error(error)); + }); + } + }) + .catch((error) => console.error(error)); +} + async function main(deleteOnly) { - createTags(deleteOnly).then(() => { - createPosts(deleteOnly); - }); + createTags(deleteOnly) + .then(() => { + createPosts(deleteOnly); + }) + .then(() => { + createPages(deleteOnly); + }); } var myArgs = process.argv.slice(2); diff --git a/src/app.module.ts b/src/app.module.ts index 96c38318f..148e1d602 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { MailerModule } from './mailer/mailer.module'; import { TclModule } from './tcl/tcl.module'; import { AdminModule } from './admin/admin.module'; import { PostsModule } from './posts/posts.module'; +import { PagesModule } from './pages/pages.module'; import { TempUserModule } from './temp-user/temp-user.module'; import { NewsletterModule } from './newsletter/newsletter.module'; import { ContactModule } from './contact/contact.module'; @@ -29,6 +30,7 @@ import { ContactModule } from './contact/contact.module'; TclModule, AdminModule, PostsModule, + PagesModule, TempUserModule, NewsletterModule, ContactModule, diff --git a/src/pages/pages.controller.spec.ts b/src/pages/pages.controller.spec.ts new file mode 100644 index 000000000..da887e40a --- /dev/null +++ b/src/pages/pages.controller.spec.ts @@ -0,0 +1,91 @@ +import { HttpModule, HttpService } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { of } from 'rxjs'; +import { ConfigurationModule } from '../configuration/configuration.module'; +import { PagesController } from './pages.controller'; +import { AxiosResponse } from 'axios'; + +describe('PagesController', () => { + let controller: PagesController; + + const httpServiceMock = { + get: jest.fn(), + }; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigurationModule, HttpModule], + providers: [ + { + provide: HttpService, + useValue: httpServiceMock, + }, + ], + controllers: [PagesController], + }).compile(); + + controller = module.get<PagesController>(PagesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getPagebySlug', () => { + it('should get page Hello by Slug hello', async () => { + const data = [ + { + id: '61c4847b0ff4550001505090', + uuid: 'f4ee5a37-a343-4cad-8a32-3f6cf87f9569', + title: 'Hello', + slug: 'hello', + html: '<p>Test</p>', + comment_id: '61c4847b0ff4550001505090', + feature_image: 'http://localhost:2368/content/images/2021/12/dacc-4.png', + featured: false, + visibility: 'public', + created_at: '2022-01-25T09:35:07.000+00:00', + updated_at: '2022-01-26T15:24:24.000+00:00', + published_at: '2022-01-25T09:35:16.000+00:00', + custom_excerpt: null, + codeinjection_head: null, + codeinjection_foot: null, + custom_template: null, + canonical_url: null, + url: 'http://localhost:2368/hello/', + excerpt: + '« Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.\n' + + 'Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,\n' + + 'dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper\n' + + 'congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est\n' + + 'eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu\n' + + 'massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut\n' + + 'in risus volutpat libero pharetra tem', + reading_time: 1, + page: true, + access: true, + og_image: null, + og_title: null, + og_description: null, + twitter_image: null, + twitter_title: null, + twitter_description: null, + meta_title: null, + meta_description: null, + frontmatter: null, + }, + ]; + const axiosResult: AxiosResponse = { + data: { + pages: data, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + httpServiceMock.get.mockImplementationOnce(() => of(axiosResult)); + const result = await (await controller.getPagebySlug('hello')).toPromise(); + expect(result.pages).toStrictEqual(data); + }); + }); +}); diff --git a/src/pages/pages.controller.ts b/src/pages/pages.controller.ts new file mode 100644 index 000000000..4da25e34a --- /dev/null +++ b/src/pages/pages.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Get, HttpService, HttpException, HttpStatus, Logger, Param } from '@nestjs/common'; +import { ConfigurationService } from '../configuration/configuration.service'; +import { Observable } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { Page } from './schemas/page.schema'; +import { rewriteGhostImgUrl } from '../shared/utils'; + +@Controller('pages') +export class PagesController { + private readonly logger = new Logger(PagesController.name); + constructor(private readonly httpService: HttpService, private readonly configService: ConfigurationService) {} + + @Get(':slug') + public async getPagebySlug(@Param('slug') slug: string): Promise<Observable<{ pages: Page[] }>> { + return this.httpService + .get(`${process.env.GHOST_HOST_AND_PORT}/ghost/api/v3/content/pages/slug/` + slug, { + params: { + key: process.env.GHOST_CONTENT_API_KEY, + }, + }) + .pipe( + map((response) => { + return { pages: [rewriteGhostImgUrl(this.configService, response.data.pages[0])] }; + }), + catchError((err) => { + this.logger.error(err); + throw new HttpException('Page not found:' + err, HttpStatus.NOT_FOUND); + }) + ); + } +} diff --git a/src/pages/pages.module.ts b/src/pages/pages.module.ts new file mode 100644 index 000000000..c23ce7138 --- /dev/null +++ b/src/pages/pages.module.ts @@ -0,0 +1,8 @@ +import { HttpModule, Module } from '@nestjs/common'; +import { PagesController } from './pages.controller'; + +@Module({ + imports: [HttpModule], + controllers: [PagesController], +}) +export class PagesModule {} diff --git a/src/pages/schemas/page.schema.ts b/src/pages/schemas/page.schema.ts new file mode 100644 index 000000000..cdcdac58c --- /dev/null +++ b/src/pages/schemas/page.schema.ts @@ -0,0 +1,39 @@ +export class Page { + id: string; + uuid: string; + title: string; + slug: string; + html: string; + comment_id: string; + feature_image: string; + featured: false; + visibility: string; + email_recipient_filter: string; + created_at: string; + updated_at: string; + published_at: string; + url: string; + excerpt: string; + custom_excerpt: string; + reading_time: string; + access: boolean; + send_email_when_published: boolean; + og_image: string; + og_title: string; + og_description: string; + twitter_image: string; + twitter_title: string; + twitter_description: string; + meta_title: string; + meta_description: string; + email_subject: string; + codeinjection_head: string; + codeinjection_foot: string; + custom_template: string; + canonical_url: string; + tags: []; + authors: []; + primary_author: []; + primary_tag: []; + frontmatter: []; +} diff --git a/src/posts/posts.service.ts b/src/posts/posts.service.ts index b1dd31ce2..f76155787 100644 --- a/src/posts/posts.service.ts +++ b/src/posts/posts.service.ts @@ -5,6 +5,7 @@ import { ConfigurationService } from '../configuration/configuration.service'; import { TagEnum } from './enums/tag.enum'; import { Post } from './schemas/post.schema'; import { Tag } from './schemas/tag.schema'; +import { rewriteGhostImgUrl } from '../shared/utils'; @Injectable() export class PostsService { @@ -74,15 +75,7 @@ export class PostsService { } // Handle image display. Rewrite image URL to fit ghost infra issue. - if (!this.configService.isLocalConf()) { - if (postData.feature_image) { - postData.feature_image = `https://${this.configService.config.host}/blog/content${ - postData.feature_image.split('/content')[1] - }`; - } - const regex = /(https?:\/\/ghost):(\d*)?/g; - postData.html = postData.html.replace(regex, `https://${this.configService.config.host}/blog`); - } + postData = rewriteGhostImgUrl(this.configService, postData); return postData; } } diff --git a/src/shared/utils.ts b/src/shared/utils.ts new file mode 100644 index 000000000..587917202 --- /dev/null +++ b/src/shared/utils.ts @@ -0,0 +1,17 @@ +import { ConfigurationService } from '../configuration/configuration.service'; +import { Page } from '../pages/schemas/page.schema'; +import { Post } from '../posts/schemas/post.schema'; + +export function rewriteGhostImgUrl(configService: ConfigurationService, itemData: Page | Post): Page | Post { + // Handle image display. Rewrite image URL to fit ghost infra issue. + if (!configService.isLocalConf()) { + if (itemData.feature_image) { + itemData.feature_image = `https://${configService.config.host}/blog/content${ + itemData.feature_image.split('/content')[1] + }`; + } + const regex = /(https?:\/\/ghost):(\d*)?/g; + itemData.html = itemData.html.replace(regex, `https://${configService.config.host}/blog`); + } + return itemData; +} -- GitLab