') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Staging Migration Utility by sarmstead · Pull Request #32 · rubycentral/rubycentral-theme · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions utils/migrate/.env.example
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
STAGING_API_KEY_CONTENT=
STAGING_API_KEY_ADMIN=
STAGING_API_URL=

PRODUCTION_API_KEY_CONTENT=
PRODUCTION_API_KEY_ADMIN=
PRODUCTION_API_URL=
6 changes: 6 additions & 0 deletions utils/migrate/.gitignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
node_modules/
*.DS_Store
tmp/*
!tmp/.keep
.env*
!.env.example
12 changes: 12 additions & 0 deletions utils/migrate/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Ruby Central Ghost Migrator

The purpose of this package is to copy data from a staging Ghost environment to a production Ghost environment.

## Setup

1. The migration script reads the `slugs` key from `./config/settings.json` to round up a list of posts to port over to production. Make sure that array is up to date for your needs.
1. Create a `.env` file using the `.env.example` file as a guide. You will need to follow [Ghost's integration instructions](https://ghost.org/docs/admin-api/) to retrieve your API keys.

## How to run a migration

In this directory, run the command `npm run migrate`. Then verify that the production site has the pages you expect.
24 changes: 24 additions & 0 deletions utils/migrate/config/settings.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
{
"slugs": [
"about-hero-body",
"about-hero-button",
"about-history-sectionone-body",
"about-history-sectionone-button",
"about-history-sectionthree-body",
"about-history-sectiontwo-body",
"about-position-body",
"about-team-directors-header",
"about-team-header",
"about-team-staff-header",
"footer-socials",
"home-community",
"home-hero-body",
"home-hero-button-left",
"home-hero-button-right",
"home-opensource",
"home-opensource-button",
"home-pillars-button",
"home-pillars-conferences",
"home-support"
]
}
76 changes: 76 additions & 0 deletions utils/migrate/index.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
/* eslint-disable no-console */

import { readFileSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import 'dotenv/config';
import GhostAdminAPI from '@tryghost/admin-api';

const STAGING = 'staging';
const PRODUCTION = 'production';

const clientStaging = new GhostAdminAPI({
url: process.env.STAGING_API_URL,
key: process.env.STAGING_API_KEY_ADMIN,
version: 'v5.126.0',
});

const clientProduction = new GhostAdminAPI({
url: process.env.PRODUCTION_API_URL,
key: process.env.PRODUCTION_API_KEY_ADMIN,
version: 'v5.126.0',
});

const getPages = (target) => {
const normalizedTarget = target.toLowerCase();
const client =
normalizedTarget === STAGING ? clientStaging : clientProduction;
const dataPath = join(process.cwd(), `tmp/${normalizedTarget}.json`);
const settingsPath = join(process.cwd(), 'config/settings.json');
const settings = JSON.parse(readFileSync(settingsPath));
const targetSlugs = settings.slugs.join(',');

console.log(`Fetching data from ${normalizedTarget} ...\n\n`);

return new Promise((resolve, reject) => {
client.pages
.browse({ filter: `slug:[${targetSlugs}]`, limit: 1000 })
.then((pages) => {
const jsonString = `{ "pages": ${JSON.stringify(pages)} }`;
writeFileSync(dataPath, jsonString, { flag: 'a' });
resolve();
})
.catch((e) => reject(e));
});
};

const cleanTmpFile = (target) => {
const relativePath = `tmp/${target.toLowerCase()}.json`;

console.log(`Removing "${relativePath}"...\n\n`);
rmSync(join(process.cwd(), relativePath));
};

const moveToProduction = () => {
const stagingDataPath = join(process.cwd(), 'tmp/staging.json');
const stagingData = JSON.parse(readFileSync(stagingDataPath)).pages;

console.log(`Adding page copies from ${STAGING} to ${PRODUCTION}...\n\n`);

return new Promise((resolve) => {
stagingData.forEach((stagingPage) => {
clientProduction.pages.add({
lexical: stagingPage.lexical,
status: 'published',
title: stagingPage.title,
published_at: stagingPage.published_at,
});
resolve();
});
});
};

getPages(STAGING).then(() => {
moveToProduction().then(() => {
cleanTmpFile(STAGING);
});
});
Loading