diff --git a/scripts/ghost/migrations/init/pages.json b/scripts/ghost/migrations/init/pages.json new file mode 100644 index 0000000000000000000000000000000000000000..08e28b9cdbfa93af79287f7302eab207b85cfa8d --- /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 4ecc42cb5c8af6ff8afbcb25e2caebd34150efc0..8c0ba5feba09a5c3f1828d9dafcfaadef3fe91a0 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 96c38318fa9f1359ce29a6764d71da6acfafc27c..148e1d602021bffb847d7cb6c6d253f4fbfcee1a 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 0000000000000000000000000000000000000000..da887e40ae3dc5e2a41bc4e5c27f07c18616354d --- /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 0000000000000000000000000000000000000000..4da25e34a96a5e3cf330098c5f5f33d436208864 --- /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 0000000000000000000000000000000000000000..c23ce7138213ae0fa5df86d3b2c73cffd93175cd --- /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 0000000000000000000000000000000000000000..cdcdac58c241706189fc29b7842ca110d320dbac --- /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 b1dd31ce24db8bd3b0bcb6eacf0d00539d9e4d67..f76155787a74299c5f1cbe16b7bd1dbf8bccf645 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 0000000000000000000000000000000000000000..58791720252aaf47ea4b4206f3057320d5d894b4 --- /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; +}