Ok, to anyone currently following this thread, or to anyone in the future that faces the same need, here’s the initial solution I implemented on my end:
It uses puppeteer to mimic an actual person performing those steps, and it uses your session cookie to authenticate to webflow (Think of it as a self-inflicted session hijacking based on the “wfsession” cookie that gets added to your browser once you login to Webflow)
The code below has some TODOs for myself, but please don’t mind those, since everything major that is related to this forum topic is there. You can then simply adjust the code below to match your needs.
import { runWith } from "firebase-functions";
import * as yup from "yup";
import puppeteer from "puppeteer";
import WebflowAPI from "@services/Webflow";
import PayloadValidator from "@services/validators/Payload";
interface IApiReq {
siteName: string;
siteSlug: string;
folder: string;
usesFirebase: boolean;
multiDirectionalFeedbackEnabled: boolean;
wantsEmailOnSignIn: boolean;
shouldAlwaysShowLikesCounter: boolean;
siteLanguage: string;
wfSessionVal: string;
}
const cssSelectors = {
dashboardNewSiteBtn: '[data-automation-id="new-site-button"]',
dashboardSelectTeamTemplateBtn:
'.templates-category [data-automation-id="select-blank-template-link"]',
dashboardSiteNameInput: '[data-automation-id="site-name-input"]',
dashboardCreateSiteBtn: '[data-automation-id="create-site-button"]',
designerLogoBtn: '[data-automation-id="DesignerLogo-button"]',
designerLogoSettingsBtn:
'[data-automation-id="DesignerLogo-settings-button"]',
settingsFavicon: "img.favicon", // Only element in settings that holds the site id
settingsSaveBtn: "[data-automation-id='dash-site-save']",
settingsSlugInput: "[data-automation-id='dash-site-short-name']",
settingsFolderSelect: "[ng-model='site.dashboardFolder']",
settingsCodeLinkItem: "[data-automation-id='CustomCode-menu-item']",
settingsCodeHeadEditor: "form > div:nth-child(4)",
settingsCodeSave: "form > div:nth-child(3) > button",
};
export default runWith({ timeoutSeconds: 540, memory: "4GB" }).https.onRequest(
async (req, res) => {
/**
* ! START - Payload Validation
*/
await PayloadValidator.validate(
yup.object().strict().shape({
wfSessionVal: yup.string().required(),
siteName: yup.string().required(),
siteSlug: yup.string().required(),
folder: yup.string().required(),
usesFirebase: yup.boolean().required(),
multiDirectionalFeedbackEnabled: yup.boolean().required(),
wantsEmailOnSignIn: yup.boolean().required(),
shouldAlwaysShowLikesCounter: yup.boolean().required(),
siteLanguage: yup.string().required(),
}),
req.body
);
const {
siteName,
siteSlug,
folder,
usesFirebase,
multiDirectionalFeedbackEnabled,
wantsEmailOnSignIn,
shouldAlwaysShowLikesCounter,
siteLanguage,
wfSessionVal,
} = req.body as IApiReq;
// TODO: Setup site in Google Analytics
const {
dashboardNewSiteBtn,
dashboardCreateSiteBtn,
dashboardSelectTeamTemplateBtn,
dashboardSiteNameInput,
designerLogoBtn,
designerLogoSettingsBtn,
settingsSlugInput,
settingsFavicon,
settingsSaveBtn,
settingsFolderSelect,
settingsCodeLinkItem,
settingsCodeHeadEditor,
settingsCodeSave,
} = cssSelectors;
/**
* ! START - Webflow Authentication
*/
const browser = await puppeteer.launch({
// headless: false,
defaultViewport: { height: 1080, width: 1400 },
});
const page = await browser.newPage();
// Authentication
await page.setCookie({
name: "wfsession",
value: wfSessionVal,
domain: "webflow.com",
path: "/",
expires: Date.now() + 1 * 24 * 60 * 60 * 1000, // 1 day
httpOnly: true,
secure: true,
priority: "Medium",
sameSite: "None",
});
await page.goto("https://webflow.com/dashboard?org=acme-corp");
await page.waitForNetworkIdle();
/**
* ! START - Site Setup
*/
await page.waitForSelector(dashboardNewSiteBtn);
await page.click(dashboardNewSiteBtn);
await page.waitForSelector(dashboardSelectTeamTemplateBtn);
await page.click(dashboardSelectTeamTemplateBtn);
await page.type(dashboardSiteNameInput, siteName);
await page.click(dashboardCreateSiteBtn);
// Wait for webflow to finish setup of project
await page.waitForSelector(designerLogoBtn, {
timeout: 60 * 1000,
});
await page.waitForTimeout(10 * 1000); // wait for webflow overlay to be gone so we can click on settings
// Go to settings
await page.click(designerLogoBtn);
await page.waitForSelector(designerLogoSettingsBtn);
await page.click(designerLogoSettingsBtn);
// Get Site ID
await page.waitForNetworkIdle();
await page.waitForSelector(settingsFavicon);
const siteId = await page.evaluate(
`$("${settingsFavicon}").attr("src").split("/")[3]`
);
// Set site slug
await page.waitForSelector(settingsSlugInput);
await page.evaluate(
`$("${settingsSlugInput}").val("${siteSlug}").trigger("change")`
);
// Save settings
await page.click(settingsSaveBtn);
await page.waitForNetworkIdle();
// Move site to appropriate folder
await page.evaluate(
`$("${settingsFolderSelect} [label='${folder}']")[0].selected=true;`
);
await page.evaluate(`$("${settingsFolderSelect}").trigger("change");`);
// Save settings
await page.click(settingsSaveBtn);
await page.waitForNetworkIdle();
// Setup site configuration on custom code
await page.click(settingsCodeLinkItem);
await page.waitForNetworkIdle();
// Get data inside Angular Component Scope
await page.evaluate(
`scope = angular.element($("${settingsCodeHeadEditor}")[0]).scope();`
);
// Update data in Angular Component Scope
await page.evaluate(
`scope.meta.head = scope.meta.head.replace(/usesFirebase =(.*);/,'usesFirebase = ${usesFirebase};')`
);
await page.evaluate(
`scope.meta.head = scope.meta.head.replace(/multiDirectionalFeedbackEnabled =(.*) &&/,'multiDirectionalFeedbackEnabled = ${multiDirectionalFeedbackEnabled} &&')`
);
await page.evaluate(
`scope.meta.head = scope.meta.head.replace(/wantsEmailOnSignIn = (true|false)/,'wantsEmailOnSignIn = ${wantsEmailOnSignIn}')`
);
await page.evaluate(
`scope.meta.head = scope.meta.head.replace(/shouldAlwaysShowLikesCounter =(.*) &&/,'shouldAlwaysShowLikesCounter = ${shouldAlwaysShowLikesCounter} &&')`
);
await page.evaluate(
`scope.meta.head = scope.meta.head.replace(/window.siteLanguage = "(.+)";/,'window.siteLanguage = "${siteLanguage}";')`
);
// await page.evaluate(
// `scope.meta.head = scope.meta.head.replace(/ga: "(.+)"/,'ga: "${gaId}"')`
// ); // TODO: Implement
// await page.evaluate(
// `scope.meta.head = scope.meta.head.replace(/gtm: "(.+)"/,'gtm: "${gtmId}"')`
// ); // TODO: Implement
// Save settings
await page.click(settingsCodeSave);
await page.waitForNetworkIdle();
// Close browser
await browser.close();
// Delete all items of CMS
await WebflowAPI.deleteAllItemsOfSite(siteId);
// TODO: Create site in Firebase
// TODO: Publish Website
// TODO: Update Firebase allowed domains
// TODO: Setup site in Google Analytics
res.status(200).json({ message: `Site setup. ID: ${siteId}` });
}
);