diff --git a/scripts/sanity-to-markdown.mjs b/scripts/sanity-to-markdown.mjs new file mode 100644 index 00000000000..ff7005acd67 --- /dev/null +++ b/scripts/sanity-to-markdown.mjs @@ -0,0 +1,251 @@ +import { createClient } from '@sanity/client' +import { mkdir, writeFile } from 'node:fs/promises' +import path from 'path' +import axios from 'axios' + +const languages = ['en', 'fr', 'es', 'de', 'nl'] +const sanity = { + projectId: 'hl5bw8cj', + dataset: 'site-content', + apiVersion: '2023-06-17', +} + +/* + * content structure: + * blog: + * - title + * - slug + * - data + * - image + * - caption + * - intro + * - body + * - author + * showcase: + * - title + * - slug + * - date + * - imtro + * - body + * - image + * - caption + * - maker + */ + +/* + * Query to load makers from Strapi + */ +const loadMakers = async () => { + const result = await axios.get( + 'https://posts.freesewing.org/showcaseposts?_locale=en&_sort=date:ASC&_limit=500' + ) + const makers = {} + for (const post of result.data) { + makers[post.slug] = + post.maker?.displayname && post.maker.displayname !== 'A FreeSewing Maker' + ? post.maker.displayname + : 'unknown' + } + + return makers +} + +/* + * Query to load all content + */ +const query = `{ + "showcase": { + ${languages.map((l) => `"${l}": *[_type == 'showcase${l}'] `).join(', ')} + }, + "blog": { + ${languages.map((l) => `"${l}": *[_type == 'blog${l}'] `).join(', ')} + }, + "newsletter": *[_type == 'newsletter'], + "img": *[_type == 'contentimg'] {"id": _id, "url": image.asset->url} +}` + +/** Loads all sanity content */ +const loadSanityPosts = () => { + const sanityClient = createClient(sanity) + return sanityClient.fetch(query) +} + +/* + * Port a sanity blog post to markdown + */ +const sanityBlogPostToMd = (post, img) => `--- +author: ${JSON.stringify(post.author)} +caption: ${JSON.stringify(post.caption)} +date: ${JSON.stringify(post.date)} +image: ${JSON.stringify(img)} +intro: ${JSON.stringify(post.intro)} +title: ${JSON.stringify(post.title)} +--- + +${post.body} +` + +/* + * Helper method to create all blog posts + */ +const createBlogPosts = async (posts, images) => { + const promises = [] + const root = ['markdown', 'org', 'blog'] + // First create the folders + for (const post of posts.en) + promises.push(mkdir(path.resolve(path.join(...root, post.slug.current)), { recursive: true })) + + // Ensure folders exists before we go on + await Promise.all(promises) + + // Now create the content + for (const lang of languages) { + for (const post of posts[lang]) { + // fix the author + post.author = 'joostdecock' + + //console.log(sanityBlogPostToMd(post, images.blog[post.slug.current])) + promises.push( + writeFile( + path.resolve(path.join(...root, post.slug.current, `${lang}.md`)), + sanityBlogPostToMd(post, images.blog[post.slug.current]) + ) + ) + } + } + + return await Promise.all(promises) +} + +/* + * Port a sanity showcase post to markdown + */ +const sanityShowcasePostToMd = (post, img, maker) => `--- +maker: ${JSON.stringify(maker)} +caption: ${JSON.stringify(post.caption)} +date: ${JSON.stringify(post.date)} +image: ${JSON.stringify(img)} +intro: ${JSON.stringify(post.intro)} +title: ${JSON.stringify(post.title)} +--- + +${post.body} +` + +/* + * Helper method to create all showcase posts + */ +const createShowcasePosts = async (posts, images, makers) => { + const promises = [] + const root = ['markdown', 'org', 'showcase'] + // First create the folders + for (const post of posts.en) + promises.push(mkdir(path.resolve(path.join(...root, post.slug.current)), { recursive: true })) + + // Ensure folders exists before we go on + await Promise.all(promises) + + // Now create the content + for (const lang of languages) { + for (const post of posts[lang]) { + //console.log(sanityShowcasePostToMd(post, images.showcase[post.slug.current], makers[post.slug.current])) + promises.push( + writeFile( + path.resolve(path.join(...root, post.slug.current, `${lang}.md`)), + sanityShowcasePostToMd( + post, + images.showcase[post.slug.current], + makers[post.slug.current] + ) + ) + ) + } + } + + return await Promise.all(promises) +} + +/* + * Port a sanity newsletter post to markdown + */ +const sanityNewsletterPostToMd = (post) => { + // Need to add a date + let date = post.slug.current.slice(0, 4) + + const quarter = post.slug.current.slice(5, 6) + if (quarter === '1') date += '-01-01' + else if (quarter === '2') date += '-04-01' + else if (quarter === '3') date += '-07-01' + else if (quarter === '4') date += '-10-01' + + return `--- +date: ${JSON.stringify(date)} +edition: ${JSON.stringify(post.slug.current)} +intro: ${JSON.stringify(post.intro)} +title: ${JSON.stringify(post.title)} +--- + +${post.body} +` +} + +/* + * Helper method to create all newsletter posts + */ +const createNewsletterPosts = async (posts) => { + const promises = [] + const root = ['markdown', 'org', 'newsletter'] + // First create the folders + for (const post of posts) + promises.push(mkdir(path.resolve(path.join(...root, post.slug.current)), { recursive: true })) + + // Ensure folders exists before we go on + await Promise.all(promises) + + // Now create the content + for (const post of posts) { + //console.log(sanityNewsletterPostToMd(post)) + promises.push( + writeFile( + path.resolve(path.join(...root, post.slug.current, 'en.md')), + sanityNewsletterPostToMd(post) + ) + ) + } + + return await Promise.all(promises) +} + +const sanityToMarkdown = async () => { + console.log('Migrating content to local markdown') + console.log(' - Loading content from sanity') + const content = await loadSanityPosts() + // Construct lookup table for images + const images = { blog: {}, showcase: {} } + for (const img of content.img) { + const [type, slug] = img.id.split('--') + images[type][slug] = img.url + } + + //console.log(images) + //console.log(query) + //console.log(content.img) + + // Migrate blog posts + console.log(' - Migrating blog posts') + await createBlogPosts(content.blog, images) + + // We failed to migrate maker info, so let's load that from Strapi + console.log(' - Loading makers from strapi (missing in Sanity)') + const makers = await loadMakers() + + // Migrate showcase posts + console.log(' - Migrating showcase posts') + await createShowcasePosts(content.showcase, images, makers) + + // Migrate newsletter posts + console.log(' - Migrating newsletter posts') + await createNewsletterPosts(content.newsletter) +} + +sanityToMarkdown() diff --git a/sites/sanity/sanity.cli.js b/sites/sanity/sanity.cli.js index 9253cc45663..ba0098d296f 100644 --- a/sites/sanity/sanity.cli.js +++ b/sites/sanity/sanity.cli.js @@ -3,6 +3,6 @@ import { defineCliConfig } from 'sanity/cli' export default defineCliConfig({ api: { projectId: 'hl5bw8cj', - dataset: 'production', + dataset: 'site-content', }, }) diff --git a/sites/sanity/schemas/img.js b/sites/sanity/schemas/img.js new file mode 100644 index 00000000000..47d63a2f072 --- /dev/null +++ b/sites/sanity/schemas/img.js @@ -0,0 +1,22 @@ +export const contentimg = { + name: `contentimg`, + type: 'document', + title: `Content Image`, + fields: [ + { + name: 'recordid', + type: 'string', + title: 'Image ID', + }, + { + name: 'title', + type: 'string', + title: 'Title', + }, + { + name: 'image', + type: 'image', + title: 'Image', + }, + ], +} diff --git a/sites/sanity/schemas/index.js b/sites/sanity/schemas/index.js index 92d58a7b30b..054b67f801d 100644 --- a/sites/sanity/schemas/index.js +++ b/sites/sanity/schemas/index.js @@ -1,3 +1,4 @@ +import { contentimg } from './img.js' import { userimg } from './avatar.js' import { blogSchemaBuilder } from './blog.js' import { showcaseSchemaBuilder } from './showcase.js' @@ -6,6 +7,7 @@ import { newsletter } from './newsletter.js' const languages = ['en', 'es', 'fr', 'nl', 'de'] export const schemaTypes = [ + contentimg, userimg, newsletter, ...languages.map((lang) => blogSchemaBuilder(lang)), diff --git a/sites/sanity/scripts/export-strapi-images.mjs b/sites/sanity/scripts/export-strapi-images.mjs new file mode 100644 index 00000000000..ec2c3a6c000 --- /dev/null +++ b/sites/sanity/scripts/export-strapi-images.mjs @@ -0,0 +1,98 @@ +import fs from 'fs' +import axios from 'axios' + +const strapiHost = 'https://posts.freesewing.org' + +/* + * Helper method to create the API url for retrieval of Strapi posts + */ +const buildUrl = (type, site, lang) => { + if (type === 'blog') + return `${strapiHost}/blogposts?_locale=${lang}&_sort=date:ASC&dev_${ + site === 'dev' ? 'eq' : 'ne' + }=true` + if (type === 'showcase') + return `${strapiHost}/showcaseposts?_locale=${lang}&_sort=date:ASC&_limit=500` + if (type === 'newsletter') return `${strapiHost}/newsletters?_sort=date:ASC&_limit=500` +} + +/* + * Helper method to load posts from Strapi + * Exported because it's re-used by the Algolia indexing script + */ +export const getPosts = async (type) => { + let res + try { + res = await axios.get(buildUrl(type, 'org', 'en')) + } catch (err) { + console.log(`⚠️ Failed to load ${type} posts`) + } + const posts = {} + for (const post of res?.data || []) { + posts[post.slug] = post + } + + return posts +} + +/* + * Transforms an image from strapi to sanity + */ +const transformImage = async (p, type) => { + const id = `${type}--${p.slug}` + const post = { + _id: id, + _type: `contentimg`, + title: id, + image: { + _type: 'image', + _sanityAsset: `image@https://posts.freesewing.org${p.image.url}`, + }, + } + + return post +} + +/* + * Exports blog posts for a given language + */ +const exportBlogPostImages = async () => { + const images = [] + const strapiPosts = await getPosts('blog') + for (const post of Object.values(strapiPosts)) { + images.push(await transformImage(post, 'blog')) + } + + return images +} + +/* + * Exports showcase posts for a given language + */ +const exportShowcasePostImages = async (lang) => { + const images = [] + const strapiPosts = await getPosts('showcase') + for (const post of Object.values(strapiPosts)) { + images.push(await transformImage(post, 'showcase')) + } + + return images +} + +/* + * Main method that does what needs doing + */ +const exportStrapi = async () => { + // Say hi + console.log() + console.log(`Exporting images from Strapi`) + + console.log(' - Blog images') + const blog = await exportBlogPostImages() + console.log(' - Showcase images') + const showcase = await exportShowcasePostImages() + const all = [...blog, ...showcase] + fs.writeFileSync(`./export/contentimg.ndjson`, all.map((img) => JSON.stringify(img)).join('\n')) +} + +exportStrapi()