This commit is contained in:
Arjan Adriaanse 2021-01-25 15:25:44 +01:00
commit 3c7c5971c2
12 changed files with 597 additions and 0 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets" : [ "@babel/env" ]
}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
yarn.lock
*.log
dist/
.env

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "mobiledoc-wp-renderer"]
path = mobiledoc-wp-renderer
url = git@github.com:bij1/mobiledoc-wp-renderer.git

26
README.md Normal file
View File

@ -0,0 +1,26 @@
# wingspress
Tool for migrating Wings content to WordPress.
## `yarn fetch`
Fetches all content from Wings and saves it to `content.json`,
requires the following environment variables.
```
WINGS_ENDPOINT
WINGS_PROJECT
WINGS_APP_KEY
```
## `yarn migrate`
Converts all articles and pages that do not exist in WordPress yet and
publishes them along with their media, requires the following
environment variables.
```
WP_ENDPOINT
WP_USER
WP_PASSWORD
```

10
browser-env.js Normal file
View File

@ -0,0 +1,10 @@
require('browser-env')();
window.matchMedia = function () {
return {
matches: false,
addListener: function () {},
removeListener: function () {},
};
};

1
mobiledoc-wp-renderer Submodule

@ -0,0 +1 @@
Subproject commit d4176bfadad693a3155a021ad521891b91dde4e1

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "wingspress",
"scripts": {
"fetch": "babel-node src/fetch",
"migrate": "babel-node -r ./browser-env src/migrate"
},
"dependencies": {
"@wordpress/block-library": "^2.27.2",
"@wordpress/blocks": "^6.25.1",
"browser-env": "^3.3.0",
"got": "^11.8.1",
"mobiledoc-wp-renderer": "./mobiledoc-wp-renderer",
"simple-dom": "^1.4.0",
"wpapi": "^1.2.1"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"@babel/preset-env": "^7.12.11"
}
}

177
src/blocks.js Normal file
View File

@ -0,0 +1,177 @@
import { createBlock as createWPBlock,
registerBlockType } from '@wordpress/blocks';
import { registerCoreBlocks } from '@wordpress/block-library';
import { Document, HTMLSerializer } from 'simple-dom';
import Renderer from 'mobiledoc-wp-renderer';
import { RENDER_TYPE } from 'mobiledoc-wp-renderer';
registerCoreBlocks()
registerBlockType("acf/cta", {
title: 'CTA',
attributes: {
id: { type: 'string' },
name: { type: 'string' },
data: { type: 'object' }
}
})
registerBlockType("acf/insight", {
title: 'Insight',
attributes: {
id: { type: 'string' },
name: { type: 'string' },
data: { type: 'object' }
}
})
const createMissing = ({payload, env: {name}}) => {
console.error(`TODO block ${name}`)
console.error(JSON.stringify(payload, null, 2))
return createBlock("core/missing", {
originalName: name,
originalContent: JSON.stringify(payload)
})
}
export const cards = [
{
name: 'ImageCard',
type: RENDER_TYPE,
render: ({payload: {
src,
url,
caption,
alt
}}) => createBlock("core/image", {
url: src,
alt: alt,
caption: caption,
href: url
})
},
{
name: 'QuoteCard',
type: RENDER_TYPE,
render: ({payload: {
text,
source,
sourceUrl,
type
}}) => createBlock(type === 1 ? "core/pullquote" : "core/quote", {
value: `<p>${text}</p>`,
citation: sourceUrl ?
`<a href="${sourceUrl}">${source}</a>` : source
})
},
{
name: 'InsightCard',
type: RENDER_TYPE,
render: ({payload: {
text,
}}) => createBlock("acf/insight", {
id: 'generated',
name: 'acf/insight',
data: {
titel: text,
achtergrond: 'zwart',
textsize: 'groot'
}
})
},
{
name: 'EmbedCard',
type: RENDER_TYPE,
render: ({payload: {
src
}}) => createBlock("core/embed", {
align: "wide",
url: src
})
},
{
name: 'CTACard',
type: RENDER_TYPE,
render: ({payload: {
title,
actionText,
text,
actionUrl,
type
}}) => createBlock("acf/cta", {
id: 'generated',
name: 'acf/cta',
data: {
cta_title: title,
cta_text: text,
cta_link: {
title: actionText,
url: actionUrl
}
}
})
},
{
name: 'CollectionCard',
type: RENDER_TYPE,
render: createMissing // TODO is commonly used
},
{
name: 'CampaignCard',
type: RENDER_TYPE,
render: createMissing // TODO uses external API
},
{
name: 'NodesCard',
type: RENDER_TYPE,
render: ({payload: {
type
}}) => createBlock("core/latest-posts", {
// TODO configure
})
},
{
name: 'DataCard',
type: RENDER_TYPE,
render: createMissing // TODO? might not be used
},
{
name: 'TestimonialCard',
type: RENDER_TYPE,
render: createMissing // TODO? might not be used
},
{
name: 'ChapterCard',
type: RENDER_TYPE,
render: createMissing // TODO? might not be used
},
{
name: 'QACard',
type: RENDER_TYPE,
render: createMissing // TODO is commonly used
},
{
name: 'TextCard',
type: RENDER_TYPE,
render: ({payload: {content}, env: {dom, serializer}}) => {
var rendered = renderer.render(JSON.parse(content));
return createBlock("core/group", {}, rendered.result)
}
},
];
const createBlock = (name, attributes, inner) => {
const block = createWPBlock(name, attributes, inner);
if (block.name === "core/image")
media.push(block);
return block;
}
export const media = []
export const renderer = new Renderer({
cards: cards,
dom: new Document(),
serializer: new HTMLSerializer([]),
createBlock: createBlock
});

