Sharing/Syncing symbols between projects

Hello all.

I’m coming here just to quickly share with you all an exciting thing that I’ve made some progress today.: The ability to share symbols, its styles, and its configuration across projects!

The way I’ve built this first version is quite simple: You build a Symbols Library project where all your reusable symbols will reside, and then you create an infinite amount of client projects that will use those symbols.

The symbols will remain in sync to the Symbols library!! If you change the background color in the symbol’s library, the children projects will also receive that change. If you add a div block to the symbol in the library, the children projects will also see that change. Overridden fields in the ‘client’ projects are respected, so if you override a text field of the symbol, it will stay like that not mattering if the ‘master symbol’ changes.

Here’s a quick demo: (Make sure to watch it in 2x so my accent doesn’t sound so thick :sweat_smile:)

As mentioned in the video: This can ‘easily’ be turned into a browser extension, so people don’t have to deal with code when using it!

Keep in mind: This is a very rough and ‘unknown’ v0.1! I’ve just built it, but I wanted to share the progress with you all

Here’s the code:

const SYMBOLS_LIBRARY_SITE_SLUG = "symbols-library-library";
const SITES_TO_RECEIVE_SYMBOLS_FROM_LIBRARY = ["symbols-library-client-site"];

const keepUniqueInput1ItemsBasedOnId = (
  input1 = [{ _id: "" }],
  input2 = [{ _id: "" }]
) =>
  input1.filter(
    (input1Item) =>
      !input2.find((input2Item) => input2Item._id === input1Item._id)
  );

const getCSRFToken = () =>
  document.head.querySelector('meta[name="_csrf"]')?.getAttribute("content") ||
  "";

const generateWfHTTPReqHeaders = () => ({
  "x-xsrf-token": getCSRFToken(),
  "content-type": "application/json; charset=UTF-8",
});

const wfSiteLeaderships = { _initialValueForIntellisense: "" };
const getWfSiteLeadership = async (siteId = "") => {
  if (!wfSiteLeaderships[siteId]) {
    wfSiteLeaderships[siteId] = (
      await fetch(
        `https://webflow.com/api/handover/channels/${siteId}/init-leadership`,
        {
          headers: generateWfHTTPReqHeaders(),
          method: "POST",
          body: "{}",
          credentials: "include",
        }
      )
    ).json();
  }

  return wfSiteLeaderships[siteId];
};

const wfSiteDomData = { _initialValueForIntellisense: "" };
const getWfDOMData = async (siteSlug = "") => {
  if (!wfSiteDomData[siteSlug]) {
    wfSiteDomData[siteSlug] = (
      await fetch(`https://webflow.com/api/sites/${siteSlug}/dom`, {
        method: "GET",
        headers: generateWfHTTPReqHeaders(),
        body: null,
        credentials: "include",
      })
    ).json();
  }

  return wfSiteDomData[siteSlug];
};

const postWfDOMData = async (pageId = "", body = {}) =>
  (
    await fetch(`https://webflow.com/api/pages/${pageId}/dom`, {
      method: "POST",
      headers: generateWfHTTPReqHeaders(),
      body: JSON.stringify(body),
      credentials: "include",
    })
  ).json();

const postWfStylesData = async (siteSlug = "", body = {}) =>
  (
    await fetch(`https://webflow.com/api/sites/${siteSlug}/styles`, {
      method: "POST",
      headers: generateWfHTTPReqHeaders(),
      body: JSON.stringify(body),
      credentials: "include",
    })
  ).json();

// TODO: Check how this behaves with Backups or CMS Collections

const synchronizeClientWfSiteWithLibrary = async (
  request = {
    librarySiteSlug: "",
    clientSiteSlug: "",
  }
) => {
  const clientSiteData = await getWfDOMData(request.clientSiteSlug);
  const librarySiteData = await getWfDOMData(request.librarySiteSlug);

  const clientSiteSessionId = (await getWfSiteLeadership(clientSiteData.siteId))
    .leader.sessionId;

  // TODO: Keep more things in sync, such as Color Swatches; IX2 Animations...
  const clientSpecificStyleBlocks = keepUniqueInput1ItemsBasedOnId(
    clientSiteData.styles.blocks,
    librarySiteData.styles.blocks
  );
  const clientSpecificSymbols = keepUniqueInput1ItemsBasedOnId(
    clientSiteData.symbols,
    librarySiteData.symbols
  );
  const clientSpecificDomNodes = keepUniqueInput1ItemsBasedOnId(
    clientSiteData.domNodes,
    librarySiteData.domNodes
  );

  const clientHomePagePageId = clientSiteData.pages[0]._id;
  const symbolsUpdateRes = await postWfDOMData(clientHomePagePageId, {
    siteName: request.clientSiteSlug,
    snapshot: false,

    clientAppVersion: clientSiteData.version,
    sessionId: clientSiteSessionId,

    // TODO: Remove nodes/symbols in client instance that were removed
    // from library.

    symbols: [...librarySiteData.symbols, ...clientSpecificSymbols],
    nodes: [
      ...librarySiteData.domNodes,
      ...clientSpecificDomNodes,
    ],
  });

  const stylesUpdateRes = await postWfStylesData(request.clientSiteSlug, {
    clientAppVersion: clientSiteData.version,
    clientLeaderInstance: clientSiteSessionId,
    pageId: clientHomePagePageId,
    data: clientSiteData.styles.data,
    styles: [...clientSpecificStyleBlocks, ...librarySiteData.styles.blocks],
  });

  return { symbolsUpdateRes, stylesUpdateRes };
};

// @ts-ignore
await Promise.all(
  SITES_TO_RECEIVE_SYMBOLS_FROM_LIBRARY.map((clientSiteSlug) => {
    return synchronizeClientWfSiteWithLibrary({
      librarySiteSlug: SYMBOLS_LIBRARY_SITE_SLUG,
      clientSiteSlug,
    });
  })
);

5 Likes