18
src/fetch.js Normal file
View File

@ -0,0 +1,18 @@
import got from "got";
import fs from "fs";
import query from "./query";
got.post(process.env.WINGS_ENDPOINT || 'https://api.wings.dev', {
headers: {
"Authorization": "Bearer " + process.env.WINGS_APP_KEY,
"X-Wings-Project": process.env.WINGS_PROJECT
},
json: {query}
}).json().then((data) => {
fs.writeFileSync("content.json", JSON.stringify(data, null, 2));
console.log("fetched content");
});

0
src/index.js Normal file
View File

98
src/migrate.js Normal file
View File

@ -0,0 +1,98 @@
import WPAPI from 'wpapi';
import { serialize } from '@wordpress/blocks';
import got from 'got';
import url from "url";
import path from "path";
import { renderer, media } from './blocks';
import { data } from '../content';
const wp = new WPAPI({
endpoint: process.env.WP_ENDPOINT,
username: process.env.WP_USER,
password: process.env.WP_PASSWORD
});
const migrate = async (node, resource) => {
if (!node.content) return
const [existing] = await resource().slug(node.slug);
if (existing) {
console.log(`skipping "${node.slug}"`)
return
}
const mobiledoc = JSON.parse(node.content)
const rendered = renderer.render(mobiledoc);
const mediaIds = await Promise.all(media.map(async (block) => {
try {
const { id, source_url } = await wp.media()
.file(... await download(block.attributes.href))
.create({
alt_text: block.attributes.alt || "",
caption: block.attributes.caption || ""
});
block.attributes.id = id
block.attributes.url = source_url
block.attributes.href = source_url;
return id;
}
catch (e) {
console.error(`failed to upload "${block.attributes.href}"`)
}
}));
media.length = 0;
let featured = null;
const { url, alt, caption } = node.image ||
node.featured.image || {}
try {
if (url) {
featured = await wp.media()
.file(... await download(url))
.create({
alt_text: alt || "",
caption: caption || ""
});
mediaIds.push(featured.id);
}
}
catch (e) {
console.error(`failed to upload featured ${url}`)
}
console.log(`creating "${node.slug}"`);
const { id } = await resource().create({
title: node.title,
excerpt: node.featured.description,
slug: node.slug,
featured_media: featured && featured.id,
content: serialize(rendered.result),
date_gmt: new Date(node.publishedAt),
status: 'publish'
})
await Promise.all(mediaIds.map(async (mediaId) => {
if (mediaId) wp.media().id(mediaId).update({
post: id,
date_gmt: new Date(node.publishedAt),
});
}));
}
const download = async (location) => {
console.log(`downloading "${location}"`);
const result = await got.get(location).buffer();
const { pathname } = url.parse(location)
return [result, decodeURIComponent(path.basename(pathname))];
}
data.articles.edges.reduce(async (previous, {node}) => {
await previous;
return migrate(node, () => wp.posts());
}, Promise.resolve());
data.pages.edges.reduce(async (previous, {node}) => {
await previous;
return migrate(node, () => wp.pages());
}, Promise.resolve());

234
src/query.js Normal file
View File

@ -0,0 +1,234 @@
export default `
fragment NodeFields on Node {
id
title
resourceType
slug
featured {
title
description
image {
url
}
}
locale {
id
name
primary
}
publishedAt
image {
id
name
caption
alt
key
url
}
meta {
key
value
}
data {
key
data
}
menu {
id
name
items {
text
url
items {
text
url
}
}
}
status
nodeType
platforms {
search {
title
description
}
facebook {
title
description
image {
url
}
}
twitter {
title
description
image {
url
}
}
whatsapp {
text
}
meta {
tag
attributes {
key
value
}
}
}
}
fragment CampaignFields on Campaign {
intro
description
submissionSchema
settings {
legal {
terms {
url
}
privacyPolicy {
url
}
}
}
}
{
articles: entries(selector: { typeId: { eq: "article" } }, first: 0) {
edges {
node {
...NodeFields
content
type {
id
}
}
}
}
pages: entries(selector: { typeId: { eq: "page" } }, first: 0) {
edges {
node {
...NodeFields
content
type {
id
}
}
}
}
events(first: 0) {
edges {
node {
...NodeFields
...CampaignFields
schedule {
start
end
}
location {
name
street
city
zip
country
}
fee {
amount {
amount
currency {
id
name
symbol
}
}
}
attendeeCount
}
}
}
signups(first: 0) {
edges {
node {
...NodeFields
...CampaignFields
}
}
}
petitions(first: 0) {
edges {
node {
...NodeFields
...CampaignFields
signatureCount
signatureGoal
}
}
}
fundraisers(first: 0) {
edges {
node {
...NodeFields
...CampaignFields
target {
amount
currency {
id
name
symbol
}
}
amounts {
options {
amount {
amount
currency {
id
name
symbol
}
}
}
}
raised {
amount
currency {
id
name
symbol
}
}
paymentMethods {
id
title
icons {
url
}
}
}
}
}
currentApp {
... on WebApp {
home {
node {
id
}
}
menu {
id
name
items {
text
url
items {
text
url
}
}
}
}
}
}
`