<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ROUTE06, Inc.</title>
    <description>The latest articles on DEV Community by ROUTE06, Inc. (@route06).</description>
    <link>https://dev.to/route06</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9867%2Fc09ac11b-bf4c-44c7-b2bb-470d71c87e5d.jpg</url>
      <title>DEV Community: ROUTE06, Inc.</title>
      <link>https://dev.to/route06</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/route06"/>
    <language>en</language>
    <item>
      <title>What We Did to Gain 3,000 GitHub Stars for the Liam Repository</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Mon, 21 Apr 2025 08:30:55 +0000</pubDate>
      <link>https://dev.to/route06/what-we-did-to-gain-3000-github-stars-for-the-liam-repository-54lf</link>
      <guid>https://dev.to/route06/what-we-did-to-gain-3000-github-stars-for-the-liam-repository-54lf</guid>
      <description>&lt;p&gt;Recently, the &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;Liam repository&lt;/a&gt; exceeded 3,000 stars just three months after its release. The repository continues to gain stars, alongside increases in traffic, forks, and external contributions.&lt;/p&gt;

&lt;p&gt;These stars didn't accumulate naturally—they were the result of proactive strategies.&lt;/p&gt;

&lt;p&gt;In this article, I'll share our thinking and the strategies we implemented to achieve 3,000 GitHub stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Liam ERD?
&lt;/h2&gt;

&lt;p&gt;Liam ERD is the first product under the Liam brand, a tool that easily generates beautiful ER diagrams automatically from database schemas. It's released as open-source software with the goal of growing alongside the community.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://liambx.com/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Fimages%2Fliam_erd.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://liambx.com/" rel="noopener noreferrer" class="c-link"&gt;
            Liam ERD
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Automatically generates beautiful and easy-to-read ER diagrams from your database.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Ffavicon.ico"&gt;
          liambx.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Setting 3,000 GitHub Stars as a KPI
&lt;/h2&gt;

&lt;p&gt;We set 3,000 GitHub stars as our KPI for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We are currently developing a paid product in the database design space based on Liam ERD&lt;/li&gt;
&lt;li&gt;We wanted to significantly expand recognition for the first product under the Liam brand&lt;/li&gt;
&lt;li&gt;GitHub stars are a public metric that serves as a leading indicator of user trust&lt;sup id="fnref1"&gt;1&lt;/sup&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategies Found from Predecessors
&lt;/h2&gt;

&lt;p&gt;Having set GitHub stars as our KPI, what specific actions should we take?&lt;/p&gt;

&lt;p&gt;Throughout my career as a software engineer, I've developed OSS individually and created OSS repositories for companies, but I had never promoted a company's OSS. Also, blatant promotion isn't something I personally prefer.&lt;/p&gt;

&lt;p&gt;I consulted Google and ChatGPT, but ultimately found an article featured on the Star History website.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://www.star-history.com/blog/playbook-for-more-github-stars" rel="noopener noreferrer"&gt;The (Detailed &amp;amp; Creative) Playbook for Getting More GitHub Stars&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Writing the post I wish I found 6 months ago" was exactly the information I was looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategies Implemented to Gain 3,000 GitHub Stars
&lt;/h2&gt;

&lt;p&gt;Basically, I turned everything mentioned in the Star History article into actionable tasks and implemented the ones that were feasible for us.&lt;/p&gt;

&lt;p&gt;Here are the specific strategies we implemented, with links included:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Preparation
&lt;/h3&gt;

&lt;p&gt;Increasing traffic without proper repository preparation would be counterproductive. First, we focused on preparing the repository to welcome interested users.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting up the GitHub Repository for OSS
&lt;/h4&gt;

&lt;p&gt;We set up the repository following the guidelines in the article below. We tried to include all GitHub recommendations and security measures.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl" class="crayons-story__hidden-navigation-link"&gt;Sharing the "OSS Publishing Tutorial" Created by ROUTE06, Inc.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/route06"&gt;
            &lt;img alt="ROUTE06, Inc. logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9867%2Fc09ac11b-bf4c-44c7-b2bb-470d71c87e5d.jpg" class="crayons-logo__image"&gt;
          &lt;/a&gt;

          &lt;a href="/masutaka" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg" alt="masutaka profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/masutaka" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Takashi Masuda
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Takashi Masuda
                
              
              &lt;div id="story-author-preview-content-2177250" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/masutaka" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Takashi Masuda&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/route06" class="crayons-story__secondary fw-medium"&gt;ROUTE06, Inc.&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jan 6 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl" id="article-link-2177250"&gt;
          Sharing the "OSS Publishing Tutorial" Created by ROUTE06, Inc.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ospo"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ospo&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/github"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;github&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;h4&gt;
  
  
  Organizing the README.md
&lt;/h4&gt;

&lt;p&gt;README.md is the face of the repository.&lt;/p&gt;

&lt;p&gt;To make it intuitively understandable at first glance, we focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using attractive animated GIFs to instantly show what the ER diagrams look like&lt;/li&gt;
&lt;li&gt;Creating a clear tagline that concisely summarizes the tool's features&lt;/li&gt;
&lt;li&gt;Using colorful badges created with &lt;a href="https://shields.io/" rel="noopener noreferrer"&gt;Shields.io&lt;/a&gt; to display the tool's status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, to encourage community participation, we included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Links to contribution guidelines and roadmap&lt;/li&gt;
&lt;li&gt;Visualization of contributors using &lt;a href="https://contrib.rocks" rel="noopener noreferrer"&gt;contrib.rocks&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After release, screenshots of the README.md were frequently shared on social media, confirming its role as the repository's face.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Directly Increasing GitHub Stars to 100
&lt;/h3&gt;

&lt;p&gt;Before sharing on social media, we wanted to show that many people were already interested in the repository, so we aimed to reach 100 stars through direct strategies.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sending Kickoff Messages to Friends and Acquaintances
&lt;/h4&gt;

&lt;p&gt;I directly asked friends and acquaintances to star the repository via X (formerly Twitter) DMs and Slack.&lt;/p&gt;

&lt;h4&gt;
  
  
  Making the First Post on X After Exceeding 100 Stars
&lt;/h4&gt;

&lt;p&gt;We had planned to post after getting a few dozen stars, but the response exceeded our expectations, so we posted after surpassing 100 stars.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/liam_app/status/1881975247613923737" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1881975247613923737&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what we focused on in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prepared a video that clearly demonstrates what the tool can do&lt;/li&gt;
&lt;li&gt;Concisely introduced features in bullet points&lt;/li&gt;
&lt;li&gt;Mentioned well-known relevant accounts

&lt;ul&gt;
&lt;li&gt;React Flow quoted and reposted it&lt;sup id="fnref2"&gt;2&lt;/sup&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;As a result, we gained 41 reposts and approximately 30,000 views.&lt;/p&gt;

&lt;p&gt;💡️ Although we are a Japanese team, all our X posts are in English. This is to avoid being perceived as a product only for Japanese users, which could limit its appeal in English-speaking regions. Based on our estimates, the majority of stars seem to come from users outside Japan.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Indirect Strategies After Exceeding 100 GitHub Stars
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Creating Blog Content on the Service Website
&lt;/h4&gt;

&lt;p&gt;We created content for our company blog, including direct or indirect introductions to Liam ERD, list articles, and knowledge-sharing pieces.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://liambx.com/blog/liam-erd-introduction" rel="noopener noreferrer"&gt;Introducing Liam ERD - Liam ERD&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Distributing Blog Content to Various Media
&lt;/h4&gt;

&lt;p&gt;We cross-posted to dev.to, Hashnode, and Medium with canonical URLs set.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://dev.to/route06/generate-beautiful-and-interactive-er-diagrams-with-liam-erd-5d5n"&gt;Generate Beautiful and Interactive ER-Diagrams with Liam ERD - DEV Community&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We posted slightly modified content on Reddit and HackerNoon. As a side note, you can't post to certain subreddits on Reddit without accumulating karma first. It's better to build up karma early.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://hackernoon.com/new-open-source-tool-lets-you-auto-generate-er-diagrams-for-database-visualization" rel="noopener noreferrer"&gt;New Open-Source Tool Lets You Auto-Generate ER-Diagrams for Database Visualization | HackerNoon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also posted a translated article on Zenn for the Japanese audience. This article helped generate our first +100 stars, but it resulted in Japan accounting for over 60% of stars by country, raising concerns about our initial hypothesis regarding the potential to be overlooked in English-speaking regions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/route06/articles/liam-erd-introduction" rel="noopener noreferrer"&gt;Generate Beautiful and Interactive ER Diagrams with Liam ERD - Zenn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Promoting on Various Sites and Newsletters
&lt;/h4&gt;

&lt;p&gt;In addition to the sites mentioned in the Star History article, we researched other potential promotional sites. Except for Hacker News and Reddit, these were all new to me.&lt;/p&gt;

&lt;p&gt;As of writing, we've been featured on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hacker News

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=42789874" rel="noopener noreferrer"&gt;https://news.ycombinator.com/item?id=42789874&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Reddit - r/coolgithubprojects

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/coolgithubprojects/comments/1iza4q6/liam_erd_generates_beautiful_and_easytoread_er/" rel="noopener noreferrer"&gt;https://www.reddit.com/r/coolgithubprojects/comments/1iza4q6/liam_erd_generates_beautiful_and_easytoread_er/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;This subreddit doesn't require karma and had a good response with 7 upvotes and 1K views&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;daily.dev

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dly.to/D8CwMwsw2Jb" rel="noopener noreferrer"&gt;https://dly.to/D8CwMwsw2Jb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Unexpectedly good response with 211 upvotes, 2.5K views, 11 comments, generating our first +100 stars in the Japanese region&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Gitroom

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gitlibrary.club/databases/1" rel="noopener noreferrer"&gt;https://gitlibrary.club/databases/1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;CodeTriage

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.codetriage.com/liam-hq/liam" rel="noopener noreferrer"&gt;https://www.codetriage.com/liam-hq/liam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Up For Grabs

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://up-for-grabs.net/#/filters?names=563" rel="noopener noreferrer"&gt;https://up-for-grabs.net/#/filters?names=563&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Not many, but generates continuous traffic&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;freestuff.dev

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://freestuff.dev/alternative/liam-erd" rel="noopener noreferrer"&gt;https://freestuff.dev/alternative/liam-erd&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Open Hub

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openhub.net/p/liam" rel="noopener noreferrer"&gt;https://openhub.net/p/liam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;G2

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.g2.com/products/liam-erd" rel="noopener noreferrer"&gt;https://www.g2.com/products/liam-erd&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Dev Hunt

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devhunt.org/tool/liam-erd" rel="noopener noreferrer"&gt;https://devhunt.org/tool/liam-erd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Paid, but they &lt;a href="https://x.com/search?q=from%3Adevhunt_%20liam&amp;amp;src=typed_query&amp;amp;f=live" rel="noopener noreferrer"&gt;promoted us several times on X&lt;/a&gt; during the featured period&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;It's launched!

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://itslaunched.com/product/liam-erd" rel="noopener noreferrer"&gt;https://itslaunched.com/product/liam-erd&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;To get featured in React Flow's Showcase, we mentioned them several times on X and &lt;a href="https://liambx.com/blog/tuning-edge-animations-reactflow-optimal-performance" rel="noopener noreferrer"&gt;posted about our blog article&lt;/a&gt; &lt;a href="https://x.com/liam_app/status/1885222627817775216" rel="noopener noreferrer"&gt;on X&lt;/a&gt;. Thankfully, they featured us quickly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://reactflow.dev/showcase" rel="noopener noreferrer"&gt;https://reactflow.dev/showcase&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  [Column] How We Researched Potential Promotional Sites
&lt;/h5&gt;

&lt;p&gt;While still exploring, here's how we researched:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used &lt;a href="https://ahrefs.com/" rel="noopener noreferrer"&gt;Ahrefs&lt;/a&gt; to check backlinks of competitors and similar products, adding sites that featured those products to our list of candidates&lt;/li&gt;
&lt;li&gt;Searched directly using ChatGPT and Google&lt;/li&gt;
&lt;li&gt;Sometimes found sites by chance during daily checks of Hacker News, Reddit, etc.

&lt;ul&gt;
&lt;li&gt;For example, we found &lt;a href="https://itslaunched.com" rel="noopener noreferrer"&gt;It's launched!&lt;/a&gt; through &lt;a href="https://news.ycombinator.com/item?id=42712666" rel="noopener noreferrer"&gt;Hacker News' Show HN&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Sometimes found sites through a chain of discoveries:

&lt;ol&gt;
&lt;li&gt;The Star History article mentioned earlier was originally a cross-post from &lt;a href="https://dev.to/livecycle/the-detailed-creative-playbook-for-more-github-stars-5fo5"&gt;this dev.to article&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Read &lt;a href="https://dev.to/livecycle/"&gt;all articles by Livecycle on dev.to&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Learned about &lt;a href="https://devhunt.org/" rel="noopener noreferrer"&gt;Dev Hunt&lt;/a&gt;&lt;sup id="fnref3"&gt;3&lt;/sup&gt; and added it to our candidates&lt;/li&gt;
&lt;li&gt;Dev Hunt founder &lt;a href="https://johnrush.me/" rel="noopener noreferrer"&gt;John Rush&lt;/a&gt; is a serial entrepreneur who creates products solving challenges like ours, and builds more products with his existing ones&lt;/li&gt;
&lt;li&gt;Researched several of his products and added some to our candidates. Among them, &lt;a href="https://osssoftware.org/" rel="noopener noreferrer"&gt;https://osssoftware.org/&lt;/a&gt; was particularly interesting in its small-scale approach&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Introducing at Tech Conferences
&lt;/h4&gt;

&lt;p&gt;So far only in Japan, but team members have introduced Liam ERD at several conferences.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://findy-tools.connpass.com/event/339331/" rel="noopener noreferrer"&gt;Findy Tools 1st Anniversary Celebration&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding to GitHub awesome-lists Repositories
&lt;/h4&gt;

&lt;p&gt;We submitted pull requests to relevant 'awesome list' repositories related to Liam ERD. The following repositories merged our requests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mgramin/awesome-db-tools" rel="noopener noreferrer"&gt;https://github.com/mgramin/awesome-db-tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gramantin/awesome-rails" rel="noopener noreferrer"&gt;https://github.com/gramantin/awesome-rails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Approaching Influencers
&lt;/h4&gt;

&lt;p&gt;We directly reached out to influencers who introduce OSS projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;@tom_doerr

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/tom_doerr/status/1888812285491093781" rel="noopener noreferrer"&gt;https://x.com/tom_doerr/status/1888812285491093781&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;@GithubProjects

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/GithubProjects/status/1907363835910164878" rel="noopener noreferrer"&gt;https://x.com/GithubProjects/status/1907363835910164878&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/p/DH8GbcbvI1N/" rel="noopener noreferrer"&gt;https://www.instagram.com/p/DH8GbcbvI1N/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.threads.net/@githubprojects/post/DH8GdhGTGHm" rel="noopener noreferrer"&gt;https://www.threads.net/@githubprojects/post/DH8GdhGTGHm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Celebrating Milestones
&lt;/h4&gt;

&lt;p&gt;We posted celebratory messages on X when we reached milestone numbers of stars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;200 stars

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/liam_app/status/1882291797399101882" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1882291797399101882&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;500 stars

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/liam_app/status/1888786665231245525" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1888786665231245525&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;1,000 stars

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/liam_app/status/1896390308319080924" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1896390308319080924&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;3,000 stars

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/liam_app/status/1910494502873424353" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1910494502873424353&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Most Effective Strategies
&lt;/h2&gt;

&lt;p&gt;daily.dev and X posts by influencers were particularly effective.&lt;/p&gt;

&lt;p&gt;The most effective was &lt;a href="https://x.com/GithubProjects/status/1907363835910164878" rel="noopener noreferrer"&gt;@GithubProjects' X post in early April&lt;/a&gt;. This resulted in a #2 daily ranking on &lt;a href="https://github.com/trending" rel="noopener noreferrer"&gt;GitHub Trending&lt;/a&gt;, creating the following positive cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Featured on GitHub Trending&lt;/li&gt;
&lt;li&gt;Massive increase in Liam repository traffic and stars&lt;/li&gt;
&lt;li&gt;Continued featuring on GitHub Trending&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At this stage, we noticed an increase in spontaneous sharing without our involvement.&lt;/p&gt;

&lt;p&gt;It's fair to say that "to increase stars, aim to be featured on GitHub Trending."&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://www.star-history.com/#liam-hq/liam&amp;amp;Date" rel="noopener noreferrer"&gt;https://www.star-history.com/#liam-hq/liam&amp;amp;Date&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfuzmd679nweyzzax1x1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfuzmd679nweyzzax1x1.png" alt="Liam ERD - Star History"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I've shared the strategies we implemented to gain 3,000 GitHub stars for the Liam repository.&lt;/p&gt;

&lt;p&gt;As a software engineer, I initially hesitated about actively promoting OSS, but changed my mindset, recognizing that good products don't always spread organically.&lt;/p&gt;

&lt;p&gt;I'm truly grateful for &lt;a href="https://www.star-history.com/blog/playbook-for-more-github-stars" rel="noopener noreferrer"&gt;the Star History article mentioned earlier&lt;/a&gt;. Fortunately, I had the opportunity to directly express my gratitude to the author, Zevi Reinitz&lt;sup id="fnref4"&gt;4&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;This article is just one example, so I can't guarantee that following these exact steps will lead to success. I hope it serves as a useful reference for someone.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://runacap.com/ross-index/methodology/" rel="noopener noreferrer"&gt;ROSS Index Methodology - Runa Capital&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://x.com/xyflowdev/status/1882013279998148726" rel="noopener noreferrer"&gt;https://x.com/xyflowdev/status/1882013279998148726&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://dev.to/livecycle/under-the-hood-at-devhunt-94j"&gt;👀 Under the Hood at DevHunt 🚀 - DEV Community&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://dev.to/masutaka/comment/2m6om"&gt;https://dev.to/masutaka/comment/2m6om&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>marketing</category>
      <category>liamerd</category>
    </item>
    <item>
      <title>Visualizing Mastodon’s Data Schema with Liam ERD</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Wed, 09 Apr 2025 07:27:15 +0000</pubDate>
      <link>https://dev.to/route06/visualizing-mastodons-data-schema-with-liam-erd-9k1</link>
      <guid>https://dev.to/route06/visualizing-mastodons-data-schema-with-liam-erd-9k1</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was originally written by Takafumi Endo, CEO of ROUTE06, Inc., and published on &lt;a href="https://liambx.com/blog/liam-erd-reinventing-database-visualization" rel="noopener noreferrer"&gt;our company blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.joinmastodon.org/" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt; stands as a prime example of how a decentralized social network can thrive under open-source principles. Rather than replicating the centralized models seen on major platforms, it empowers individuals to create their own servers—known as “instances”—all while remaining interconnected. This design ensures that each community can maintain its autonomy while still sharing a broader network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://joinmastodon.org/reports/Mastodon%20Annual%20Report%202023.pdf" rel="noopener noreferrer"&gt;The Mastodon Annual Report 2023&lt;/a&gt;, the latest annual report available at the time of writing, offers concrete data to support this growth. Over the past year, registered accounts rose by 38.5% to reach 8.8 million, and monthly active users totaled 1.56 million across 9,800 servers. Donations increased by 65%, thanks in part to community funding drives and a notable $100,000 contribution from Sujitech. On the product side, Mastodon introduced improvements in user onboarding, enhanced search functionality, and new mobile app features—leading to 1.87 million combined downloads on iOS and Android in 2023. &lt;/p&gt;

&lt;p&gt;A key attraction of Mastodon is its “federated” design, which embraces a wide variety of communities. Each instance sets its own rules and fosters a distinct culture, giving users the freedom to join whichever environment resonates most with them—rather than adapting to a one-size-fits-all platform. In an era dominated by large tech companies, Mastodon’s personalized approach feels refreshingly different. Server administrators actively shape their local environment, and users themselves decide how they want to engage with others. This dynamic reflects the best qualities of the open-source mindset, where technology evolves through collaboration and direct user influence.&lt;/p&gt;

&lt;p&gt;Looking ahead, Mastodon offers valuable insights into the future of social media. Its example shows how technological innovation and collective energy can come together to form a platform that is both cutting-edge and inclusive. Instead of entrusting everything to a single tech giant, people can build and sustain their own communities—and in doing so, shape the online spaces that matter to them most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demystifying Mastodon’s Data with Liam ERD
&lt;/h2&gt;

&lt;p&gt;Our main goal with Liam ERD is to make complex &lt;a href="https://liambx.com/glossary/database" rel="noopener noreferrer"&gt;databases&lt;/a&gt; easier to understand. Relying solely on &lt;a href="https://liambx.com/glossary/sql" rel="noopener noreferrer"&gt;SQL&lt;/a&gt; files and text-based documentation can quickly become confusing, so Liam ERD automatically generates visual diagrams of tables and relationships. This means you don’t have to sift through code line by line to see how everything fits together.&lt;/p&gt;

&lt;p&gt;Using Liam ERD with Mastodon’s extensive database has been particularly rewarding. Mastodon has about a hundred tables that handle everything from user profiles to timelines and notifications. Rather than feeling overwhelmed, you can filter out irrelevant details and focus on what matters most—giving you a clearer picture of how Mastodon’s features interconnect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jkqxb2yug0oghb25xdt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jkqxb2yug0oghb25xdt.jpg" alt="A part of Mastodon’s ERD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also made Liam ERD interactive. Instead of a static diagram, you can zoom, pan, and click on elements for more information. This hands-on approach turns the schema into a dynamic learning tool, letting you explore &lt;a href="https://liambx.com/blog/postgresql-materialized-views-from-basics-to-practical-examples-in-mastodon" rel="noopener noreferrer"&gt;Mastodon’s architecture&lt;/a&gt; at your own pace.&lt;/p&gt;

&lt;p&gt;While Mastodon’s schema is large, it is built on solid engineering practices: normalization, well-chosen data types, and foreign keys that maintain relational integrity. Seeing these best practices in action—especially at the scale of millions of users—can be far more illuminating than simply reading about them in a textbook.&lt;/p&gt;

&lt;p&gt;Because Mastodon is open source, you can watch its schema evolve as new features are introduced or performance is optimized. For developers aiming to specialize in database engineering, Mastodon offers lessons in iterative design and scalability. Our goal with Liam ERD is to support that exploration, helping you dive deep into Mastodon’s data design without losing your way.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/main/db/schema.rb" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapp.liambx.com%2Fassets%2Fliam_erd.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/main/db/schema.rb" rel="noopener noreferrer" class="c-link"&gt;
            
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Generate ER diagrams effortlessly by entering a schema file URL. Ideal for visualizing, reviewing, and documenting schemas.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Ffavicon.ico"&gt;
          liambx.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How Liam ERD Empowers OSS
&lt;/h2&gt;

&lt;p&gt;Mastodon brings open-source values to life. Its code is fully public, contributions are welcomed worldwide, and the development process is transparent. This openness—together with its responsiveness to user feedback—reflects a strong culture of collaboration that aligns perfectly with Liam ERD’s mission to share knowledge and tools for everyone’s benefit.&lt;/p&gt;

&lt;p&gt;Before Liam released their ERD, we asked Mastodon about featuring it, and we were especially pleased when Mastodon agreed to feature their ERD on our site. This underscores Mastodon’s commitment to transparency and its desire to help others learn from its platform. Such a spirit of shared resources and ideas drives supportive, innovative communities.&lt;/p&gt;

&lt;p&gt;Mastodon’s user-centric decision-making also shows how &lt;a href="https://liambx.com/glossary/agility" rel="noopener noreferrer"&gt;agility&lt;/a&gt; and responsiveness can flourish when diverse voices have a say. The project tackles bugs, adds features, and evolves efficiently while remaining true to its vision—showcasing how shared ownership fosters sustained progress.&lt;/p&gt;

&lt;p&gt;I believe many open-source projects have well-designed databases that often go underappreciated due to their lack of visibility. While text-based documentation can be overwhelming for potential contributors, visualizing the structure makes it much easier for maintainers, developers, and newcomers to understand how everything fits together.&lt;/p&gt;

&lt;p&gt;We see Liam ERD as more than a diagramming tool—it’s a way for anyone, including non-developers, to understand a project’s data architecture and see where they can contribute. That approach matches the open-source principle that good ideas can come from anyone.&lt;/p&gt;

&lt;p&gt;Making database structures easy to visualize also boosts inclusivity. When relationships are clearly outlined, anyone can jump in and say, “I know how to help.” Mastodon’s steady growth—fueled by contributors of all skill levels—demonstrates the impact of this openness.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/liam-hq" rel="noopener noreferrer"&gt;
        liam-hq
      &lt;/a&gt; / &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;
        liam
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Automatically generates beautiful and easy-to-read ER diagrams from your database.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Open Source and Transparent Schemas
&lt;/h2&gt;

&lt;p&gt;Mastodon’s robust database and Liam ERD’s visual approach complement each other naturally, both driven by the belief that sharing and accessibility accelerate progress. By showcasing Mastodon’s underlying structure, we aim to lower barriers and inspire people to learn, collaborate, and innovate.&lt;/p&gt;

&lt;p&gt;Clarity and transparency in database design amplify a software project’s potential. Developers work more confidently when they can see a clear map, and open-source communities thrive when people learn from one another’s work. This blend of technical excellence and human-centered collaboration continues to propel software forward for anyone ready to join in.&lt;/p&gt;

</description>
      <category>mastodon</category>
      <category>liamerd</category>
      <category>erd</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Tuning Edge Animations in Reactflow for Optimal Performance</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Tue, 08 Apr 2025 04:41:16 +0000</pubDate>
      <link>https://dev.to/route06/tuning-edge-animations-in-reactflow-for-optimal-performance-3g32</link>
      <guid>https://dev.to/route06/tuning-edge-animations-in-reactflow-for-optimal-performance-3g32</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was originally written by Junki Saito, and published on &lt;a href="https://liambx.com/blog/tuning-edge-animations-reactflow-optimal-performance" rel="noopener noreferrer"&gt;our company blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Liam ERD relies on the incredible capabilities of React Flow to visualize complex data models—like tables and relationships—on the web. We’re genuinely grateful for how React Flow enables rapid development and a beautiful user experience right out of the box. That said, when you’re working with truly massive datasets, you need to make sure your app still runs at top speed. In this post, I’ll walk you through how we tackled performance issues related to edge animations and the steps we took to keep everything running smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Animated Edges and Performance Bottlenecks
&lt;/h2&gt;

&lt;p&gt;One of the standout features in React Flow is the effortless animation you can apply to edges. By simply toggling &lt;code&gt;animated&lt;/code&gt; to true, you get a dashed-line effect that makes relationships feel alive. We loved this feature—it instantly added a sense of dynamic flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu6pqy9u93s3013pwpk39.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu6pqy9u93s3013pwpk39.gif" alt="React Flow Demo for animated edges in React Flow with animated=true."&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * This code is written in Stackblitz
 * @see https://stackblitz.com/edit/sb1-74jfdcqt?file=src%2FApp.tsx
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialNodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Input Node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Default Node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;125&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Output Node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialEdges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e1-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e2-3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactFlow&lt;/span&gt;
      &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initialNodes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initialEdges&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The issue arose when we started rendering a huge number of edges for large ERDs, like in &lt;a href="https://docs.joinmastodon.org/" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt;, which can involve over 100 tables and countless relationships. We noticed a significant dip in responsiveness whenever we hovered over or dragged around nodes—some browsers handled it better than others, but overall, it was clear that the animations were holding back performance.&lt;/p&gt;
&lt;h2&gt;
  
  
  Identifying the Root Cause: The Culprit - stroke-dasharray
&lt;/h2&gt;

&lt;p&gt;After digging in, we discovered that React Flow’s edge animations rely on the CSS property &lt;code&gt;stroke-dasharray&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbn7rpeoh0d8dgkaiw6ok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbn7rpeoh0d8dgkaiw6ok.png" alt="CSS style for animated edges in React Flow with animated=true."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While it does create a neat dashed effect, it can seriously push your CPU when you have lots of SVG elements doing the same thing at once. Multiple user reports and bug threads confirmed that &lt;code&gt;stroke-dasharray&lt;/code&gt; can be a major slowdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://issues.chromium.org/issues/40958492" rel="noopener noreferrer"&gt;High CPU utilisation when CSS animating stroke-dashoffset - Chromium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/svgdotjs/svg.js/issues/1003" rel="noopener noreferrer"&gt;Locked fps of animation · Issue #1003 · svgdotjs/svg.js - Github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our case, with hundreds of edges animated simultaneously, &lt;code&gt;stroke-dasharray&lt;/code&gt; quickly became the main bottleneck.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Goal: Retain Animation, Regain Smoothness
&lt;/h2&gt;

&lt;p&gt;We still wanted the user experience of visually “flowing” edges. It’s a small detail that helps people see how tables and relationships link together in real time. But we absolutely had to solve the lag problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Our Solution: A Custom Animated Edge
&lt;/h2&gt;

&lt;p&gt;To tackle this, we sidestepped the default animation approach in React Flow and crafted our own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bypassing React Flow's animated props&lt;/strong&gt;: We disabled the library’s &lt;code&gt;animated&lt;/code&gt; prop on edges to stop using &lt;code&gt;stroke-dasharray&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminating &lt;code&gt;stroke-dasharray&lt;/code&gt; entirely&lt;/strong&gt;: We cut it out for both animations and any default styling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementing a Custom approach&lt;/strong&gt;: We built a &lt;code&gt;CustomEdge&lt;/code&gt; component that animates an SVG object along the edge path, as outlined in the &lt;a href="https://reactflow.dev/components/edges/animated-svg-edge" rel="noopener noreferrer"&gt;React Flow docs&lt;/a&gt;. This gave us the same visual flair—minus the performance hit.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PARTICLE_COUNT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ANIMATE_DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EdgeProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RelationshipEdgeType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RelationshipEdge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;edgePath&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBezierPath&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BaseEdge&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edgePath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* data.isHighlighted will be true if the edge should be highlighted. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isHighlighted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PARTICLE_COUNT&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ellipse&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`particle-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ANIMATE_DURATION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;
            &lt;span class="na"&gt;ry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"1.2"&lt;/span&gt;
            &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"url(#myGradient)"&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* The &amp;lt;animateMotion&amp;gt; element defines how an element moves along a motion path.  */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;animateMotion&lt;/span&gt;
              &lt;span class="na"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ANIMATE_DURATION&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;PARTICLE_COUNT&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;dur&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ANIMATE_DURATION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;repeatCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"indefinite"&lt;/span&gt;
              &lt;span class="na"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"auto"&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edgePath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;calcMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"spline"&lt;/span&gt;
              &lt;span class="na"&gt;keySplines&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0.42, 0, 0.58, 1.0"&lt;/span&gt;
            &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ellipse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;By implementing these three approaches, the appearance changed from looking like Before gif image to looking like After gif image.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can see exactly how we did it in our Pull Request:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/liam-hq/liam/pull/367" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        🚸 Add animated particles to highlighted relationship edges
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#367&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/junkisai" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F28256336%3Fv%3D4" alt="junkisai avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/junkisai" rel="noopener noreferrer"&gt;junkisai&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/liam-hq/liam/pull/367" rel="noopener noreferrer"&gt;&lt;time&gt;Dec 24, 2024&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Summary&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;Drawing multiple strokes with stroke-dasharray specified causes performance degradation, especially in Safari browser.&lt;/p&gt;
&lt;p&gt;Therefore, I stopped using stroke-dasharray.
Additionally, since stroke-dasharray is also used when 'animated' is set to true in Edge, I made sure that 'animated' is always set to false.&lt;/p&gt;
&lt;p&gt;Furthermore, since setting 'animated' to false made it difficult to visually distinguish the flow of Edges, I used animateMotion instead to express Edge animations.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/liam-hq/liam/pull/367" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  Results: Measuring the Improvement
&lt;/h2&gt;

&lt;p&gt;We ran before-and-after tests using Chrome DevTools to measure frame drops. Our test steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hover over the “Accounts” table node in Mastodon’s ERD: &lt;a href="https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/e2f085e2b2cec08dc1f5ae825730c2a3bf62e054/db/schema.rb" rel="noopener noreferrer"&gt;Mastodon’s schema.rb&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wait 3 seconds, then remove the hover.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://liambx.com/images/blogs/tuning-edge-animations-reactflow-optimal-performance/demo.gif" rel="noopener noreferrer"&gt;Demonstration of actions for comparison.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Findings&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;: We saw consistent frame drops of around 5 frames at a time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvur9tu9l86tpocajpath.png" width="100%"&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;: Frame drops went down to 2–3 frames at a time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9z24dack12hgr8j2ijlu.png" width="100%"&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Not surprisingly, our changes made the overall experience smoother and more responsive—no more choppy dragging or sluggish hover states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing React Flow for Scalable Products
&lt;/h2&gt;

&lt;p&gt;By digging into React Flow’s edge animation logic and crafting a custom approach, we found a sweet spot between visual polish and raw performance. Disabling &lt;code&gt;stroke-dasharray&lt;/code&gt; and opting for a more controlled animation model let us keep the dynamic look of animated edges while ensuring that our largest ERDs run like butter. We hope this helps anyone else wrestling with performance challenges on React Flow—you can absolutely keep those animations, as long as you give them some thoughtful tweaks.&lt;/p&gt;

&lt;p&gt;Liam ERD required a highly scalable node-based engine to efficiently render large, interconnected schemas. That drove our choice of React Flow, which effortlessly manages 100+ tables and hundreds of edges while allowing the fine-grained control we need for selective highlighting and custom animations. Since Liam ERD relies on type-safe schema transformations, React Flow’s robust TypeScript support proved indispensable. Its performance tuning capabilities, including the custom animation optimizations we described, make it ideally suited for complex database diagrams. If your product demands large-scale interactive data modeling or specialized node behavior, React Flow is an excellent foundation to build upon.&lt;/p&gt;

&lt;p&gt;For more insights into Liam ERD and what we’ve been working on, feel free to check out our introduction post:  &lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://liambx.com/blog/liam-erd-introduction" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Fimages%2Fblogs%2Fliam-erd-introduction%2Fcover.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://liambx.com/blog/liam-erd-introduction" rel="noopener noreferrer" class="c-link"&gt;
            Introducing Liam ERD - Liam ERD
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Discover Liam ERD, a modern tool for effortlessly auto-generating interactive, readable ER diagrams from database schemas. Perfect for team collaborat...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Ffavicon.ico"&gt;
          liambx.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>reactflow</category>
      <category>liamerd</category>
      <category>erd</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Liam ERD: Reinventing Database Visualization</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Tue, 01 Apr 2025 04:55:00 +0000</pubDate>
      <link>https://dev.to/route06/liam-erd-reinventing-database-visualization-kff</link>
      <guid>https://dev.to/route06/liam-erd-reinventing-database-visualization-kff</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was originally written by Takafumi Endo, CEO of ROUTE06, Inc., and published on &lt;a href="https://liambx.com/blog/liam-erd-reinventing-database-visualization" rel="noopener noreferrer"&gt;our company blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In today’s fast-evolving tech world, advancements in AI, DevOps, and visual development tools have transformed how we approach software development. Yet, one area that often gets overlooked but holds great potential for improvement is &lt;a href="https://liambx.com/glossary/data-visualization" rel="noopener noreferrer"&gt;data visualization&lt;/a&gt;. Even as new frameworks and collaboration tools emerge, quite a few teams are likely finding themselves wrestling with grasping complex data relationships - a challenge that can delay critical decisions and slow down innovation.&lt;/p&gt;

&lt;p&gt;Early on, I began to notice that well-organized data structures and effective visualization played a key role in streamlining operations. It made me wonder: As other areas of software development evolve toward more intuitive and visually guided approaches, why does &lt;a href="https://liambx.com/glossary/database" rel="noopener noreferrer"&gt;database&lt;/a&gt; design still often rely on dense, text-based definitions?&lt;/p&gt;

&lt;p&gt;With the release of &lt;strong&gt;Liam ERD&lt;/strong&gt;, we feel fortunate to be at a point where both technology and workplace culture are ready for a fresh, more accessible take on &lt;a href="https://liambx.com/glossary/erd" rel="noopener noreferrer"&gt;ER diagrams&lt;/a&gt;. And as AI becomes ever more central to our daily workflows, addressing the longstanding struggles of visualizing and collaborating on data has never felt more timely.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Database Journey
&lt;/h2&gt;

&lt;p&gt;I first encountered ER diagrams during my initial year in my career. Though I was a banker, my background in information science led me to help maintain internal tools, including Microsoft Access. At the time, Access felt somewhat daunting, but it turned out to be an incredibly effective way to learn about databases and data relationships.&lt;/p&gt;

&lt;p&gt;Using Access’s built-in visual diagrams, instead of jumping straight into &lt;a href="https://liambx.com/glossary/sql" rel="noopener noreferrer"&gt;SQL&lt;/a&gt;, made it much easier for me to understand how multiple tables connected. This insight proved invaluable for tasks like managing a complex investment portfolio system. Had I approached these data structures solely through raw SQL, I imagine the complexity would have been overwhelming.&lt;/p&gt;

&lt;p&gt;Over the years, I’ve come to see that starting with visuals rather than code is more than a matter of convenience. It genuinely helps people at any level of expertise recognize the true shape and flow of their data. And in an era when AI continues to expand its influence, having that firm grasp of data structure only becomes more critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  How ERDs Shaped Our Startup
&lt;/h2&gt;

&lt;p&gt;After transitioning into tech, I founded an e-commerce company where, as product manager, I oversaw everything from advertising analytics to logistics and payment systems—each with its own complexities and stakeholders. While our KPI dashboards in BigQuery and Redshift were essential, ERDs proved invaluable across the board—from designing internal tools and customer-facing features to planning new UI flows.&lt;/p&gt;

&lt;p&gt;Mapping out data flows—from advertising metrics to sales data, or inventory status to shipping details—proved invaluable. The ERDs served as a universal language, allowing even non-technical team members to quickly grasp the implications of system changes. More than just documentation, they became a tool that accelerated our decision-making and strengthened alignment, helping everyone understand the ripple effects of any system modifications.&lt;/p&gt;

&lt;p&gt;Over time, I began to see ERDs as a valuable tool. By clearly visualizing relationships, they helped reduce misunderstandings and ensured that all teams—from operations to marketing—were aligned in their understanding of our systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Power of Visual Database Design
&lt;/h2&gt;

&lt;p&gt;Reflecting on these experiences, I’ve found that ER diagrams can be at least as beneficial for non-developers as they are for engineers. Key advantages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dramatically Faster Team Onboarding&lt;/strong&gt;&lt;br&gt;
New hires and external collaborators don’t have to comb through unclear documentation. With ERDs, they can quickly understand how data flows through the system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accelerated Product Development&lt;/strong&gt;&lt;br&gt;
Clear visual blueprints minimize the risk of unseen database issues, allowing teams to iterate on new features with confidence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cross-Functional Alignment&lt;/strong&gt;&lt;br&gt;
Designers, product managers, and engineers can converge on a single view of the data, reducing duplicated work and bridging communication gaps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Streamlining Success&lt;/strong&gt;&lt;br&gt;
In fast-paced markets, clarity in data architecture can significantly speed up development cycles and overall responsiveness to change.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Throughout my career, I've consistently advised new product managers to thoroughly understand the ERDs of their products. It's one of the first things I recommend to anyone stepping into a product management role. Understanding your product's data structure isn't just helpful—it's fundamental to effective product leadership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Liam ERD
&lt;/h2&gt;

&lt;p&gt;Recognizing how foundational database visualization is, we’re introducing &lt;strong&gt;Liam ERD&lt;/strong&gt;—a product designed to help teams of any size work with data more effectively.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Effortless Clarity for Complex Databases&lt;/strong&gt;&lt;br&gt;
Liam ERD generates interactive diagrams automatically, empowering you to zoom in, filter, and navigate even large-scale schemas. Rather than wrestle with the complexity, you can focus on meaningful insights.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Seamless Collaboration That Just Works&lt;/strong&gt;&lt;br&gt;
Use &lt;strong&gt;custom filters&lt;/strong&gt; to direct each person’s attention to relevant tables—be it marketers monitoring campaign data or engineers planning architecture changes. &lt;strong&gt;Shareable links&lt;/strong&gt; make it easy to gather feedback or guide discussions, whether you’re having a brief huddle or an in-depth review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CI/CD-Ready Documentation&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Automated updates&lt;/strong&gt; ensure your ER diagram always reflects the latest database changes. Compatibility with &lt;a href="https://liambx.com/docs/parser/supported-formats/postgresql" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;, &lt;a href="https://liambx.com/docs/parser/supported-formats/prisma" rel="noopener noreferrer"&gt;Prisma&lt;/a&gt;, and &lt;a href="https://liambx.com/docs/parser/supported-formats/rails" rel="noopener noreferrer"&gt;Ruby on Rails&lt;/a&gt; helps you maintain consistent schema documentation as you deploy new iterations.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By focusing on the database layer first, Liam ERD aims to provide a platform where creativity and collaboration intersect, ultimately helping teams design more robust solutions.&lt;/p&gt;

&lt;p&gt;As our service is just getting started, we're excited about the road ahead. We have many feature enhancements planned and are committed to continuously refining the user experience based on your feedback. We believe that by building together with our users, we can create a tool that truly transforms how teams work with their data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4tdbajn126m8fwe71wjo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4tdbajn126m8fwe71wjo.jpg" alt="Liam ERD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual Design Matters More in the AI Era
&lt;/h2&gt;

&lt;p&gt;In the midst of rapid advances in generative AI, we often ask ourselves: Will ER diagrams still be relevant if AI can interpret data on its own? The reality is that high-quality, structured data remains fundamental, even for the most advanced AI. And it's still humans who must design these data structures and align them with business contexts. A loosely defined or inconsistent schema can undermine the best of predictive models.&lt;/p&gt;

&lt;p&gt;As we look ahead, the human role in database design involves strategic thinking—connecting the dots between various departments and ensuring data quality. Liam ERD goes beyond mere data visualization. We're building a platform that supports everything from database structure design to cross-team collaboration, aiming to unlock better data utilization.&lt;/p&gt;

&lt;p&gt;In essence, empowering database designers and merging their expertise with cutting-edge computational power represents a meaningful step forward for many organizations. Tools like Liam ERD can help teams stay focused on bigger-picture insights instead of wrestling with messy, opaque schemas, ultimately enhancing the capabilities of the people behind the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shape the Future of Database Design
&lt;/h2&gt;

&lt;p&gt;While beautiful visualization is our starting point, we're building Liam to be much more than just a diagram tool. We believe there's never been a better time to reimagine how teams work with their databases. If you've ever found yourself explaining the same table structure again and again, or if you've struggled to align your data strategy with your team's ambitions, &lt;strong&gt;Liam ERD&lt;/strong&gt; may offer a valuable solution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kick off your journey&lt;/strong&gt;&lt;br&gt;
Run &lt;code&gt;npx @liam-hq/cli init&lt;/code&gt; in your terminal or visit &lt;a href="https://liambx.com/docs" rel="noopener noreferrer"&gt;our documentation&lt;/a&gt; to dive into the details.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Engage with us&lt;/strong&gt;&lt;br&gt;
Join the conversation on &lt;a href="https://github.com/liam-hq/liam/discussions" rel="noopener noreferrer"&gt;our GitHub Discussions&lt;/a&gt; to share ideas, ask questions, and provide feedback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explore our roadmap&lt;/strong&gt;&lt;br&gt;
See what’s next on &lt;a href="https://github.com/orgs/liam-hq/projects/1" rel="noopener noreferrer"&gt;our roadmap&lt;/a&gt; and help shape the future of database design&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even as development tools become more sophisticated, there's no substitute for clarity and alignment in the way we work with data. Starting with visualization and expanding into collaboration features, we're creating a platform that will transform how teams design and evolve their databases. We hope you'll join us in reimagining how ER diagrams can empower teams of all sizes—and together, we can help shape a more collaborative, data-driven future.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://liambx.com/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Fimages%2Fliam_erd.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://liambx.com/" rel="noopener noreferrer" class="c-link"&gt;
            Liam ERD
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Automatically generates beautiful and easy-to-read ER diagrams from your database.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Ffavicon.ico"&gt;
          liambx.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/liam-hq" rel="noopener noreferrer"&gt;
        liam-hq
      &lt;/a&gt; / &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;
        liam
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Automatically generates beautiful and easy-to-read ER diagrams from your database.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>erd</category>
      <category>liamerd</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Published an OSS Repository That Continuously Stores GitHub Repository Metrics Using GAS</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Fri, 21 Mar 2025 02:17:55 +0000</pubDate>
      <link>https://dev.to/route06/published-an-oss-repository-that-continuously-stores-github-repository-metrics-using-gas-4f0g</link>
      <guid>https://dev.to/route06/published-an-oss-repository-that-continuously-stores-github-repository-metrics-using-gas-4f0g</guid>
      <description>&lt;p&gt;Hey Devs!&lt;/p&gt;

&lt;p&gt;Previously, I explored how we can monitor OSS activity metrics based on GitHub repository data.&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/route06" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9867%2Fc09ac11b-bf4c-44c7-b2bb-470d71c87e5d.jpg" alt="ROUTE06, Inc." width="800" height="800"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg" alt="" width="460" height="460"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/route06/consideration-of-oss-activity-metrics-based-on-github-repository-data-247i" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Consideration of OSS Activity Metrics Based on GitHub Repository Data&lt;/h2&gt;
      &lt;h3&gt;Takashi Masuda for ROUTE06, Inc. ・ Dec 16 '24&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#ospo&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#opensource&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#github&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#marketing&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;Building on that, I implemented a Google Apps Script (GAS) that continuously stores traffic data from GitHub repositories.&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/route06" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9867%2Fc09ac11b-bf4c-44c7-b2bb-470d71c87e5d.jpg" alt="ROUTE06, Inc." width="800" height="800"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg" alt="" width="460" height="460"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/route06/continuously-storing-traffic-data-of-multiple-github-repositories-using-gas-4fio" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Continuously Storing Traffic Data of Multiple GitHub Repositories Using GAS&lt;/h2&gt;
      &lt;h3&gt;Takashi Masuda for ROUTE06, Inc. ・ Dec 23 '24&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#github&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#googlesheets&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#googleappsscript&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;This time, while adding GAS functions to store other metrics, I have published the repository as OSS. I’d like to take this opportunity to introduce it once again.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Published Repository&lt;/li&gt;
&lt;li&gt;
Stored Metrics

&lt;ul&gt;
&lt;li&gt;
1. Stargazers

&lt;ul&gt;
&lt;li&gt;Objectives&lt;/li&gt;
&lt;li&gt;Issues&lt;/li&gt;
&lt;li&gt;Solution&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

2. Traffic views

&lt;ul&gt;
&lt;li&gt;Objectives&lt;/li&gt;
&lt;li&gt;Issues&lt;/li&gt;
&lt;li&gt;Solution&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

3. Traffic clones

&lt;ul&gt;
&lt;li&gt;Objectives&lt;/li&gt;
&lt;li&gt;Issues&lt;/li&gt;
&lt;li&gt;Solution&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

4. Top referral sources

&lt;ul&gt;
&lt;li&gt;Objectives&lt;/li&gt;
&lt;li&gt;Issues&lt;/li&gt;
&lt;li&gt;Solution&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Published Repository
&lt;/h2&gt;

&lt;p&gt;The published repository is available here.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/route06inc" rel="noopener noreferrer"&gt;
        route06inc
      &lt;/a&gt; / &lt;a href="https://github.com/route06inc/ospo-google-apps-script" rel="noopener noreferrer"&gt;
        ospo-google-apps-script
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      OSPO Google Apps Script by ROUTE06, Inc.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The initial setup is summarized in the &lt;a href="https://github.com/route06inc/ospo-google-apps-script#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt;. I will focus mainly on why I decided to store these metrics.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stored Metrics
&lt;/h2&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Stargazers
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Objectives
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;To understand the daily increase in stars.&lt;/li&gt;
&lt;li&gt;To correlate the days with star increases with possible reasons or initiatives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Issues
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;On GitHub’s web UI, only the total number of stars and the accounts that starred are visible.

&lt;ul&gt;
&lt;li&gt;Example: &lt;a href="https://github.com/giselles-ai/giselle/stargazers" rel="noopener noreferrer"&gt;https://github.com/giselles-ai/giselle/stargazers&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;With Star History, the counts for nearby days are rounded off, and it does not allow for note-taking.

&lt;ul&gt;
&lt;li&gt;Example: &lt;a href="https://www.star-history.com/#giselles-ai/giselle&amp;amp;Date" rel="noopener noreferrer"&gt;https://www.star-history.com/#giselles-ai/giselle&amp;amp;Date&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Solution
&lt;/h4&gt;

&lt;p&gt;I implemented the function &lt;a href="https://github.com/route06inc/ospo-google-apps-script/blob/d693ef5f6c8526f2c803500a5815d925bc98ee7f/lib/star_service.js#L1-L22" rel="noopener noreferrer"&gt;updateSheetWithStargazers&lt;/a&gt; to record who starred on which day.&lt;/p&gt;

&lt;p&gt;The graph was created as shown below. The key is to enable “Aggregate” on the X-axis. Except for the colored cells, you can freely add notes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foj9uxwphysuxuz19uxfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foj9uxwphysuxuz19uxfi.png" alt="Stargazers - Google Sheets" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Traffic views
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Objectives
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;To permanently store the numerical data from the &lt;code&gt;Visitors&lt;/code&gt; graph in the repository’s &lt;code&gt;Insights &amp;gt; Traffic&lt;/code&gt; section.

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://github.com/[ORG]/[REPO]/graphs/traffic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqc7d4sg6f015femfewpo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqc7d4sg6f015femfewpo.png" alt="Visitors - GitHub" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Issues
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The graph does not display data older than two weeks.&lt;/li&gt;
&lt;li&gt;The GitHub REST API &lt;a href="https://docs.github.com/rest/metrics/traffic?apiVersion=2022-11-28#get-page-views" rel="noopener noreferrer"&gt;Get traffic views&lt;/a&gt; has the same limitation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Solution
&lt;/h4&gt;

&lt;p&gt;I implemented the function &lt;a href="https://github.com/route06inc/ospo-google-apps-script/blob/d693ef5f6c8526f2c803500a5815d925bc98ee7f/lib/traffic_service.js#L1-L15" rel="noopener noreferrer"&gt;updateSheetWithLatestTrafficViews&lt;/a&gt; that uses this API to store the daily Views and Unique visitors in a spreadsheet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgg8mltkpqbuvgot8q1px.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgg8mltkpqbuvgot8q1px.png" alt="Traffic views - Google Sheets" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Traffic clones
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Objectives
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;To permanently store the numerical data from the &lt;code&gt;Git clones&lt;/code&gt; graph in the repository’s &lt;code&gt;Insights &amp;gt; Traffic&lt;/code&gt; section.

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://github.com/[ORG]/[REPO]/graphs/traffic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftu8aiab94d0ud9nd6n5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftu8aiab94d0ud9nd6n5t.png" alt="Git clones - GitHub" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Issues
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The graph does not display data older than two weeks.&lt;/li&gt;
&lt;li&gt;The GitHub REST API &lt;a href="https://docs.github.com/rest/metrics/traffic?apiVersion=2022-11-28#get-repository-clones" rel="noopener noreferrer"&gt;Get traffic clones&lt;/a&gt; has the same limitation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Solution
&lt;/h4&gt;

&lt;p&gt;I implemented the function &lt;a href="https://github.com/route06inc/ospo-google-apps-script/blob/d693ef5f6c8526f2c803500a5815d925bc98ee7f/lib/traffic_service.js#L17-L31" rel="noopener noreferrer"&gt;updateSheetWithLatestTrafficClones&lt;/a&gt; that uses this API to store the daily Clones and Unique cloners in a spreadsheet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flpn2ufamb7in7g2qbf7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flpn2ufamb7in7g2qbf7o.png" alt="Traffic clones - Google Sheets" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Top referral sources
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Objectives
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;To permanently store the numerical data from the &lt;code&gt;Referring sites&lt;/code&gt; graph in the repository’s &lt;code&gt;Insights &amp;gt; Traffic&lt;/code&gt; section.

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://github.com/[ORG]/[REPO]/graphs/traffic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycoro8juu9evh5ig8ekm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycoro8juu9evh5ig8ekm.png" alt="Referring sites - GitHub" width="400" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Issues
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The graph does not display data older than two weeks.&lt;/li&gt;
&lt;li&gt;The GitHub REST API &lt;a href="https://docs.github.com/rest/metrics/traffic?apiVersion=2022-11-28#get-top-referral-sources" rel="noopener noreferrer"&gt;Get top referral sources&lt;/a&gt; has the same limitation.&lt;/li&gt;
&lt;li&gt;Each value is the total for the past 14 days, which is not very user-friendly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Solution
&lt;/h4&gt;

&lt;p&gt;I implemented the function &lt;a href="https://github.com/route06inc/ospo-google-apps-script/blob/d693ef5f6c8526f2c803500a5815d925bc98ee7f/lib/traffic_service.js#L33-L51" rel="noopener noreferrer"&gt;updateSheetWithLatestTrafficReferrers&lt;/a&gt; using this API to store the daily Referring sites in a spreadsheet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wpj710a9q4z14w0l25h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wpj710a9q4z14w0l25h.png" alt="Top referral sources (raw) - Google Sheets" width="400" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To create a stacked graph, I made a pivot table of Views and Unique visitors on a separate sheet, along with their respective graphs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7inmeeq2h1x7t0kbcxut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7inmeeq2h1x7t0kbcxut.png" alt="Top referral sources (views) - Google Sheets" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxl32p5xy3fpgyeweyzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxl32p5xy3fpgyeweyzk.png" alt="Top referral sources (unique visitors) - Google Sheets" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While the issue of each value being a total for the past 14 days remains unsolved, it seems possible to separate them using spreadsheet functions.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I have published the OSS repository &lt;a href="https://github.com/route06inc/ospo-google-apps-script" rel="noopener noreferrer"&gt;https://github.com/route06inc/ospo-google-apps-script&lt;/a&gt; that continuously stores GitHub repository metrics using GAS.&lt;/p&gt;

&lt;p&gt;This tool is already in daily use on the repositories of Giselle and Liam, and the data is being effectively utilized.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;https://github.com/giselles-ai/giselle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;https://github.com/liam-hq/liam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ospo</category>
      <category>github</category>
      <category>googlesheets</category>
      <category>googleappsscript</category>
    </item>
    <item>
      <title>Generate Beautiful and Interactive ER-Diagrams with Liam ERD</title>
      <dc:creator>Hirotaka Miyagi</dc:creator>
      <pubDate>Wed, 22 Jan 2025 07:12:16 +0000</pubDate>
      <link>https://dev.to/route06/generate-beautiful-and-interactive-er-diagrams-with-liam-erd-5d5n</link>
      <guid>https://dev.to/route06/generate-beautiful-and-interactive-er-diagrams-with-liam-erd-5d5n</guid>
      <description>&lt;p&gt;We’ve developed a new database design tool called &lt;strong&gt;Liam ERD&lt;/strong&gt;, and it’s finally here! Let us introduce it to you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5utqj7knocjchqk8gmc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5utqj7knocjchqk8gmc.png" alt="Liam ERD" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://liambx.com/" rel="noopener noreferrer"&gt;https://liambx.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;We’ve released &lt;strong&gt;Liam ERD&lt;/strong&gt;, a tool that automatically generates ER diagrams to visualize database table structures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Version&lt;/strong&gt;: For public repositories, you can try prepending &lt;code&gt;https://liambx.com/erd/p/&lt;/code&gt; to the URL of a public schema file. For example, to view Mastodon’s schema: &lt;a href="https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/main/db/schema.rb" rel="noopener noreferrer"&gt;https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/main/db/schema.rb&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI Version&lt;/strong&gt;: For private repositories, we also provide a guide for deploying with Prisma + GitHub Actions + Cloudflare Pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why We Built Liam ERD
&lt;/h2&gt;

&lt;p&gt;In software development, ER diagrams (Entity Relationship Diagrams) simplify visualizing and sharing database structures, making communication smoother. They help reduce onboarding costs for new team members, assist non-engineering roles like PdMs or customer support with explanations, and enable data analytics teams to understand table structures without reading product code.&lt;/p&gt;

&lt;p&gt;While having ER diagrams as documentation is convenient, manually updating them with spreadsheets or diagramming tools is labor-intensive and prone to errors or omissions. That’s why it’s ideal to &lt;strong&gt;auto-generate ER diagrams&lt;/strong&gt; from schema files committed to the project repository or metadata retrieved from a database connection.&lt;/p&gt;

&lt;p&gt;Several tools already offer ER diagram auto-generation. For instance, tools using Mermaid.js or PlantUML generate images, but static images become hard to read for large, complex projects. Tools like SchemaSpy that output in HTML format also exist, but they often require extensive runtime and middleware dependencies, making integration into CI/CD pipelines challenging.&lt;/p&gt;

&lt;p&gt;We wanted a CI/CD-friendly, easy-to-set-up, and highly readable ER diagram auto-generation tool—thus, &lt;strong&gt;Liam ERD&lt;/strong&gt; was born.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features of Liam ERD
&lt;/h2&gt;

&lt;p&gt;Liam ERD has four key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modern and Interactive UI&lt;/strong&gt;: Supports panning, zooming, filtering, and focusing for better interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High Performance&lt;/strong&gt;: Works smoothly even with 100+ tables, with fast filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD Friendly&lt;/strong&gt;: Easy to set up and deploy, compatible with many hosting services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Source &amp;amp; Community-Driven&lt;/strong&gt;: Freely modifiable code with new features developed based on community feedback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s take a closer look at its features with some screenshots!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9m7mvho4a1t6etxzy5e.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9m7mvho4a1t6etxzy5e.jpg" alt="Initial view" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Liam ERD positions related tables close to each other, avoiding overly complex cardinality lines and ensuring a clean layout. Even for large-scale table structures, it provides excellent readability from the initial view.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuou15o0pov4j0xmgsscr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuou15o0pov4j0xmgsscr.jpg" alt="Highlighting related tables and columns on hover" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still, large-scale structures can be hard to grasp. Liam ERD highlights related tables and columns when hovering over a table, helping you quickly locate the tables you’re interested in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1v8yd36yopiw7hdwb14c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1v8yd36yopiw7hdwb14c.jpg" alt="Detailed table view pane" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Selecting a table brings up a detailed pane from the right, displaying comments on the table and columns, indexes, and a focused ER diagram of related tables.&lt;/p&gt;

&lt;p&gt;Other subtle user experiences make it easier to grasp table structures. For example, here’s a link to the open-source social media platform Mastodon’s ER diagram, which includes about 99 tables. Check it out!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/main/db/schema.rb" rel="noopener noreferrer"&gt;https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/main/db/schema.rb&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Liam ERD for Public Repositories
&lt;/h2&gt;

&lt;p&gt;For public GitHub repositories, it’s incredibly easy to use. Just prepend &lt;code&gt;https://liambx.com/erd/p/&lt;/code&gt; to the URL of your schema file, and Liam ERD will render the ER diagram.&lt;/p&gt;

&lt;p&gt;This allows you to view ER diagrams generated from the main branch schema. You can also replace &lt;code&gt;main&lt;/code&gt; with a specific commit hash to render the ER diagram at that point in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Prisma + GitHub Actions + Cloudflare Pages
&lt;/h2&gt;

&lt;p&gt;While the above is the simplest usage, you might think, "This won’t work for our internal projects!" And you’d be right!&lt;/p&gt;

&lt;p&gt;Liam ERD is also available as a CLI tool via npm, enabling you to generate ER diagrams locally or with GitHub Actions and host them easily.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@liam-hq/cli" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@liam-hq/cli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s a practical example of deploying with Prisma, GitHub Actions, and Cloudflare Pages. We chose Cloudflare Pages because its Cloudflare Access feature makes it simple to restrict access, such as allowing only internal members.&lt;/p&gt;

&lt;p&gt;First, prepare a &lt;code&gt;schema.prisma&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id             Int           @id @default(autoincrement())
  email          String        @unique
  username       String
  password       String
  createdAt      DateTime      @default(now())
  updatedAt      DateTime      @updatedAt
  profile        Profile?
  posts          Post[]
  comments       Comment[]
  orders         Order[]
  notifications  Notification[]
}

model Profile {
  id          Int      @id @default(autoincrement())
  userId      Int      @unique
  firstName   String
  lastName    String
  bio         String?
  avatar      String?
  birthDate   DateTime?
  phoneNumber String?
  user        User     @relation(fields: [userId], references: [id])
}

model Post {
  id        Int       @id @default(autoincrement())
  title     String
  content   String
  published Boolean   @default(false)
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  authorId  Int
  author    User      @relation(fields: [authorId], references: [id])
  comments  Comment[]
  tags      Tag[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  postId    Int
  authorId  Int
  post      Post     @relation(fields: [postId], references: [id])
  author    User     @relation(fields: [authorId], references: [id])
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

model Product {
  id          Int      @id @default(autoincrement())
  name        String
  description String
  price       Decimal
  stock       Int
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  orderItems  OrderItem[]
  categoryId  Int
  category    Category @relation(fields: [categoryId], references: [id])
}

model Category {
  id       Int       @id @default(autoincrement())
  name     String    @unique
  products Product[]
}

model Order {
  id         Int         @id @default(autoincrement())
  userId     Int
  status     OrderStatus @default(PENDING)
  totalPrice Decimal
  createdAt  DateTime    @default(now())
  updatedAt  DateTime    @updatedAt
  user       User        @relation(fields: [userId], references: [id])
  orderItems OrderItem[]
}

model OrderItem {
  id        Int     @id @default(autoincrement())
  orderId   Int
  productId Int
  quantity  Int
  price     Decimal
  order     Order   @relation(fields: [orderId], references: [id])
  product   Product @relation(fields: [productId], references: [id])
}

model Notification {
  id        Int      @id @default(autoincrement())
  userId    Int
  title     String
  content   String
  read      Boolean  @default(false)
  createdAt DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id])
}

enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, create a Cloudflare Pages project using Wrangler with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wrangler pages project create prisma-with-cloudflare-pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, add the following GitHub Actions workflow file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prisma-with-cloudflare-pages&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prisma/schema.prisma&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-deploy-erd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate ER Diagrams&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx @liam-hq/cli erd build --input prisma/schema.prisma --format prisma&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy ERD to Cloudflare Pages&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare/wrangler-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;apiToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_ACCOUNT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pages deploy ./dist --project-name=prisma-with-cloudflare-pages&lt;/span&gt;
          &lt;span class="na"&gt;gitHubToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s it—your ERD is deployed!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prisma-with-cloudflare-pages.pages.dev/" rel="noopener noreferrer"&gt;https://prisma-with-cloudflare-pages.pages.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4gegzy8d02ctttbpzvpz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4gegzy8d02ctttbpzvpz.jpg" alt="ERD in action" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since Liam ERD is built with Vite and React, it should be deployable on most hosting services with ease.&lt;/p&gt;

&lt;p&gt;A sample repository used in this example is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/liam-hq/liam-erd-samples/tree/main/samples/prisma-with-cloudflare-pages" rel="noopener noreferrer"&gt;https://github.com/liam-hq/liam-erd-samples/tree/main/samples/prisma-with-cloudflare-pages&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently, we support schema files for Ruby on Rails (&lt;code&gt;schema.rb&lt;/code&gt;), Prisma (&lt;code&gt;schema.prisma&lt;/code&gt;), and SQL DDL. We’re considering supporting other formats and welcome pull requests!&lt;/p&gt;

&lt;p&gt;Our documentation also includes instructions for using Liam ERD with supported ORMs and RDBMS. Check it out: &lt;a href="https://liambx.com/docs/parser/supported-formats" rel="noopener noreferrer"&gt;Supported Formats&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Features
&lt;/h2&gt;

&lt;p&gt;While Liam ERD currently focuses on visualizing table structures, we’re planning to add various database design features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced Documentation&lt;/strong&gt;: Grouping aggregations (similar to &lt;a href="https://github.com/k1LoW/tbls?tab=readme-ov-file#viewpoints" rel="noopener noreferrer"&gt;tbls viewpoints&lt;/a&gt;) and adding comments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ERD Editing&lt;/strong&gt;: Adding/editing tables and columns, with migration file generation for changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record Insights&lt;/strong&gt;: Connect to a database, run simple SQL, and make table structures more accessible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Version&lt;/strong&gt;: Private projects and real-time collaborative editing for teams.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As an open-source project, we need your contributions to make Liam ERD even better! Whether it’s reporting issues, suggesting features, or submitting pull requests, your help is invaluable.&lt;/p&gt;

&lt;p&gt;If you like what we’re building, we’d love your support—please give &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;our repository&lt;/a&gt; a ⭐️ on GitHub! Your encouragement helps us grow and continue improving.&lt;/p&gt;

&lt;p&gt;You can track our &lt;a href="https://github.com/orgs/liam-hq/projects/1/views/1" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt; here—feel free to comment or provide feedback!&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Liam ERD Now
&lt;/h2&gt;

&lt;p&gt;That’s &lt;strong&gt;Liam ERD&lt;/strong&gt;, a tool for effortlessly generating clear, readable ER diagrams. Give it a try!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;https://github.com/liam-hq/liam&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>database</category>
      <category>opensource</category>
      <category>liamerd</category>
    </item>
    <item>
      <title>Tutorial: Building a Collaborative Editing App with Yjs, valtio, and React</title>
      <dc:creator>Hirotaka Miyagi</dc:creator>
      <pubDate>Sat, 18 Jan 2025 09:02:07 +0000</pubDate>
      <link>https://dev.to/route06/tutorial-building-a-collaborative-editing-app-with-yjs-valtio-and-react-1mcl</link>
      <guid>https://dev.to/route06/tutorial-building-a-collaborative-editing-app-with-yjs-valtio-and-react-1mcl</guid>
      <description>&lt;p&gt;This article is an English translation of the following article:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tech.route06.co.jp/entry/2024/07/03/154219" rel="noopener noreferrer"&gt;チュートリアル: Yjs, valtio, React で実現する共同編集アプリケーション - ROUTE06 Tech Blog&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://yjs.dev/" rel="noopener noreferrer"&gt;Yjs&lt;/a&gt; is a framework that provides algorithms and data structures for real-time collaborative editing. It can offer experiences where multiple people simultaneously update the same content, similar to Notion and Figma.&lt;br&gt;&lt;br&gt;
Yjs provides shared data types such as Y.Map, Y.Array, and Y.Text, which can be used similarly to JavaScript’s Map and Array. Moreover, any changes made to this data are automatically distributed and synchronized with other clients.&lt;br&gt;&lt;br&gt;
Yjs is an implementation of what is called a &lt;strong&gt;Conflict-free Replicated Data Type (CRDT)&lt;/strong&gt; and is designed so that even if multiple people operate on the data at the same time, no conflicts occur and all clients eventually reach the same state.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;p&gt;Let’s look at a code example where Y.Map is automatically synchronized between clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Yjs documents are collections of&lt;/span&gt;
&lt;span class="c1"&gt;// shared objects that sync automatically.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ydoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Doc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// Define a shared Y.Map instance&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ymap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ydoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;ymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keyA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valueA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Create another Yjs document (simulating a remote user)&lt;/span&gt;
&lt;span class="c1"&gt;// and create some conflicting changes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ydocRemote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Doc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ymapRemote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ydocRemote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;ymapRemote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keyB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valueB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Merge changes from remote&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encodeStateAsUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ydocRemote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ydoc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Observe that the changes have merged&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toJSON&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// =&amp;gt; { keyA: 'valueA', keyB: 'valueB' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;(Quoted from &lt;a href="https://docs.yjs.dev/#quick-start" rel="noopener noreferrer"&gt;https://docs.yjs.dev/#quick-start&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The central component is the Y.Doc (a Yjs document). A Y.Doc contains multiple shared data types and manages synchronization with other clients. You create a Y.Doc for each client session, and each one holds a unique clientID.&lt;/p&gt;
&lt;h2&gt;
  
  
  Providers
&lt;/h2&gt;

&lt;p&gt;In the example above, multiple users were simulated on the same machine, but if you actually want to synchronize changes over a network, you use &lt;strong&gt;providers&lt;/strong&gt;. Yjs itself is not dependent on any particular network protocol, allowing you to freely switch among various providers or use multiple at the same time. Here are some examples of providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.yjs.dev/ecosystem/connection-provider/y-websocket" rel="noopener noreferrer"&gt;y-websocket&lt;/a&gt;: Implements a client-server model that sends and receives changes via WebSocket. It’s useful if you want to persist Yjs documents on the server or enable request authentication.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/yjs/y-webrtc" rel="noopener noreferrer"&gt;y-webrtc&lt;/a&gt;: Synchronizes via peer-to-peer with WebRTC, making fully distributed applications possible.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/yjs/y-indexeddb" rel="noopener noreferrer"&gt;y-indexeddb&lt;/a&gt;: Uses an IndexedDB database to store shared data in the browser, enabling offline editing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For other providers, please see the official documentation: &lt;a href="https://docs.yjs.dev/ecosystem/connection-provider" rel="noopener noreferrer"&gt;https://docs.yjs.dev/ecosystem/connection-provider&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Editor Bindings
&lt;/h2&gt;

&lt;p&gt;If you’re building an application that involves text editing, you’ll likely use an editor framework like ProseMirror or Quill. Yjs supports many common editor frameworks and can be used as a plugin or extension. In most common use cases, you won’t need to directly manipulate Yjs’s shared data types.&lt;br&gt;&lt;br&gt;
Check out the following list of editor frameworks supported by Yjs: &lt;a href="https://docs.yjs.dev/ecosystem/editor-bindings" rel="noopener noreferrer"&gt;https://docs.yjs.dev/ecosystem/editor-bindings&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Therefore, if you are using one of these editor frameworks, you’d be better off using a corresponding plugin. However, if you’re building a complex GUI for an application such as a design editor like Figma, there are plenty of cases where you might develop the editor UI from scratch.&lt;/p&gt;

&lt;p&gt;In this tutorial, we’ll introduce how to build a collaborative editing application connected to Yjs using React as the UI library, specifically demonstrating an example &lt;em&gt;without&lt;/em&gt; using editor bindings.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we’ll build a Kanban-style task management application. First, let’s show the final product.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://stackblitz.com/edit/yjs-kanban-tutorial?embed=1&amp;amp;file=src%2FApp.tsx&amp;amp;view=preview" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Fork on StackBlitz, open the preview in two tabs, and move your cursor over them!&lt;/p&gt;

&lt;p&gt;We’ll implement the following features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding and editing tasks&lt;/li&gt;
&lt;li&gt;Managing statuses: To Do / In Progress / Done&lt;/li&gt;
&lt;li&gt;Drag-and-drop reordering&lt;/li&gt;
&lt;li&gt;Multiple people can operate on it at the same time, and changes are reflected in real time&lt;/li&gt;
&lt;li&gt;Displaying other participants’ cursors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check the entire app in the following repository:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/route06" rel="noopener noreferrer"&gt;
        route06
      &lt;/a&gt; / &lt;a href="https://github.com/route06/yjs-kanban-tutorial" rel="noopener noreferrer"&gt;
        yjs-kanban-tutorial
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;yjs-kanban-tutorial&lt;/h1&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;EN: &lt;a href="https://dev.to/route06/tutorial-building-a-collaborative-editing-app-with-yjs-valtio-and-react-1mcl" rel="nofollow"&gt;Tutorial: Building a Collaborative Editing App with Yjs, valtio, and React - DEV Community&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;JA: &lt;a href="https://tech.route06.co.jp/entry/2024/07/03/154219" rel="nofollow noopener noreferrer"&gt;チュートリアル: Yjs, valtio, React で実現する共同編集アプリケーション - ROUTE06 Tech Blog&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This repository is sample code for the above.&lt;br&gt;
If you find any inadequacies in the tutorial, we would appreciate it if you could let us know at &lt;a href="https://github.com/route06/yjs-kanban-tutorial/issues/new" rel="noopener noreferrer"&gt;https://github.com/route06/yjs-kanban-tutorial/issues/new&lt;/a&gt; .&lt;/p&gt;
&lt;p&gt;このリポジトリは上記のサンプルコードです。&lt;br&gt;
チュートリアルの内容に不備があった場合、 &lt;a href="https://github.com/route06/yjs-kanban-tutorial/issues/new" rel="noopener noreferrer"&gt;https://github.com/route06/yjs-kanban-tutorial/issues/new&lt;/a&gt; でお知らせ下さると幸いです。&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/route06/yjs-kanban-tutorial" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  Using valtio as a Middle Layer Between Yjs and React
&lt;/h2&gt;

&lt;p&gt;Because we’re not using editor bindings this time, we’ll be directly operating on Yjs’s shared data types. However, if React components access these shared data types directly, it will become tightly coupled with Yjs, making testing difficult. Moreover, you might end up mixing React state management and another data flow in ways that can easily introduce bugs.&lt;/p&gt;

&lt;p&gt;Hence, we’ll use &lt;a href="https://valtio.pmnd.rs/" rel="noopener noreferrer"&gt;valtio&lt;/a&gt;, a simple proxy-based state management library. By combining valtio with &lt;a href="https://github.com/valtiojs/valtio-yjs" rel="noopener noreferrer"&gt;valtio-yjs&lt;/a&gt;, you can synchronize valtio’s state with Yjs’s shared data types. This lets React components interact with Yjs data via valtio’s state, making state management much simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Libraries Used
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we will also use the following libraries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt;: A superset of JavaScript that supports static typing. It improves code quality and reduces development-time errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt;: A build tool that provides a fast development environment. Lightweight, easy to use, and supports hot reloading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS Modules&lt;/strong&gt;: A mechanism for achieving scoped CSS. Helps you manage styles per component and avoid collisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nanoid&lt;/strong&gt;: A fast library for generating unique IDs. In this tutorial, it’s used to generate task IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project Setup
&lt;/h2&gt;

&lt;p&gt;That was a lot of background. Let’s jump right in! We’ll use Vite’s React and TypeScript template to create a new project called &lt;code&gt;yjs-kanban-tutorial&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm create vite@latest yjs-kanban-tutorial -- --template react-ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, set up the type definitions for tasks. In this tutorial, tasks have fields for status, value, and order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/types.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TaskStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;To Do&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;In Progress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s build out the UI at once. We’ll add these three components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TaskAddButton&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TaskItem&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TaskColumn&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since they’re just for styling at this point, we won’t go into detail. Please copy and paste the code as is.&lt;/p&gt;

&lt;h3&gt;
  
  
  TaskAddButton
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/TaskAddButton.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./TaskAddButton.module.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TaskAddButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;Add&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/TaskAddButton.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rounded-sm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.375rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-950&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;background-color&lt;/span&gt; &lt;span class="m"&gt;300ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.button&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.button&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rounded-sm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TaskItem
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/TaskItem.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./TaskItem.module.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listitem&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;
          &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;24&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;24&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;viewBox&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6 0 12 24&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;stroke&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currentColor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;strokeWidth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;strokeLinecap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;round&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;strokeLinejoin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;round&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Drag&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/title&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;circle&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;circle&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;circle&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;19&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;circle&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;circle&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;circle&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;19&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/svg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/TaskItem.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.listitem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;list-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="m"&gt;150ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.input&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rounded-xs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.button&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rounded-xs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TaskColumn
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/TaskColumn.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TaskAddButton&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./TaskAddButton&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./TaskColumn.module.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TaskItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./TaskItem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TaskStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TaskColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// TODO&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task 2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task 3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h2&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h2&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TaskItem&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="p"&gt;))}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ul&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TaskAddButton&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/TaskColumn.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-950&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rounded-md&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.list&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;list-style-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, replace the contents of App.tsx, main.tsx, App.css, and index.css with the following. Please delete the existing code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./App.module.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TaskColumn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./TaskColumn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Projects&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;Board&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TaskColumn&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;To Do&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TaskColumn&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;In Progress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TaskColumn&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;reactDom&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-dom/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./App.tsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./index.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Root element not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;reactDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StrictMode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/React.StrictMode&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/App.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.grid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/index.css */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="py"&gt;--zinc-950&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#09090b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--zinc-900&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#18181b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--zinc-800&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#27272a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--zinc-700&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3f3f46&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--zinc-400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#a1a1aa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--zinc-100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f4f4f5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--rounded-md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.375rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--rounded-sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--rounded-xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.125rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scrollbar-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;thin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we changed &lt;code&gt;App.css&lt;/code&gt; to &lt;code&gt;App.module.css&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;npm run dev&lt;/code&gt; at this point, the screen should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn2s52936exevf8vw5e3u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn2s52936exevf8vw5e3u.png" alt="Image description" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can check out the current state of this setup in the &lt;a href="https://github.com/route06/yjs-kanban-tutorial/tree/section-1-setup-project" rel="noopener noreferrer"&gt;section-1-setup-project&lt;/a&gt; branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Task Addition
&lt;/h2&gt;

&lt;p&gt;Let’s enable the addition of tasks. We’ll install valtio for state management and nanoid for generating IDs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install valtio nanoid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll use valtio to manage the state of the tasks. Add a file called &lt;code&gt;taskStore.ts&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/taskStore.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSnapshot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;valtio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TaskStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TaskStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filteredTasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;taskStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskStore&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskStore&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TaskStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useTasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskStore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type &lt;code&gt;TaskStore&lt;/code&gt; defines an object using the taskId as the key and &lt;code&gt;Task&lt;/code&gt; as the value. We’ve also provided a &lt;code&gt;filteredTasks()&lt;/code&gt; function that returns an array of tasks.&lt;br&gt;&lt;br&gt;
You may wonder, “Why not define &lt;code&gt;TaskStore&lt;/code&gt; as an array?” This is to make searching and editing tasks simpler, as we’ll explain later.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;proxy()&lt;/code&gt; is the foundation of valtio; it creates a proxy object that tracks changes to the object passed to it.&lt;br&gt;&lt;br&gt;
&lt;code&gt;useSnapshot()&lt;/code&gt; is a custom hook for using valtio’s proxy object from React components, which returns a read-only snapshot. React components automatically re-render when the object changes.&lt;/p&gt;

&lt;p&gt;Let’s see how to use these while we add some functionality. First, let’s make sure we can display tasks using &lt;code&gt;useSnapshot()&lt;/code&gt;. We’ll update &lt;code&gt;TaskColumn.tsx&lt;/code&gt; so that we can get tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/TaskColumn.tsx
&lt;span class="p"&gt;import type { FC } from "react";
import { TaskAddButton } from "./TaskAddButton";
import styles from "./TaskColumn.module.css";
import { TaskItem } from "./TaskItem";
&lt;/span&gt;&lt;span class="gd"&gt;-import type { Task, TaskStatus } from "./types";
&lt;/span&gt;&lt;span class="gi"&gt;+import type { TaskStatus } from "./types";
+import { filteredTasks, useTasks } from "./taskStore";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface Props {
&lt;/span&gt;  status: TaskStatus;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const TaskColumn: FC&amp;lt;Props&amp;gt; = ({ status }) =&amp;gt; {
&lt;/span&gt;&lt;span class="gd"&gt;- // TODO
- const tasks: Task[] = [
-   { id: "1", status, value: "Task 1", order: 1 },
-   { id: "2", status, value: "Task 2", order: 2 },
-   { id: "3", status, value: "Task 3", order: 3 },
- ];
&lt;/span&gt;&lt;span class="gi"&gt;+ const snapshot = useTasks();
+ const tasks = filteredTasks(status, snapshot);
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;div className={styles.wrapper}&amp;gt;
      &amp;lt;h2 className={styles.heading}&amp;gt;{status}&amp;lt;/h2&amp;gt;
      &amp;lt;ul className={styles.list}&amp;gt;
        {tasks.map((task) =&amp;gt; (
          &amp;lt;TaskItem key={task.id} task={task} /&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
      &amp;lt;TaskAddButton /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve replaced the dummy data so that we now get data from the read-only snapshot returned by &lt;code&gt;useSnapshot&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
However, because &lt;code&gt;taskStore&lt;/code&gt; has no data right now, nothing will display. Let’s make it possible to add new tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/taskStore.ts
&lt;span class="p"&gt;import { proxy, useSnapshot } from "valtio";
import type { Task, TaskStatus } from "./types";
&lt;/span&gt;&lt;span class="gi"&gt;+import { nanoid } from "nanoid";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface TaskStore {
&lt;/span&gt;  [taskId: string]: Task;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const filteredTasks = (status: TaskStatus, taskStore: TaskStore): Task[] =&amp;gt;
&lt;/span&gt;  Object.values(taskStore).filter((task) =&amp;gt; task.status === status)
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const taskStore = proxy&amp;lt;TaskStore&amp;gt;({});
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const useTasks = () =&amp;gt; useSnapshot(taskStore);
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+export const addTask = (status: TaskStatus) =&amp;gt; {
+  const order = 0; // dummy
+  const id = nanoid();
+  taskStore[id] = {
+    id,
+    status,
+    value: "",
+    order,
+  };
+};
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve added an &lt;code&gt;addTask()&lt;/code&gt; function. The &lt;code&gt;order&lt;/code&gt; value will be computed when we implement drag-and-drop, so for now we’ll use a placeholder.&lt;/p&gt;

&lt;p&gt;Next, let’s make &lt;code&gt;TaskAddButton&lt;/code&gt; use &lt;code&gt;addTask()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/TaskAddButton.tsx
&lt;span class="p"&gt;import type { FC } from "react";
import styles from "./TaskAddButton.module.css";
&lt;/span&gt;&lt;span class="gi"&gt;+import type { TaskStatus } from "./types";
+import { addTask } from "./taskStore";
+
+interface Props {
+  status: TaskStatus;
+}
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-export const TaskAddButton: FC = () =&amp;gt; {
&lt;/span&gt;&lt;span class="gi"&gt;+export const TaskAddButton: FC&amp;lt;Props&amp;gt; = ({ status }) =&amp;gt; {
&lt;/span&gt;  return (
&lt;span class="gd"&gt;-    &amp;lt;button type="button" className={styles.button}&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+    &amp;lt;button type="button" className={styles.button} onClick={() =&amp;gt; addTask(status)}&amp;gt;
&lt;/span&gt;      + Add
    &amp;lt;/button&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/TaskColumn.tsx
&lt;span class="p"&gt;import type { FC } from "react";
import { TaskAddButton } from "./TaskAddButton";
import styles from "./TaskColumn.module.css";
import { TaskItem } from "./TaskItem";
import type { TaskStatus } from "./types";
import { filteredTasks, useTasks } from "./taskStore";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface Props {
&lt;/span&gt;  status: TaskStatus;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const TaskColumn: FC&amp;lt;Props&amp;gt; = ({ status }) =&amp;gt; {
&lt;/span&gt;  const snapshot = useTasks();
  const tasks = filteredTasks(status, snapshot);
&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;div className={styles.wrapper}&amp;gt;
      &amp;lt;h2 className={styles.heading}&amp;gt;{status}&amp;lt;/h2&amp;gt;
      &amp;lt;ul className={styles.list}&amp;gt;
        {tasks.map((task) =&amp;gt; (
          &amp;lt;TaskItem key={task.id} task={task} /&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
&lt;span class="gd"&gt;-     &amp;lt;TaskAddButton /&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+     &amp;lt;TaskAddButton status={status} /&amp;gt;
&lt;/span&gt;    &amp;lt;/div&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, clicking the “+ Add” button on the screen lets us add tasks.&lt;/p&gt;

&lt;p&gt;When working with valtio, it’s good to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Displaying data&lt;/strong&gt;: Use &lt;code&gt;useSnapshot()&lt;/code&gt; to retrieve a read-only object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing data&lt;/strong&gt;: Directly modify the proxy object created by &lt;code&gt;proxy()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check the current state of things in the &lt;a href="https://github.com/route06/yjs-kanban-tutorial/tree/section-2-add-task" rel="noopener noreferrer"&gt;section-2-add-task&lt;/a&gt; branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Synchronizing Data Across Multiple Clients
&lt;/h2&gt;

&lt;p&gt;We’ve only made it possible to add tasks so far, but even at this stage we can enable collaborative editing. Let’s set up data synchronization across multiple clients. Install the necessary libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;yjs valtio-yjs@0.5.1 y-websocket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: The latest version of valtio-yjs, &lt;a href="https://github.com/valtiojs/valtio-yjs/releases/tag/v0.6.0" rel="noopener noreferrer"&gt;v0.6.0&lt;/a&gt;, seems to require valtio at version v2.0.0-rc.0 or later. Since v2 is still an RC release, this tutorial will use &lt;a href="https://github.com/valtiojs/valtio-yjs/releases/tag/v0.5.1" rel="noopener noreferrer"&gt;v0.5.1&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To synchronize data via a network in Yjs, you need a provider, as mentioned earlier. This time, we’ll be using &lt;a href="https://github.com/yjs/y-websocket" rel="noopener noreferrer"&gt;y-websocket&lt;/a&gt;. The client will connect to a single endpoint over WebSocket. The y-websocket package includes a server with an in-memory database, making it easy to persist data as well.&lt;/p&gt;

&lt;p&gt;First, let’s enable the WebSocket server. Add an npm script to your package.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev-websocket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"y-websocket --port 1234"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;npm run dev-websocket&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm run dev-websocket

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; yjs-kanban-tutorial@0.0.0 dev-websocket
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; y-websocket &lt;span class="nt"&gt;--port&lt;/span&gt; 1234

running at &lt;span class="s1"&gt;'localhost'&lt;/span&gt; on port 1234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add &lt;code&gt;yjs/yjs.ts&lt;/code&gt; to manage the Yjs document, and &lt;code&gt;yjs/useSyncToYjsEffect()&lt;/code&gt; to synchronize taskStore via Yjs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/yjs/yjs.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WebsocketProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y-websocket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Doc&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ydoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Doc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ymap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ydoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;taskStore.v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebsocketProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws://localhost:1234&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yjs-kanban-tutorial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ydoc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/yjs/useSyncToYjsEffect.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bind&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;valtio-yjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;taskStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../taskStore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ymap&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./yjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useSyncToYjsEffect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unbind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ymap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;unbind&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s take a closer look. A Y.Map named &lt;code&gt;"taskStore.v1"&lt;/code&gt; is created inside the Y.Doc instance. You can name it arbitrarily.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WebsocketProvider&lt;/code&gt; takes the endpoint, the room name, and the Y.Doc, in that order. The room name can also be any name you like, but in most real-world applications, you’ll likely have multiple rooms and let users pick the room to join, generally by specifying an identifiable value like an ID.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;useSyncToYjsEffect()&lt;/code&gt;, &lt;code&gt;bind()&lt;/code&gt; from &lt;code&gt;valtio-yjs&lt;/code&gt; is called inside a React &lt;code&gt;useEffect&lt;/code&gt; hook, passing in the valtio proxy object (taskStore) and the Y.Map (ymap).&lt;br&gt;&lt;br&gt;
This means that any operation on your screen is sent from &lt;code&gt;taskStore&lt;/code&gt; → &lt;code&gt;Y.Map&lt;/code&gt; → &lt;code&gt;WebSocket&lt;/code&gt; to other clients, while changes from other clients arrive via &lt;code&gt;WebSocket&lt;/code&gt; → &lt;code&gt;Y.Map&lt;/code&gt; → &lt;code&gt;taskStore&lt;/code&gt; and update your screen.&lt;/p&gt;

&lt;p&gt;Use this &lt;code&gt;useSyncToYjsEffect()&lt;/code&gt; in App.tsx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/App.tsx
&lt;span class="p"&gt;import type { FC } from "react";
import styles from "./App.module.css";
import { TaskColumn } from "./TaskColumn";
&lt;/span&gt;&lt;span class="gi"&gt;+import { useSyncToYjsEffect } from "./yjs/useSyncToYjsEffect";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;const App: FC = () =&amp;gt; {
&lt;/span&gt;&lt;span class="gi"&gt;+  useSyncToYjsEffect();
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;div className={styles.wrapper}&amp;gt;
      &amp;lt;h1 className={styles.heading}&amp;gt;Projects / Board&amp;lt;/h1&amp;gt;
      &amp;lt;div className={styles.grid}&amp;gt;
        &amp;lt;TaskColumn status="To Do" /&amp;gt;
        &amp;lt;TaskColumn status="In Progress" /&amp;gt;
        &amp;lt;TaskColumn status="Done" /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export default App;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you open two browser tabs and add tasks on one tab, they will be instantly reflected in the other tab in real time!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8c0kw3szc82if4a75fa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8c0kw3szc82if4a75fa.png" alt="Image description" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check &lt;a href="https://github.com/route06/yjs-kanban-tutorial/tree/section-3-bind-yjs" rel="noopener noreferrer"&gt;section-3-bind-yjs&lt;/a&gt; for the current state of this setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editing Task Contents
&lt;/h2&gt;

&lt;p&gt;We can now synchronize the addition of tasks, but you may notice that you can’t edit the text field of a task yet. Let’s add editing functionality!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/taskStore.ts
&lt;span class="p"&gt;import { nanoid } from "nanoid";
import { proxy, useSnapshot } from "valtio";
import type { Task, TaskStatus } from "./types";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface TaskStore {
&lt;/span&gt;  [taskId: string]: Task;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const filteredTasks = (status: TaskStatus, taskStore: TaskStore): Task[] =&amp;gt;
&lt;/span&gt;  Object.values(taskStore).filter((task) =&amp;gt; task.status === status);
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const taskStore = proxy&amp;lt;TaskStore&amp;gt;({});
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const useTasks = () =&amp;gt; useSnapshot(taskStore);
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const addTask = (status: TaskStatus) =&amp;gt; {
&lt;/span&gt;  const order = 0; // dummy
  const id = nanoid();
  taskStore[id] = {
    id,
    status,
    value: "",
    order,
  };
};
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+export const updateTask = (id: string, value: string) =&amp;gt; {
+  const task = taskStore[id];
+  if (task) {
+    task.value = value;
+  }
+};
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/TaskItem.tsx
&lt;span class="gd"&gt;-import type { FC } from "react";
&lt;/span&gt;&lt;span class="gi"&gt;+import { type ChangeEvent, type FC, useCallback } from "react";
&lt;/span&gt;&lt;span class="p"&gt;import styles from "./TaskItem.module.css";
&lt;/span&gt;&lt;span class="gi"&gt;+import { updateTask } from "./taskStore";
&lt;/span&gt;&lt;span class="p"&gt;import type { Task } from "./types";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface Props {
&lt;/span&gt;  task: Task;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const TaskItem: FC&amp;lt;Props&amp;gt; = ({ task }) =&amp;gt; {
&lt;/span&gt;&lt;span class="gi"&gt;+  const handleChange = useCallback(
+    (event: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
+      updateTask(task.id, event.target.value);
+    },
+    [task],
+  );
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;li className={styles.listitem}&amp;gt;
      &amp;lt;button type="button" className={styles.button}&amp;gt;
        &amp;lt;svg
          width="24"
          height="24"
          viewBox="6 0 12 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        &amp;gt;
          &amp;lt;title&amp;gt;Drag&amp;lt;/title&amp;gt;
          &amp;lt;circle cx="9" cy="12" r="1" /&amp;gt;
          &amp;lt;circle cx="9" cy="5" r="1" /&amp;gt;
          &amp;lt;circle cx="9" cy="19" r="1" /&amp;gt;
          &amp;lt;circle cx="15" cy="12" r="1" /&amp;gt;
          &amp;lt;circle cx="15" cy="5" r="1" /&amp;gt;
          &amp;lt;circle cx="15" cy="19" r="1" /&amp;gt;
        &amp;lt;/svg&amp;gt;
      &amp;lt;/button&amp;gt;
&lt;span class="gd"&gt;-      &amp;lt;input className={styles.input} value={task.value} /&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+      &amp;lt;input className={styles.input} value={task.value} onChange={handleChange} /&amp;gt;
&lt;/span&gt;    &amp;lt;/li&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now edit the text and have those edits synced via Yjs!&lt;/p&gt;

&lt;p&gt;To edit a task, we need to look it up; thanks to the data structure being an object, retrieving it with &lt;code&gt;taskStore[id]&lt;/code&gt; is straightforward. If we had used an array, we’d need to do something like &lt;code&gt;taskStore.find(task =&amp;gt; task.id === id)&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
You may feel some friction about directly editing state—&lt;code&gt;task.name = name&lt;/code&gt;—as in many React patterns you avoid mutating state directly. However, this is the way valtio works, and there’s a reason for it.&lt;br&gt;&lt;br&gt;
Every change made to a Yjs shared data type is grouped into what’s called a &lt;a href="https://docs.yjs.dev/getting-started/working-with-shared-types#transactions" rel="noopener noreferrer"&gt;transaction&lt;/a&gt;. Whenever you make a change, Yjs sends the update to other clients. Minimizing the scope of these changes helps reduce message size, so it’s important to keep changes as small as possible.&lt;/p&gt;

&lt;p&gt;Check &lt;a href="https://github.com/route06/yjs-kanban-tutorial/tree/section-4-update-task" rel="noopener noreferrer"&gt;section-4-update-task&lt;/a&gt; to see the current state.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reordering Tasks with Drag &amp;amp; Drop
&lt;/h2&gt;

&lt;p&gt;Let’s implement a more complex feature: reordering tasks by drag-and-drop.&lt;br&gt;&lt;br&gt;
First, we’ll implement a way to compute the &lt;code&gt;order&lt;/code&gt; field that we added to the &lt;code&gt;Task&lt;/code&gt; type earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/taskStore.ts
&lt;span class="p"&gt;import { proxy, useSnapshot } from "valtio";
import type { Task, TaskStatus } from "./types";
import { nanoid } from "nanoid";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface TaskStore {
&lt;/span&gt;  [taskId: string]: Task;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+const computeOrder = (prevId?: string, nextId?: string): number =&amp;gt; {
+  const prevOrder = taskStore[prevId ?? ""]?.order ?? 0;
+  const nextOrder = nextId ? taskStore[nextId].order : 1;
+
+  return (prevOrder + nextOrder) / 2;
+};
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const filteredTasks = (status: TaskStatus, taskStore: TaskStore) =&amp;gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  Object.values(taskStore).filter((task) =&amp;gt; task.status === status)
&lt;/span&gt;&lt;span class="gi"&gt;+  Object.values(taskStore)
+    .filter((task) =&amp;gt; task.status === status)
+    .sort((a, b) =&amp;gt; a.order - b.order);
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const taskStore = proxy&amp;lt;TaskStore&amp;gt;({});
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const useTasks = () =&amp;gt; useSnapshot(taskStore);
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const addTask = (status: TaskStatus) =&amp;gt; {
&lt;/span&gt;&lt;span class="gd"&gt;-  const order = 0; // dummy
&lt;/span&gt;&lt;span class="gi"&gt;+  const tasks = filteredTasks(status, taskStore);
+  const lastTask = tasks[tasks.length - 1];
+  const order = computeOrder(lastTask?.id)
&lt;/span&gt;  const id = nanoid();
  taskStore[id] = {
    id,
    status,
    value: "",
    order,
  };
};
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const updateTask = (id: string, value: string) =&amp;gt; {
&lt;/span&gt;  const task = taskStore[id];
  if (task) {
    task.value = value;
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several ways to implement list reordering, but we’ll do a simple version of &lt;strong&gt;Fractional Indexing&lt;/strong&gt; here. In short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All &lt;code&gt;order&lt;/code&gt; values are floating numbers such that &lt;code&gt;0 &amp;lt; index &amp;lt; 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We set the position by computing the average of the &lt;code&gt;order&lt;/code&gt; values on the elements before and after the new position.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s what &lt;code&gt;computeOrder()&lt;/code&gt; does.&lt;br&gt;&lt;br&gt;
Fractional indexing allows you to specify a position without touching existing elements’ indices, which is handy. However, doing many reorderings in a row can approach floating-point limits and lead to collisions, in which case you’d have to recalculate all positions. We won’t implement that for simplicity.&lt;/p&gt;

&lt;p&gt;We also apply &lt;code&gt;computeOrder()&lt;/code&gt; in &lt;code&gt;addTask()&lt;/code&gt;. We’ll treat any new tasks as if they’re added at the bottom of the list by taking the average of the last task’s &lt;code&gt;order&lt;/code&gt; value and &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, let’s implement &lt;code&gt;moveTask()&lt;/code&gt; to move a task using &lt;code&gt;computeOrder()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/taskStore.ts
&lt;span class="p"&gt;import { proxy, useSnapshot } from "valtio";
import type { Task, TaskStatus } from "./types";
import { nanoid } from "nanoid";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface TaskStore {
&lt;/span&gt;  [taskId: string]: Task;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;const computeOrder = (prevId?: string, nextId?: string): number =&amp;gt; {
&lt;/span&gt;  const prevOrder = taskStore[prevId ?? ""]?.order ?? 0;
  const nextOrder = nextId ? taskStore[nextId].order : 1;
&lt;span class="err"&gt;
&lt;/span&gt;  return (prevOrder + nextOrder) / 2;
};
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const filteredTasks = (status: TaskStatus, taskStore: TaskStore): Task[] =&amp;gt;
&lt;/span&gt;  Object.values(taskStore)
    .filter((task) =&amp;gt; task.status === status)
    .sort((a, b) =&amp;gt; a.order - b.order);
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const taskStore = proxy&amp;lt;TaskStore&amp;gt;({});
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const useTasks = () =&amp;gt; useSnapshot(taskStore);
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const addTask = (status: TaskStatus) =&amp;gt; {
&lt;/span&gt;  const tasks = filteredTasks(status, taskStore);
  const lastTask = tasks[tasks.length - 1];
  const order = computeOrder(lastTask?.id)
  const id = nanoid();
  taskStore[id] = {
    id,
    status,
    value: "",
    order,
  };
};
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const updateTask = (id: string, value: string) =&amp;gt; {
&lt;/span&gt;  const task = taskStore[id];
  if (task) {
    task.value = value;
  }
};
&lt;span class="gi"&gt;+
+export const moveTask = (id: string, status: TaskStatus, prevId?: string, nextId?: string) =&amp;gt; {
+  const order = computeOrder(prevId, nextId);
+  const task = taskStore[id];
+  if (task) {
+    task.status = status;
+    task.order = order;
+  }
+};
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll use &lt;a href="https://dndkit.com/" rel="noopener noreferrer"&gt;dnd kit&lt;/a&gt; for drag-and-drop. Let’s install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @dnd-kit/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll skip some details of how to use &lt;code&gt;@dnd-kit/core&lt;/code&gt;. First, we’ll implement a handler function in the &lt;code&gt;onDragEnd&lt;/code&gt; of &lt;a href="https://docs.dndkit.com/api-documentation/context-provider" rel="noopener noreferrer"&gt;DndContext&lt;/a&gt;. This handler calls &lt;code&gt;moveTask()&lt;/code&gt; to drop the task into its new position.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/dnd/DndProvider.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DndContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DragEndEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@dnd-kit/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PropsWithChildren&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;moveTask&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../taskStore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DndProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PropsWithChildren&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleDragEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DragEndEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;over&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;over&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;moveTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;prevId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;nextId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DndContext&lt;/span&gt; &lt;span class="nx"&gt;onDragEnd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleDragEnd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/DndContext&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we’ll enable dragging in &lt;code&gt;TaskItem&lt;/code&gt; using &lt;a href="https://docs.dndkit.com/api-documentation/draggable" rel="noopener noreferrer"&gt;useDraggable&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/TaskItem.tsx
&lt;span class="gi"&gt;+import { useDraggable } from "@dnd-kit/core";
+import { CSS } from "@dnd-kit/utilities";
&lt;/span&gt;&lt;span class="p"&gt;import { type ChangeEvent, type FC, useCallback } from "react";
import styles from "./TaskItem.module.css";
import { updateTask } from "./taskStore";
import type { Task } from "./types";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface Props {
&lt;/span&gt;  task: Task;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const TaskItem: FC&amp;lt;Props&amp;gt; = ({ task }) =&amp;gt; {
&lt;/span&gt;&lt;span class="gi"&gt;+  const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
+    id: task.id,
+  });
+  const style = {
+    transform: CSS.Translate.toString(transform),
+  };
&lt;/span&gt;  const handleChange = useCallback(
    (event: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
      updateTask(task.id, event.target.value);
    },
    [task],
  );
&lt;span class="err"&gt;
&lt;/span&gt;  return (
&lt;span class="gd"&gt;-    &amp;lt;li className={styles.listitem}&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+    &amp;lt;li
+      className={`${styles.listitem} ${isDragging ? styles.isDragging : ""}`}
+      ref={setNodeRef}
+      style={style}
+    &amp;gt;
&lt;/span&gt;&lt;span class="gd"&gt;-      &amp;lt;button type="button" className={styles.button}&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+      &amp;lt;button type="button" className={styles.button} {...listeners} {...attributes}&amp;gt;
&lt;/span&gt;        &amp;lt;svg
          width="24"
          height="24"
          viewBox="6 0 12 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        &amp;gt;
          &amp;lt;title&amp;gt;Drag&amp;lt;/title&amp;gt;
          &amp;lt;circle cx="9" cy="12" r="1" /&amp;gt;
          &amp;lt;circle cx="9" cy="5" r="1" /&amp;gt;
          &amp;lt;circle cx="9" cy="19" r="1" /&amp;gt;
          &amp;lt;circle cx="15" cy="12" r="1" /&amp;gt;
          &amp;lt;circle cx="15" cy="5" r="1" /&amp;gt;
          &amp;lt;circle cx="15" cy="19" r="1" /&amp;gt;
        &amp;lt;/svg&amp;gt;
      &amp;lt;/button&amp;gt;
      &amp;lt;input className={styles.input} value={task.value} onChange={handleChange} /&amp;gt;
    &amp;lt;/li&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we’ll use &lt;a href="https://docs.dndkit.com/api-documentation/droppable" rel="noopener noreferrer"&gt;useDroppable&lt;/a&gt; to define where items can be dropped. Here, we’ll display a marker between tasks to show where the user can drop. Let’s implement &lt;code&gt;DroppableMarker&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/dnd/DroppableMarker.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDroppable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@dnd-kit/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TaskStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./DroppableMarker.module.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;prevId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;nextId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DroppableMarker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isOver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNodeRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDroppable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
      &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setNodeRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;isOver&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOver&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can define arbitrary values in &lt;code&gt;data&lt;/code&gt;, so we store the information needed by &lt;code&gt;moveTask()&lt;/code&gt; here.&lt;br&gt;&lt;br&gt;
Let’s add the styles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/dnd/DroppableMarker.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;background-color&lt;/span&gt; &lt;span class="m"&gt;300ms&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.isOver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--zinc-400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we’ll place the &lt;code&gt;DroppableMarker&lt;/code&gt; between tasks in &lt;code&gt;TaskColumn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/TaskColumn.tsx
&lt;span class="gd"&gt;-import type { FC } from "react";
&lt;/span&gt;&lt;span class="gi"&gt;+import { type FC, Fragment } from "react";
&lt;/span&gt;&lt;span class="p"&gt;import { TaskAddButton } from "./TaskAddButton";
import styles from "./TaskColumn.module.css";
import { TaskItem } from "./TaskItem";
import { DroppableMarker } from "./dnd/DroppableMarker";
import { filteredTasks, useTasks } from "./taskStore";
import type { TaskStatus } from "./types";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface Props {
&lt;/span&gt;  status: TaskStatus;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const TaskColumn: FC&amp;lt;Props&amp;gt; = ({ status }) =&amp;gt; {
&lt;/span&gt;  const snapshot = useTasks();
  const tasks = filteredTasks(status, snapshot);
&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;div className={styles.wrapper}&amp;gt;
      &amp;lt;h2 className={styles.heading}&amp;gt;{status}&amp;lt;/h2&amp;gt;
      &amp;lt;ul className={styles.list}&amp;gt;
&lt;span class="gd"&gt;-       {tasks.map((task) =&amp;gt; (
-         &amp;lt;TaskItem key={task.id} task={task} /&amp;gt;
-       ))}
&lt;/span&gt;&lt;span class="gi"&gt;+       &amp;lt;DroppableMarker status={status} nextId={tasks[0]?.id} /&amp;gt;
+       {tasks.map((task, index) =&amp;gt; (
+         &amp;lt;Fragment key={task.id}&amp;gt;
+           &amp;lt;TaskItem task={task} /&amp;gt;
+           &amp;lt;DroppableMarker
+             key={`${task.id}-border`}
+             status={status}
+             prevId={task.id}
+             nextId={tasks[index + 1]?.id}
+           /&amp;gt;
+         &amp;lt;/Fragment&amp;gt;
+       ))}
&lt;/span&gt;      &amp;lt;/ul&amp;gt;
      &amp;lt;TaskAddButton status={status} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now reorder tasks by dragging and dropping them!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rlkuuert12mbl8ndc3b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rlkuuert12mbl8ndc3b.png" alt="Image description" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check &lt;a href="https://github.com/route06/yjs-kanban-tutorial/tree/section-5-dnd" rel="noopener noreferrer"&gt;section-5-dnd&lt;/a&gt; to see the current state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Synchronizing Cursor Positions
&lt;/h2&gt;

&lt;p&gt;So far, we’ve focused on syncing content, but in collaborative editing it’s also important to show “Who’s working where, right now?”—i.e., information about other users, such as cursor positions.&lt;br&gt;&lt;br&gt;
Because this information is only needed while editing and doesn’t require permanent storage, Yjs provides a feature called &lt;a href="https://docs.yjs.dev/api/about-awareness" rel="noopener noreferrer"&gt;Awareness CRDT&lt;/a&gt; separately from its shared data types. Let’s look at a code example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// All of our network providers implement the awareness crdt&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;awareness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awareness&lt;/span&gt;

&lt;span class="c1"&gt;// You can observe when a user updates their awareness information&lt;/span&gt;
&lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Whenever somebody updates their awareness information,&lt;/span&gt;
  &lt;span class="c1"&gt;// we log all awareness information from all users.&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStates&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// You can think of your own awareness information as a key-value store.&lt;/span&gt;
&lt;span class="c1"&gt;// We update our "user" field to propagate relevant user information.&lt;/span&gt;
&lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalStateField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Define a print name that should be displayed&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Emmanuelle Charpentier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Define a color that should be associated to the user:&lt;/span&gt;
  &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffb61e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// should be a hex color&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Quoted from &lt;a href="https://docs.yjs.dev/getting-started/adding-awareness#quick-start-awareness-crdt" rel="noopener noreferrer"&gt;https://docs.yjs.dev/getting-started/adding-awareness#quick-start-awareness-crdt&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;As you can see with &lt;code&gt;awareness.setLocalStateField()&lt;/code&gt;, you can store any data in JSON-encodable format in the Awareness CRDT. Here we store a user’s name and color code, but you could store cursor positions, text selection ranges, icon images, etc.&lt;/p&gt;

&lt;p&gt;Let’s implement a feature to synchronize cursor positions using the Awareness CRDT. First, we’ll create a custom hook to handle Awareness CRDT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/yjs/useAwareness.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSyncExternalStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./yjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UseAwarenessResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setLocalState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;awareness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStates&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setLocalState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAwareness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;UseAwarenessResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;states&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;awareness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;setLocalState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We used React’s built-in &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;useSyncExternalStore()&lt;/a&gt; to synchronize with an external data store, since valtio-yjs doesn’t support Awareness CRDT.&lt;br&gt;&lt;br&gt;
&lt;code&gt;useSyncExternalStore()&lt;/code&gt; is for when you want to reference an external data store and re-render the component whenever that data changes. It checks for changes by comparing snapshots via &lt;code&gt;Object.is()&lt;/code&gt;. Because &lt;code&gt;awareness.getStates()&lt;/code&gt; returns a Map, we serialize it to a JSON string in &lt;code&gt;getSnapshot()&lt;/code&gt; to compare easily.&lt;/p&gt;

&lt;p&gt;Next, let’s create a &lt;code&gt;Cursors&lt;/code&gt; component that saves and displays cursor data using this custom hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/yjs/Cursors.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAwareness&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./useAwareness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./Cursors.module.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;randomIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;randomIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// dummy&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Charlie&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;David&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Eve&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;green&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;magenta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fuchsia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MyInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CursorState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MyInfo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Cursors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;myInfo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocalState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useAwareness&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CursorState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cursors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MouseEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLocalState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;myInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousemove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousemove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;setLocalState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;myInfo&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cursors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
            &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;
            &lt;span class="nx"&gt;xmlns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://www.w3.org/2000/svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;24&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;24&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;viewBox&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 0 24 24&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;stroke&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currentColor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;strokeWidth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;strokeLinecap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;round&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;strokeLinejoin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;round&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
              &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Cursor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/title&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;m3 3 7.07 16.97 2.51-7.39 7.39-2.51L3 3z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;m13 13 6 6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/svg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;
            &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
            &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/yjs/Cursors.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pointer-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.cursor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.svg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.6rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.05rem&lt;/span&gt; &lt;span class="m"&gt;0.2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rounded-xs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, import and render &lt;code&gt;Cursors&lt;/code&gt; in &lt;code&gt;App.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// src/App.tsx
&lt;span class="p"&gt;import type { FC } from "react";
import styles from "./App.module.css";
import { TaskColumn } from "./TaskColumn";
import { DndProvider } from "./dnd/DndProvider";
import { useSyncToYjsEffect } from "./yjs/useSyncToYjsEffect";
&lt;/span&gt;&lt;span class="gi"&gt;+import { Cursors } from "./yjs/Cursors";
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;const App: FC = () =&amp;gt; {
&lt;/span&gt;  useSyncToYjsEffect();
&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;DndProvider&amp;gt;
      &amp;lt;div className={styles.wrapper}&amp;gt;
        &amp;lt;h1 className={styles.heading}&amp;gt;Projects / Board&amp;lt;/h1&amp;gt;
        &amp;lt;div className={styles.grid}&amp;gt;
          &amp;lt;TaskColumn status="To Do" /&amp;gt;
          &amp;lt;TaskColumn status="In Progress" /&amp;gt;
          &amp;lt;TaskColumn status="Done" /&amp;gt;
        &amp;lt;/div&amp;gt;
&lt;span class="gi"&gt;+        &amp;lt;Cursors /&amp;gt;
&lt;/span&gt;      &amp;lt;/div&amp;gt;
    &amp;lt;/DndProvider&amp;gt;
  );
};
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export default App;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we capture the mousemove event to update the cursor position and store it in the Awareness CRDT. We also exclude our own cursor data from the array of remote cursors for display.&lt;br&gt;&lt;br&gt;
We can now see the positions of other users’ cursors in real time!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdact6zya9xn2yecq8htn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdact6zya9xn2yecq8htn.png" alt="Image description" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see this in &lt;a href="https://github.com/route06/yjs-kanban-tutorial/tree/section-6-sync-cursors" rel="noopener noreferrer"&gt;section-6-sync-cursors&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Conclusion
&lt;/h2&gt;

&lt;p&gt;You’ve now built a practical collaborative editing application using Yjs. Here are some suggestions if you’d like to expand further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a login function and display the user’s actual name (from login) in the cursor&lt;/li&gt;
&lt;li&gt;Create multiple Kanban boards&lt;/li&gt;
&lt;li&gt;Persist Yjs documents to Redis or Amazon S3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you notice any issues in this tutorial, feel free to open a ticket at:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/route06/yjs-kanban-tutorial/issues/new" rel="noopener noreferrer"&gt;https://github.com/route06/yjs-kanban-tutorial/issues/new&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>yjs</category>
      <category>valtio</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Sharing the "OSS Publishing Tutorial" Created by ROUTE06, Inc.</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Mon, 06 Jan 2025 03:40:14 +0000</pubDate>
      <link>https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl</link>
      <guid>https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl</guid>
      <description>&lt;p&gt;Hey Devs!&lt;/p&gt;

&lt;p&gt;Recently, ROUTE06, Inc. has made two products, Giselle and Liam, open-source.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;https://github.com/giselles-ai/giselle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;https://github.com/liam-hq/liam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I'll share the "OSS Publishing Tutorial" that ROUTE06, Inc.'s OSS Promotion Office created and used when making these products open-source. Some internal links and references have been omitted.&lt;/p&gt;

&lt;p&gt;👉 ROUTE06, Inc.'s OSS Promotion Office functions as an &lt;a href="https://ospoglossary.todogroup.org/ospo-definition/" rel="noopener noreferrer"&gt;Open Source Program Office (OSPO)&lt;/a&gt;, responsible for promoting OSS utilization, managing risks, supporting community contributions, and advancing the open-sourcing of internal products and projects.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
OSS Publishing Tutorial

&lt;ul&gt;
&lt;li&gt;1. Preparation&lt;/li&gt;
&lt;li&gt;
2. Repository Setup

&lt;ul&gt;
&lt;li&gt;
2-1. Setup Items

&lt;ul&gt;
&lt;li&gt;Column: What to write in &lt;code&gt;[INSERT CONTACT EMAIL]&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;2-2. Create a Slack channel &lt;code&gt;#gh-*&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

3. Pre-Publishing Review

&lt;ul&gt;
&lt;li&gt;3-1. Conduct a Security Review&lt;/li&gt;
&lt;li&gt;3-2. Confirm Compliance&lt;/li&gt;
&lt;li&gt;3-3. Obtain Internal Approval for OSS Publishing&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

4. Repository Publishing

&lt;ul&gt;
&lt;li&gt;4-1. Publish the Repository&lt;/li&gt;
&lt;li&gt;4-2. Conduct a Post-Publishing Review by the OSS Promotion Office&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;li&gt;Supplement: About Giselle and Liam as OSS&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  OSS Publishing Tutorial
&lt;/h2&gt;

&lt;p&gt;This is a tutorial on how to publish an OSS repository on GitHub.&lt;/p&gt;

&lt;p&gt;💡 The OSS Promotion Office is available to consult on topics like license selection and other aspects.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Preparation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Decide which GitHub organization will host the repository

&lt;ul&gt;
&lt;li&gt;Choose whether to create the repository under &lt;a href="https://github.com/route06inc" rel="noopener noreferrer"&gt;https://github.com/route06inc&lt;/a&gt; or another organization&lt;sup id="fnref1"&gt;1&lt;/sup&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Decide the repository name

&lt;ul&gt;
&lt;li&gt;Choose a name for the repository under the organization&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Decide when to make the repository public

&lt;ul&gt;
&lt;li&gt;Typically, start as Private and switch to Public when preparations are complete&lt;/li&gt;
&lt;li&gt;If everything is ready, create it as Public from the beginning&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Agree to enable all repository features except for Wikis

&lt;ul&gt;
&lt;li&gt;Issues, Pull requests, Discussions, Projects&lt;/li&gt;
&lt;li&gt;Note: To encourage external contributions, this is the default policy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Decide whether to use English, Japanese, or both in various parts of the repository

&lt;ul&gt;
&lt;li&gt;Repository description&lt;/li&gt;
&lt;li&gt;Issues, Pull Requests, Discussions, Projects

&lt;ul&gt;
&lt;li&gt;Title, Content, Comments&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Source code comments&lt;/li&gt;
&lt;li&gt;Git commit messages&lt;/li&gt;
&lt;li&gt;Documentation

&lt;ul&gt;
&lt;li&gt;README.md, LICENSE, CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md

&lt;ul&gt;
&lt;li&gt;English is required, Japanese is optional. Both are allowed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Other text files&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Note: Decide within the limits of what won't hinder development. Add this later to CONTRIBUTING.md&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Decide the name of the private repository for internal communication

&lt;ul&gt;
&lt;li&gt;Create a private repository for purposes like the following. For example, &lt;code&gt;route06/*-internal&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Medium- to long-term planning&lt;/li&gt;
&lt;li&gt;Discussions about vulnerabilities not yet ready for disclosure&lt;/li&gt;
&lt;li&gt;Discussions involving confidential or collaborator-only&lt;sup id="fnref2"&gt;2&lt;/sup&gt; information&lt;/li&gt;
&lt;li&gt;Others&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use public repositories whenever possible to ensure transparency. For example:

&lt;ul&gt;
&lt;li&gt;Feature proposals&lt;/li&gt;
&lt;li&gt;Bug reports&lt;/li&gt;
&lt;li&gt;General Q&amp;amp;A&lt;/li&gt;
&lt;li&gt;Code reviews&lt;/li&gt;
&lt;li&gt;Documentation updates&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Decide on the repository's operation policy immediately after publishing

&lt;ul&gt;
&lt;li&gt;While it's unlikely there will be too many contributions right after publishing, set some guidelines. For example:

&lt;ul&gt;
&lt;li&gt;Keep operations light at the start&lt;/li&gt;
&lt;li&gt;When an Issue, Pull Request, or Discussion from a Contributor comes in, ⚪︎⚪︎ responds to it as needed&lt;/li&gt;
&lt;li&gt;Establish triage policies when contributions increase&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Repository Setup
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  2-1. Setup Items
&lt;/h4&gt;

&lt;p&gt;Create a repository in the organization decided in "1. Preparation" and proceed with the setup. All items are required unless otherwise discussed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;th&gt;Official Documentation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create the repository and invite members&lt;/td&gt;
&lt;td&gt;Available to use &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt; as a template&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/organizations/managing-organization-settings/restricting-repository-creation-in-your-organization" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable Issues, Discussions, Projects, and disable Wiki&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set the Description&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configure Topics&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/classifying-your-repository-with-topics" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create README.md&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-readmes" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create LICENSE&lt;/td&gt;
&lt;td&gt;Included in &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create issue templates&lt;/td&gt;
&lt;td&gt;Included in &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create pull request templates&lt;/td&gt;
&lt;td&gt;Included in &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create CONTRIBUTING.md&lt;/td&gt;
&lt;td&gt;Included in &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create SECURITY.md&lt;/td&gt;
&lt;td&gt;Included in &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/code-security/getting-started/adding-a-security-policy-to-your-repository" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create CODE_OF_CONDUCT.md&lt;/td&gt;
&lt;td&gt;Included in &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configure automatic deletion of merged branches&lt;/td&gt;
&lt;td&gt;Enable "Automatically delete head branches"&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-the-automatic-deletion-of-branches" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protect the default branch&lt;/td&gt;
&lt;td&gt;Define and configure rulesets&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable Dependabot Alerts&lt;/td&gt;
&lt;td&gt;Detect vulnerable dependencies&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/code-security/dependabot/dependabot-alerts/about-dependabot-alerts" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable GitHub Advanced Security (GHAS)&lt;/td&gt;
&lt;td&gt;Always enabled for public repositories&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/get-started/learning-about-github/about-github-advanced-security" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configure CodeQL&lt;/td&gt;
&lt;td&gt;Part of GHAS for detecting vulnerabilities. GitHub Actions workflow included in &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt;, so no additional setup required&lt;sup id="fnref3"&gt;3&lt;/sup&gt;.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make Dependency Review a required status before PR merges&lt;/td&gt;
&lt;td&gt;A GHAS feature to catch vulnerable dependencies before PR merges. GitHub Actions workflow included in &lt;a href="https://github.com/route06inc/template" rel="noopener noreferrer"&gt;route06inc/template&lt;/a&gt;.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/code-security/supply-chain-security/understanding-your-software-supply-chain/configuring-dependency-review" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable Secret scanning and Push protection&lt;/td&gt;
&lt;td&gt;A GHAS feature to detect secrets in code and block new pushes&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/code-security/secret-scanning/enabling-secret-scanning-features" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create CODEOWNERS (optional)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Column: What to write in &lt;code&gt;[INSERT CONTACT EMAIL]&lt;/code&gt;?
&lt;/h5&gt;

&lt;p&gt;In templates like CODE_OF_CONDUCT.md and SECURITY.md, replace &lt;code&gt;[INSERT CONTACT EMAIL]&lt;/code&gt; as follows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you want to use separate email addresses for each purpose:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="mailto:conduct@example.com"&gt;conduct@example.com&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;Specify in CODE_OF_CONDUCT.md&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="mailto:security@example.com"&gt;&lt;/a&gt;&lt;a href="mailto:security@example.com"&gt;security@example.com&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;Specify in SECURITY.md&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you want a single email address for all community inquiries:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Example 1: &lt;a href="mailto:community@example.com"&gt;community@example.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Example 2: &lt;a href="mailto:oss@example.com"&gt;oss@example.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  2-2. Create a Slack channel &lt;code&gt;#gh-*&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;💡 At ROUTE06, Inc., creating a &lt;code&gt;#gh-*&lt;/code&gt; Slack channel for GitHub repository notifications is encouraged.&lt;/p&gt;

&lt;p&gt;Once the channel is created, run the following command in it. Replace &lt;code&gt;ORG/REPO&lt;/code&gt; as needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/github subscribe ORG/REPO issues, pulls, commits, releases, deployments, reviews, comments, discussions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will enable notifications. Check the configuration with &lt;code&gt;/github subscribe list features&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By default, notifications are posted in threads. Disable this by following these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;/github settings&lt;/code&gt; in the channel&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Disable&lt;/code&gt; under "Disable threading for Pull Request and Issue notifications"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ref: &lt;a href="https://github.com/integrations/slack/issues/1500#issuecomment-1335564029" rel="noopener noreferrer"&gt;https://github.com/integrations/slack/issues/1500#issuecomment-1335564029&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Pre-Publishing Review
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  3-1. Conduct a Security Review
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Verify that the &lt;code&gt;SECURITY.md&lt;/code&gt; file is added to the repository and that it clearly outlines the security policy and vulnerability reporting procedure&lt;/li&gt;
&lt;li&gt;Ensure that the following &lt;a href="https://docs.github.com/get-started/learning-about-github/about-github-advanced-security" rel="noopener noreferrer"&gt;GHAS&lt;/a&gt; features are enabled

&lt;ul&gt;
&lt;li&gt;Code scanning (CodeQL)

&lt;ul&gt;
&lt;li&gt;Confirm the &lt;code&gt;.github/workflows/codeql.yml&lt;/code&gt; commit&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Secret scanning and Push protection&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Confirm that &lt;a href="https://docs.github.com/code-security/dependabot/dependabot-alerts/about-dependabot-alerts" rel="noopener noreferrer"&gt;Dependabot Alerts&lt;/a&gt; is enabled&lt;/li&gt;

&lt;li&gt;Ensure that no internal information is hardcoded in the repository's source code

&lt;ul&gt;
&lt;li&gt;Example: Preventing non-company email addresses from being registered as a safeguard&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Confirm there are no internal links in the repository's source code or commit messages

&lt;ul&gt;
&lt;li&gt;Example: Slack URLs, internal private repository URLs, issue numbers starting with &lt;code&gt;#&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;If such links exist, they do not need to be erased from history with &lt;code&gt;git rebase&lt;/code&gt;. Just ensure they are not included going forward&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Verify that no proprietary names appear in the repository's source code or commit messages

&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;$ git log -p | grep -E '(◯◯|□□|△△)'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Use full commit hashes for third-party GitHub Actions version specifications (optional)

&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;- uses: owner/action-name@26968a09c0ea4f3e233fdddbafd1166051a095f6 # v1.0.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  3-2. Confirm Compliance
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Verify licensing

&lt;ul&gt;
&lt;li&gt;Ensure the &lt;code&gt;LICENSE&lt;/code&gt; file is added to the repository and that the selected license applies appropriately to the source code without infringing on third-party rights&lt;/li&gt;
&lt;li&gt;Confirm compatibility between the repository's license and the licenses of dependent libraries&lt;/li&gt;
&lt;li&gt;Reference:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/route06/how-we-started-continuous-oss-license-monitoring-with-license-finder-a9i"&gt;How We Started Continuous OSS License Monitoring with License Finder - DEV Community&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Check export regulations

&lt;ul&gt;
&lt;li&gt;Confirm that the project does not include technologies subject to export regulations&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  3-3. Obtain Internal Approval for OSS Publishing
&lt;/h4&gt;

&lt;p&gt;This step requires that you first decide on a license for your project. Handle this as soon as the license is finalized.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Repository Publishing
&lt;/h3&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  4-1. Publish the Repository
&lt;/h4&gt;

&lt;p&gt;Coordinate as needed with a blog post or a press release.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  4-2. Conduct a Post-Publishing Review by the OSS Promotion Office
&lt;/h4&gt;

&lt;p&gt;Even after publishing the repository, verify that the repository settings are configured as intended.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check for any omissions in "2. Repository Setup"&lt;/li&gt;
&lt;li&gt;Confirm that the following &lt;a href="https://docs.github.com/get-started/learning-about-github/about-github-advanced-security" rel="noopener noreferrer"&gt;GHAS&lt;/a&gt; features are enabled: Code scanning (CodeQL), Secret scanning, Push protection&lt;/li&gt;
&lt;li&gt;Verify that &lt;a href="https://docs.github.com/code-security/dependabot/dependabot-alerts/about-dependabot-alerts" rel="noopener noreferrer"&gt;Dependabot Alerts&lt;/a&gt; is enabled&lt;/li&gt;
&lt;li&gt;Ensure the "Report a Vulnerability" link in the repository's &lt;code&gt;SECURITY.md&lt;/code&gt; file works for both:

&lt;ul&gt;
&lt;li&gt;Members of the GitHub organization&lt;/li&gt;
&lt;li&gt;External users of the GitHub organization

&lt;ul&gt;
&lt;li&gt;If the link results in a 404 Not Found error for external users, &lt;a href="https://docs.github.com/code-security/security-advisories/working-with-repository-security-advisories/configuring-private-vulnerability-reporting-for-a-repository" rel="noopener noreferrer"&gt;Private vulnerability reporting&lt;/a&gt; might be disabled&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Confirm that &lt;code&gt;Reported content&lt;/code&gt; in &lt;a href="https://docs.github.com/communities/moderating-comments-and-conversations/managing-how-contributors-report-abuse-in-your-organizations-repository" rel="noopener noreferrer"&gt;Reported content settings&lt;/a&gt; is set to &lt;code&gt;Prior contributors and collaborators&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;💡 This setting restricts or relaxes who can report vulnerabilities&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, I shared the "OSS Publishing Tutorial" that we created and used when making Giselle and Liam open-source.&lt;/p&gt;

&lt;p&gt;Although I have experience with open-sourcing projects personally, I initially thought, "Isn't it just about publishing a repository?" However, in a professional setting, numerous other things to consider came to light, resulting in this detailed "OSS Publishing Tutorial." We plan to keep updating it moving forward.&lt;/p&gt;

&lt;p&gt;I hope this serves as a useful reference.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Supplement: About Giselle and Liam as OSS
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;giselles-ai/giselle&lt;/a&gt; repository recently published as OSS is for Giselle, a platform that enables no-code creation of agents and workflows utilizing generative AI. Detailed information can be found on the Giselle service website: &lt;a href="https://giselles.ai/" rel="noopener noreferrer"&gt;https://giselles.ai/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;liam-hq/liam&lt;/a&gt; repository is for Liam, a tool that effortlessly generates beautiful and easy-to-read ER diagrams. Detailed information can be found on the Liam service website: &lt;a href="https://liambx.com/" rel="noopener noreferrer"&gt;https://liambx.com/&lt;/a&gt;.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Example: &lt;a href="https://github.com/giselles-ai" rel="noopener noreferrer"&gt;https://github.com/giselles-ai&lt;/a&gt;, &lt;a href="https://github.com/liam-hq" rel="noopener noreferrer"&gt;https://github.com/liam-hq&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;ROUTE06, Inc. employees or partners with Admin/Write permissions for the repository ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Requires using the default CodeQL configuration to set it as a required status before PR merges ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ospo</category>
      <category>opensource</category>
      <category>github</category>
    </item>
    <item>
      <title>Continuously Storing Traffic Data of Multiple GitHub Repositories Using GAS</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Mon, 23 Dec 2024 01:38:10 +0000</pubDate>
      <link>https://dev.to/route06/continuously-storing-traffic-data-of-multiple-github-repositories-using-gas-4fio</link>
      <guid>https://dev.to/route06/continuously-storing-traffic-data-of-multiple-github-repositories-using-gas-4fio</guid>
      <description>&lt;p&gt;Hey Devs!&lt;/p&gt;

&lt;p&gt;In my previous post, I discussed metrics related to OSS activities, particularly those with limited retention periods:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As mentioned above, Views, Unique views, and Clones are only available for the last two weeks. In the next issue, we will show how we solved this problem.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/route06" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9867%2Fc09ac11b-bf4c-44c7-b2bb-470d71c87e5d.jpg" alt="ROUTE06, Inc." width="800" height="800"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg" alt="" width="460" height="460"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/route06/consideration-of-oss-activity-metrics-based-on-github-repository-data-247i" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Consideration of OSS Activity Metrics Based on GitHub Repository Data&lt;/h2&gt;
      &lt;h3&gt;Takashi Masuda for ROUTE06, Inc. ・ Dec 16 '24&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#ospo&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#opensource&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#github&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#marketing&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;This time, I'll introduce a method to store these metrics for more than two weeks using Google Apps Script (GAS) and Google Sheets.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a Google Sheet&lt;/li&gt;
&lt;li&gt;Using a GitHub App Access Token&lt;/li&gt;
&lt;li&gt;Creating a GitHub App&lt;/li&gt;
&lt;li&gt;
Implementing GAS

&lt;ul&gt;
&lt;li&gt;main.gs&lt;/li&gt;
&lt;li&gt;github.gs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Configuring Script Properties in GAS

&lt;ul&gt;
&lt;li&gt;GITHUB_APP_ID&lt;/li&gt;
&lt;li&gt;GITHUB_APP_PRIVATE_KEY&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Creating a Trigger for Scheduled Execution&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;li&gt;Supplement: About Giselle and Liam as OSS&lt;/li&gt;

&lt;li&gt;Update (March 21, 2025): Now Available on GitHub Repository&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Google Sheet
&lt;/h2&gt;

&lt;p&gt;Create a Google Sheet with one sheet per repository, as shown below. For this post, I created sheets for &lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;giselle&lt;/a&gt; and &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;liam&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frg2idx0okfwa6uvm55c1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frg2idx0okfwa6uvm55c1.png" alt="Example of Google Sheet Creation" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Traffic data can be viewed in the Insights tab under the Traffic sidebar for each repository. Here are some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/giselles-ai/giselle/graphs/traffic" rel="noopener noreferrer"&gt;https://github.com/giselles-ai/giselle/graphs/traffic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liam-hq/liam/graphs/traffic" rel="noopener noreferrer"&gt;https://github.com/liam-hq/liam/graphs/traffic&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manually inputting data by referencing these graphs is tedious, so I recommend using the &lt;a href="https://docs.github.com/github-cli" rel="noopener noreferrer"&gt;gh CLI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Retrieving Views and Unique Visitors for the past two weeks:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gh api &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github.v3.star+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   /repos/giselles-ai/giselle/traffic/views &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'["Date", "Views", "Unique visitors"],(.views[] | [.timestamp, .count, .uniques]) | @csv'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/T00:00:00Z//g'&lt;/span&gt;
&lt;span class="s2"&gt;"Date"&lt;/span&gt;,&lt;span class="s2"&gt;"Views"&lt;/span&gt;,&lt;span class="s2"&gt;"Unique visitors"&lt;/span&gt;
&lt;span class="s2"&gt;"2024-12-03"&lt;/span&gt;,33,7
&lt;span class="s2"&gt;"2024-12-04"&lt;/span&gt;,273,17
&lt;span class="o"&gt;(&lt;/span&gt;snip&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Example: Retrieving Clones and Unique Cloners for the past two weeks:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gh api &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github.v3.star+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   /repos/giselles-ai/giselle/traffic/clones &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'["Date", "Clones", "Unique cloners"],(.clones[] | [.timestamp, .count, .uniques]) | @csv'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/T00:00:00Z//g'&lt;/span&gt;
&lt;span class="s2"&gt;"Date"&lt;/span&gt;,&lt;span class="s2"&gt;"Clones"&lt;/span&gt;,&lt;span class="s2"&gt;"Unique cloners"&lt;/span&gt;
&lt;span class="s2"&gt;"2024-12-03"&lt;/span&gt;,12,5
&lt;span class="s2"&gt;"2024-12-04"&lt;/span&gt;,148,12
&lt;span class="o"&gt;(&lt;/span&gt;snip&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These commands use the following GitHub REST API endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/rest/metrics/traffic?apiVersion=2022-11-28#get-page-views" rel="noopener noreferrer"&gt;Get page views | REST API endpoints for repository traffic - GitHub Doc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/rest/metrics/traffic?apiVersion=2022-11-28#get-repository-clones" rel="noopener noreferrer"&gt;Get repository clones | REST API endpoints for repository traffic - GitHub Doc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Using a GitHub App Access Token
&lt;/h2&gt;

&lt;p&gt;An Access Token is required to use the GitHub API.&lt;/p&gt;

&lt;p&gt;While using a Personal Access Token is straightforward, both Classic and Fine-grained tokens have the following issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The operational problems associated with a GitHub user&lt;/li&gt;
&lt;li&gt;The security risks associated with a long-lived token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To address these, I created a dedicated GitHub App and issued short-lived Access Tokens for each GAS execution.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating a GitHub App
&lt;/h2&gt;

&lt;p&gt;Follow the official documentation to create a GitHub App and install it to the required repositories.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://docs.github.com/apps/creating-github-apps/registering-a-github-app/registering-a-github-app" rel="noopener noreferrer"&gt;Registering a GitHub App - GitHub Docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only the following permissions are needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Administration Read-only&lt;/li&gt;
&lt;li&gt;Metadata Read-only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since we won't use Webhooks, uncheck the "Active" box.&lt;/p&gt;

&lt;p&gt;After you have completed the creation, please also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Note the App ID&lt;/li&gt;
&lt;li&gt;Generate a Private Key and download it locally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementing GAS
&lt;/h2&gt;

&lt;p&gt;Open the spreadsheet and click "Extensions" → "Apps Script" from the menu.&lt;/p&gt;

&lt;p&gt;Create two files and paste the following code into each:&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  main.gs
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Copyright (c) 2024 ROUTE06, Inc.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Licensed under the Apache License, Version 2.0 (the "License");&lt;/span&gt;
&lt;span class="c1"&gt;// you may not use this file except in compliance with the License.&lt;/span&gt;
&lt;span class="c1"&gt;// You may obtain a copy of the License at&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;//     https://www.apache.org/licenses/LICENSE-2.0&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Unless required by applicable law or agreed to in writing, software&lt;/span&gt;
&lt;span class="c1"&gt;// distributed under the License is distributed on an "AS IS" BASIS,&lt;/span&gt;
&lt;span class="c1"&gt;// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&lt;/span&gt;
&lt;span class="c1"&gt;// See the License for the specific language governing permissions and&lt;/span&gt;
&lt;span class="c1"&gt;// limitations under the License.&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COLLECTION_TARGETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;giselles-ai/giselle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;giselle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewsDateColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clonesDateColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;liam-hq/liam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;liam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewsDateColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clonesDateColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;COLLECTION_TARGETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateSheetWithLatestData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Update Google Sheet with latest GitHub data
 *
 * @param {string} repo - GitHub Repository name (e.g., 'giselles-ai/giselle')
 * @param {string} sheetName - (e.g., 'giselle')
 * @param {string} viewsDateColumn - (e.g., 'A')
 * @param {string} clonesDateColumn - (e.g., 'E')
 * @return {void}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateSheetWithLatestData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheetName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viewsDateColumn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clonesDateColumn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;updateSheetWithLatestTrafficViews&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;viewsDateColumn&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;updateSheetWithLatestTrafficClones&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clonesDateColumn&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Update Google Sheet with latest traffic views
 *
 * @param {string} repo - GitHub Repository name (e.g., 'giselles-ai/giselle')
 * @param {string} sheetName - (e.g., 'giselle')
 * @param {string} column - (e.g., 'A')
 * @return {void}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateSheetWithLatestTrafficViews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheetName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trafficViews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GitHubGetTrafficViews&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertTimestampToDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trafficViews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;views&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;updateSheetWithLatestCore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;actualData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Update Google Sheet with latest traffic clones
 *
 * @param {string} repo - GitHub Repository name (e.g., 'giselles-ai/giselle')
 * @param {string} sheetName - (e.g., 'giselle')
 * @param {string} column - (e.g., 'E')
 * @return {void}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateSheetWithLatestTrafficClones&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheetName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trafficClones&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GitHubGetTrafficClones&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertTimestampToDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trafficClones&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clones&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;updateSheetWithLatestCore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;actualData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Update Google Sheet with the data passed as argument
 *
 * @param {Array.&amp;lt;{date: Date, count: number, uniques: number}&amp;gt;} actualData
 * @param {string} sheetName - (e.g., 'giselle')
 * @param {string} column - (e.g., 'E')
 * @return {void}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateSheetWithLatestCore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;actualData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sheetName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;earliestDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getEarliestDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actualData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blankData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildBlankData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;earliestDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;completeData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mergeActualAndBlank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actualData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blankData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;curDateCell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;vlookupWithDate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;earliestDate&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;completeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// YYYY-MM-DD&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;curCountCell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCountCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;curDateCell&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;curUniquesCell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUniquesCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;curDateCell&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[Write] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;curDateCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getA1Notation&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;formattedDate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;curCountCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getA1Notation&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;curUniquesCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getA1Notation&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uniques&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;curDateCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formattedDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;curCountCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;curUniquesCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uniques&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;curDateCell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getNextDateCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;curDateCell&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DateNotFoundError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Searches the specified column vertically and returns cell names matching the specified date
 *
 * @param {string} sheetName - (e.g., 'giselle')
 * @param {string} column - (e.g., 'A')
 * @param {Date} targetDate
 * @return {Range} - (e.g., the range of 'A31')
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vlookupWithDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSpreadsheet&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getSheetByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Get the entire column range&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rowIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rowIndex&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;DateNotFoundError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;column&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;rowIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getCountCell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dateCell&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;dateCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getUniquesCell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dateCell&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;dateCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getNextDateCell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dateCell&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;dateCell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;span class="cm"&gt;/**
 * Convet timestamp to date
 *
 * @param {Array.&amp;lt;{timestamp: string, count: number, uniques: number}&amp;gt;} data
 * @return {Array.&amp;lt;{date: Date, count: number, uniques: number}&amp;gt;}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;convertTimestampToDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;uniques&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uniques&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Merge actual data and blank data
 *
 * @param {Array.&amp;lt;{date: Date, count: number, uniques: number}&amp;gt;} actual
 * @param {Array.&amp;lt;{date: Date, count: 0, uniques: 0}&amp;gt;} blank
 * @return {Array.&amp;lt;{date: Date, count: number, uniques: number}&amp;gt;}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mergeActualAndBlank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blankItem&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Find data matching date in `actual`&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actualItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;blankItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="c1"&gt;// If `actual` data is available, it is given priority; otherwise, `blank` data is used.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;actualItem&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;blankItem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Get earliest date
 *
 * @param {Array.&amp;lt;{date: Date, count: number, uniques: number}&amp;gt;} data
 * @return {Date}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getEarliestDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Build blank data
 *
 * @param {Date} inStartDate
 * @return {Array.&amp;lt;{date: Date, count: 0, uniques: 0}&amp;gt;}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buildBlankData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inStartDate&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inStartDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Don't let the argument values change&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;uniques&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  github.gs
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Copyright (c) 2024 ROUTE06, Inc.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Licensed under the Apache License, Version 2.0 (the "License");&lt;/span&gt;
&lt;span class="c1"&gt;// you may not use this file except in compliance with the License.&lt;/span&gt;
&lt;span class="c1"&gt;// You may obtain a copy of the License at&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;//     https://www.apache.org/licenses/LICENSE-2.0&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Unless required by applicable law or agreed to in writing, software&lt;/span&gt;
&lt;span class="c1"&gt;// distributed under the License is distributed on an "AS IS" BASIS,&lt;/span&gt;
&lt;span class="c1"&gt;// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&lt;/span&gt;
&lt;span class="c1"&gt;// See the License for the specific language governing permissions and&lt;/span&gt;
&lt;span class="c1"&gt;// limitations under the License.&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_APP_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PropertiesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptProperties&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GITHUB_APP_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_APP_PRIVATE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PropertiesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptProperties&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GITHUB_APP_PRIVATE_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Get traffic views
 *
 * @param {string} repo - GitHub Repository name (e.g., 'giselles-ai/giselle')
 * @return {Object}
 * @see https://docs.github.com/rest/metrics/traffic?apiVersion=2022-11-28#get-page-views
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GitHubGetTrafficViews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;gitHubApiGet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/repos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/traffic/views`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Get traffic clones
 *
 * @param {string} repo - GitHub Repository name (e.g., 'giselles-ai/giselle')
 * @return {Object}
 * @see https://docs.github.com/rest/metrics/traffic?apiVersion=2022-11-28#get-repository-clones
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GitHubGetTrafficClones&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;gitHubApiGet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/repos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/traffic/clones`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Call [GET] GitHub API
 *
 * @param {string} repo - GitHub Repository name (e.g., 'giselles-ai/giselle')
 * @param {string} path - the API path (e.g., /repos/giselles-ai/giselle/traffic/views)
 * @return {Object}
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gitHubApiGet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createGitHubAppToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UrlFetchApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.github.com&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.github+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`token &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-GitHub-Api-Version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2022-11-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Create GitHub App installation access token
 *
 * @param {string} repo - GitHub Repository name (e.g., 'giselles-ai/giselle')
 * @return {string}
 * @see https://docs.github.com/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
 * @see https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app
 * @note Use Closure to cache the App Tokens by repo
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createGitHubAppToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Hit the cache for the GitHub App Token for repo &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tokenCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createJWT&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_APP_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;private_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_APP_PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;installationID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getGitHubAppInstallationID&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`repo: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, installationID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;installationID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UrlFetchApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.github.com/app/installations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;installationID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/access_tokens`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.github+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-GitHub-Api-Version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2022-11-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContentText&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;tokenCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Cached GitHub App Token for repo &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Create JWT
 *
 * @param {string} app_id - GitHub App ID
 * @param {string} private_key - GitHub App private key
 * @return {string}
 * @see https://docs.github.com/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createJWT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;app_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;private_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Issues 60 seconds in the past&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Expires 10 minutes in the future&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headerJSON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;typ&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JWT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base64EncodeWebSafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headerJSON&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payloadJSON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;iat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;iat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;iss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base64EncodeWebSafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payloadJSON&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headerPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base64EncodeWebSafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;computeRsaSha256Signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headerPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;private_key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;headerPayload&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Get a repository installation ID for the authenticated app
 *
 * @param {string} repo - GitHub Repository name (e.g., 'giselles-ai/giselle')
 * @param {string} jwt
 * @return {string}
 * @see https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getGitHubAppInstallationID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UrlFetchApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.github.com/repos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/installation`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.github+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-GitHub-Api-Version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2022-11-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContentText&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;After pasting, update the &lt;code&gt;COLLECTION_TARGETS&lt;/code&gt; constant in main.gs with your information:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COLLECTION_TARGETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;giselles-ai/giselle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;giselle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewsDateColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clonesDateColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;liam-hq/liam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;liam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewsDateColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clonesDateColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn16hmd46rf25ht1nu4sx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn16hmd46rf25ht1nu4sx.png" alt="Relationship between COLLECTION_TARGETS and Google Sheet" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuring Script Properties in GAS
&lt;/h2&gt;

&lt;p&gt;Click "⚙️Project Settings" on the GAS sidebar to configure script properties.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  GITHUB_APP_ID
&lt;/h3&gt;

&lt;p&gt;Add a script property named &lt;code&gt;GITHUB_APP_ID&lt;/code&gt; and set its value to the previously noted the App ID.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  GITHUB_APP_PRIVATE_KEY
&lt;/h3&gt;

&lt;p&gt;Convert the downloaded Private Key from &lt;code&gt;PKCS#1&lt;/code&gt; to &lt;code&gt;PKCS#8&lt;/code&gt; format, as required by GAS. Replace &lt;code&gt;GITHUB.PRIVATE-KEY.pem&lt;/code&gt; and &lt;code&gt;GAS.PRIVATE-KEY.pem&lt;/code&gt; with your file name.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl pkcs8 &lt;span class="nt"&gt;-topk8&lt;/span&gt; &lt;span class="nt"&gt;-inform&lt;/span&gt; PEM &lt;span class="nt"&gt;-outform&lt;/span&gt; PEM &lt;span class="nt"&gt;-in&lt;/span&gt; GITHUB.PRIVATE-KEY.pem &lt;span class="nt"&gt;-out&lt;/span&gt; GAS.PRIVATE-KEY.pem &lt;span class="nt"&gt;-nocrypt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Next, temporarily create the following code in GAS.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TMP_PRIVATE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
Paste the contents of GAS.PRIVATE-KEY.pem here
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;PropertiesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptProperties&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GITHUB_APP_PRIVATE_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TMP_PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And select the &lt;code&gt;setKey&lt;/code&gt; function from the menu and run it. The script property &lt;code&gt;GITHUB_APP_PRIVATE_KEY&lt;/code&gt; should be created. Once created, delete the above code.&lt;/p&gt;

&lt;p&gt;💡 Note: If you set the property via "⚙️Project Settings", an &lt;code&gt;Exception: Invalid argument: key&lt;/code&gt; error occurs when running GAS. It seems to be a problem with how GAS handles line break codes. And it seems that you need to reconfigure it even if you change other script properties.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating a Trigger for Scheduled Execution
&lt;/h2&gt;

&lt;p&gt;Click "🕓Triggers" on the GAS sidebar to set up a scheduled trigger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose which function to run

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Select event source

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Time-driven&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Select type of time based trigger

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Day timer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Select time of day

&lt;ul&gt;
&lt;li&gt;e.g., &lt;code&gt;9am to 10am&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Failure notification settings

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Notify me immediately&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this setup, your spreadsheet will automatically update once a day. Errors will be notified you via email.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This post introduced a method to store &lt;code&gt;Views&lt;/code&gt;, &lt;code&gt;Unique Views&lt;/code&gt;, &lt;code&gt;Clones&lt;/code&gt;, and &lt;code&gt;Unique Clones&lt;/code&gt; with limited retention periods into Google Sheets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Centralized management of Traffic data for multiple repositories&lt;/li&gt;
&lt;li&gt;Store of data beyond two weeks&lt;/li&gt;
&lt;li&gt;Reduced operational burden through automation&lt;/li&gt;
&lt;li&gt;Secure authentication via a GitHub App&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you find this helpful.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Supplement: About Giselle and Liam as OSS
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;giselles-ai/giselle&lt;/a&gt; repository mentioned in the &lt;code&gt;Creating a Google Sheet&lt;/code&gt; is for Giselle, a platform that enables no-code creation of agents and workflows utilizing generative AI. Detailed information can be found on the Giselle service website: &lt;a href="https://giselles.ai/" rel="noopener noreferrer"&gt;https://giselles.ai/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;liam-hq/liam&lt;/a&gt; repository is for Liam, a tool that effortlessly generates beautiful and easy-to-read ER diagrams. Detailed information can be found on the Liam service website: &lt;a href="https://liambx.com/" rel="noopener noreferrer"&gt;https://liambx.com/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Update (March 21, 2025): Now Available on GitHub Repository
&lt;/h2&gt;

&lt;p&gt;The GAS introduced here is now available on the GitHub repository &lt;a href="https://github.com/route06inc/ospo-google-apps-script" rel="noopener noreferrer"&gt;https://github.com/route06inc/ospo-google-apps-script&lt;/a&gt;, along with the following article:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/route06" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9867%2Fc09ac11b-bf4c-44c7-b2bb-470d71c87e5d.jpg" alt="ROUTE06, Inc." width="800" height="800"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg" alt="" width="460" height="460"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/route06/published-an-oss-repository-that-continuously-stores-github-repository-metrics-using-gas-4f0g" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Published an OSS Repository That Continuously Stores GitHub Repository Metrics Using GAS&lt;/h2&gt;
      &lt;h3&gt;Takashi Masuda for ROUTE06, Inc. ・ Mar 21&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#ospo&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#github&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#googlesheets&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#googleappsscript&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



</description>
      <category>github</category>
      <category>googlesheets</category>
      <category>googleappsscript</category>
    </item>
    <item>
      <title>Consideration of OSS Activity Metrics Based on GitHub Repository Data</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Mon, 16 Dec 2024 03:18:02 +0000</pubDate>
      <link>https://dev.to/route06/consideration-of-oss-activity-metrics-based-on-github-repository-data-247i</link>
      <guid>https://dev.to/route06/consideration-of-oss-activity-metrics-based-on-github-repository-data-247i</guid>
      <description>&lt;p&gt;Hey Devs!&lt;/p&gt;

&lt;p&gt;Recently at ROUTE06, Inc. we published two projects, Giselle and Liam, as OSS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;https://github.com/giselles-ai/giselle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;https://github.com/liam-hq/liam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we move forward with our OSS activities, it is important to define and measure key metrics in order to understand project growth, user engagement, and identify areas for improvement.&lt;/p&gt;

&lt;p&gt;This article will explore metrics used to measure results in OSS marketing, how they correlate, and the range of metrics that can be obtained.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Type of Metrics

&lt;ul&gt;
&lt;li&gt;1. Awareness&lt;/li&gt;
&lt;li&gt;2. Interest&lt;/li&gt;
&lt;li&gt;3. Consideration&lt;/li&gt;
&lt;li&gt;4. Action&lt;/li&gt;
&lt;li&gt;5. Adoption&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Funnel structure and close but different correlations&lt;/li&gt;

&lt;li&gt;

Range of metrics that can be obtained

&lt;ul&gt;
&lt;li&gt;1. Awareness&lt;/li&gt;
&lt;li&gt;2. Interest&lt;/li&gt;
&lt;li&gt;3. Consideration&lt;/li&gt;
&lt;li&gt;4. Action&lt;/li&gt;
&lt;li&gt;5. Adoption&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;li&gt;Supplement: About Giselle and Liam as OSS&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Type of Metrics
&lt;/h2&gt;

&lt;p&gt;We have identified the following five categories of metrics for evaluating the success of OSS marketing efforts on GitHub. Through these metrics, we will track user behavior from project awareness to usage and determine the effectiveness of our marketing efforts.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Awareness
&lt;/h3&gt;

&lt;p&gt;These metrics are used to determine how well a project is perceived by its users.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Views

&lt;ul&gt;
&lt;li&gt;The total number of visits to the repository, counted as multiple visits by a single user&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Unique views

&lt;ul&gt;
&lt;li&gt;The total number of unique visits to the repository, counted as one visit, even if one user visits multiple times&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Interest
&lt;/h3&gt;

&lt;p&gt;These metrics show the number of users interested in the project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stars

&lt;ul&gt;
&lt;li&gt;The total number of stars users have given the repository. Each user can give only one star&lt;/li&gt;
&lt;li&gt;The number of prospective users, indicating support for and interest in the repository&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Watchers

&lt;ul&gt;
&lt;li&gt;The total number of users watching the repository&lt;/li&gt;
&lt;li&gt;The number of prospective users interested in repository updates&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Consideration
&lt;/h3&gt;

&lt;p&gt;This metrics represents users who are specifically considering using or participating in the project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clones

&lt;ul&gt;
&lt;li&gt;The total number of times users have cloned the repository&lt;/li&gt;
&lt;li&gt;The number of prospective users who will review and use the contents of the repository in detail&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Forks

&lt;ul&gt;
&lt;li&gt;The total number of repositories forked by users on GitHub&lt;/li&gt;
&lt;li&gt;The number of prospective users who will make their own changes to the repository, create pull requests, learn, and use for reference&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Action
&lt;/h3&gt;

&lt;p&gt;These metrics are indicators of actual project participation and contribution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Issues by outside collaborators

&lt;ul&gt;
&lt;li&gt;The total number of prospective users who would like to contribute to the improvement and development of the repository by reporting bugs, requesting new features, etc.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Pull Requests by outside collaborators

&lt;ul&gt;
&lt;li&gt;The final step of the action phase. The number of prospective users who are highly motivated to grow and improve the quality of the repository and have the technical skills and knowledge to do so&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Adoption
&lt;/h3&gt;

&lt;p&gt;This metric shows the actual usage of the project. Only for projects such as npm packages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of repository downloads

&lt;ul&gt;
&lt;li&gt;The total number of potential users actually using the repository&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Funnel structure and close but different correlations
&lt;/h2&gt;

&lt;p&gt;The above indicators represent user behavior from awareness to usage, a flow similar to a funnel structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Awareness
↓
Interest
↓
Consideration
↓
Action
↓
Adoption
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, due to the nature of OSS projects, they have a different correlation to the general marketing funnel.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The progression of stages is not linear

&lt;ul&gt;
&lt;li&gt;Users do not necessarily progress from awareness to usage in a sequential manner. For example, some users may download and use a product immediately after learning about it in the awareness phase, and sometimes they may skip steps in the funnel&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Cyclical relationships

&lt;ul&gt;
&lt;li&gt;OSS projects are more cyclical than traditional funnel structures, as users may return to the interest and action phases after using the project. In many cases, users provide feedback to the project and add new stars, so there is a reciprocal relationship&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Interdependence of indicators

&lt;ul&gt;
&lt;li&gt;The indicators at each stage are interrelated. For example, as usage increases so do awareness and interest. Therefore, in OSS marketing, it is necessary to be aware not only of fluctuations in individual indicators, but also of the overall balance&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Range of metrics that can be obtained
&lt;/h2&gt;

&lt;p&gt;There are some limitations to the metrics that can be obtained on GitHub to measure the marketing performance of OSS projects.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Awareness
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Views

&lt;ul&gt;
&lt;li&gt;Can be obtained via the &lt;code&gt;count&lt;/code&gt; property of the REST API &lt;a href="https://docs.github.com/en/rest/metrics/traffic?apiVersion=2022-11-28#get-page-views" rel="noopener noreferrer"&gt;Get page views&lt;/a&gt; (Only available for the last 2 weeks)&lt;/li&gt;
&lt;li&gt;Similar figures are available from &lt;code&gt;Traffic&lt;/code&gt; in the repository &lt;code&gt;Insights&lt;/code&gt; tab&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Unique views

&lt;ul&gt;
&lt;li&gt;Can be obtained via the &lt;code&gt;uniques&lt;/code&gt; property of the same REST API (Only available for the last 2 weeks)&lt;/li&gt;
&lt;li&gt;Similar figures are available from &lt;code&gt;Traffic&lt;/code&gt; in the repository &lt;code&gt;Insights&lt;/code&gt; tab&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;💡 Visits by you and Organization members also appears to be counted.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/22839597/does-the-github-traffic-graph-include-your-own-views" rel="noopener noreferrer"&gt;Does the GitHub traffic graph include your own views? - Stack Overflow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Interest
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Stars

&lt;ul&gt;
&lt;li&gt;Can be obtained via the REST API &lt;a href="https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#list-stargazers" rel="noopener noreferrer"&gt;List stargazers&lt;/a&gt;. It is also possible to obtain who starred and when for the entire period of time.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Watchers

&lt;ul&gt;
&lt;li&gt;Current Watchers can be obtained via the REST API &lt;a href="https://docs.github.com/en/rest/activity/watching?apiVersion=2022-11-28#list-watchers" rel="noopener noreferrer"&gt;List watchers&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Consideration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Clones

&lt;ul&gt;
&lt;li&gt;Can be obtained via the &lt;code&gt;count&lt;/code&gt; property of the REST API &lt;a href="https://docs.github.com/en/rest/metrics/traffic?apiVersion=2022-11-28#get-repository-clones" rel="noopener noreferrer"&gt;Get repository clones&lt;/a&gt;. The number of unique clones can also be obtained from the &lt;code&gt;uniques&lt;/code&gt; property. (Both are only available for the last 2 weeks)&lt;/li&gt;
&lt;li&gt;Similar figures are available from &lt;code&gt;Traffic&lt;/code&gt; in the repository &lt;code&gt;Insights&lt;/code&gt; tab&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Forks

&lt;ul&gt;
&lt;li&gt;Can be obtained via the REST API &lt;a href="https://docs.github.com/en/rest/repos/forks?apiVersion=2022-11-28#list-forks" rel="noopener noreferrer"&gt;List forks&lt;/a&gt;. All forks can be obtained for the entire period, including when and by whom they were forked&lt;/li&gt;
&lt;li&gt;Similar figures are available from &lt;code&gt;Forks&lt;/code&gt; in the repository &lt;code&gt;Insights&lt;/code&gt; tab&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;💡 For Clones, the number of actions/checkouts, etc. executed in GitHub Actions also appear to be included.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/orgs/community/discussions/23806" rel="noopener noreferrer"&gt;Do GitHub Actions contribute to a repo's Traffic stats under Insights? · community · Discussion #23806&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Action
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Issues by outside collaborators

&lt;ul&gt;
&lt;li&gt;Can be obtained via the REST API &lt;a href="https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues" rel="noopener noreferrer"&gt;List repository issues&lt;/a&gt;. You can also obtain who created the issue and when for the entire time period&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Pull Requests by outside collaborators

&lt;ul&gt;
&lt;li&gt;Can be obtained via the REST API &lt;a href="https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests" rel="noopener noreferrer"&gt;List pull requests&lt;/a&gt;. Pull requests can also be obtained for the entire period, including who made the pull request and when&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Adoption
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Number of project downloads

&lt;ul&gt;
&lt;li&gt;This metric is difficult to track on GitHub, so download counts from package management systems (e.g. npm and PyPI) are commonly used as a reference&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We have looked at various indicators of OSS activity from GitHub repository data.&lt;/p&gt;

&lt;p&gt;As mentioned above, Views, Unique views, and Clones are only available for the last two weeks. In the next issue, we will show how we solved this problem.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Supplement: About Giselle and Liam as OSS
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;giselles-ai/giselle&lt;/a&gt; repository recently published as OSS is for Giselle, a platform that enables no-code creation of agents and workflows utilizing generative AI. Detailed information can be found on the Giselle service website: &lt;a href="https://giselles.ai/" rel="noopener noreferrer"&gt;https://giselles.ai/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;liam-hq/liam&lt;/a&gt; repository is for Liam, a tool that effortlessly generates beautiful and easy-to-read ER diagrams. Detailed information can be found on the Liam service website: &lt;a href="https://liambx.com/" rel="noopener noreferrer"&gt;https://liambx.com/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ospo</category>
      <category>opensource</category>
      <category>github</category>
      <category>marketing</category>
    </item>
    <item>
      <title>What Is security.txt and How Can It Help Improve Website Security?</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Mon, 09 Dec 2024 01:55:28 +0000</pubDate>
      <link>https://dev.to/route06/what-is-securitytxt-and-how-can-it-help-improve-website-security-3kij</link>
      <guid>https://dev.to/route06/what-is-securitytxt-and-how-can-it-help-improve-website-security-3kij</guid>
      <description>&lt;p&gt;Hey Devs!&lt;/p&gt;

&lt;p&gt;While working on open-sourcing our product, I came across &lt;code&gt;security.txt&lt;/code&gt;. Here is a brief introduction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overview of security.txt&lt;/li&gt;
&lt;li&gt;Format of security.txt&lt;/li&gt;
&lt;li&gt;ROUTE06, Inc.'s security.txt file template&lt;/li&gt;
&lt;li&gt;ROUTE06, Inc.'s example&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;
Appendix: Examples from Other Companies

&lt;ul&gt;
&lt;li&gt;Supabase&lt;/li&gt;
&lt;li&gt;Others&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of security.txt
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;security.txt&lt;/code&gt; is a standardized text file that provides security-related information for websites. Defined in &lt;a href="https://www.rfc-editor.org/rfc/rfc9116" rel="noopener noreferrer"&gt;RFC 9116&lt;/a&gt; published in April 2022, it is placed under &lt;code&gt;/.well-known/security.txt&lt;/code&gt; on a website.&lt;/p&gt;

&lt;p&gt;By implementing &lt;code&gt;security.txt&lt;/code&gt;, you can enjoy the following benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security researchers and ethical hackers can easily find contact details and guidelines to report security issues on your website&lt;/li&gt;
&lt;li&gt;Organizations can efficiently receive and address reports of security vulnerabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without &lt;code&gt;security.txt&lt;/code&gt; or clear contact information, discovered security issues might go unreported.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Format of security.txt
&lt;/h2&gt;

&lt;p&gt;You can include up to eight types of fields in the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Required
Contact:  # URL or email address for reporting. If multiple, list Contact fields in order of priority.
Expires:  # Expiration date and time. To prevent `security.txt` from becoming outdated, an expiry date within a year is recommended.

# Optional
Preferred-Languages:  # Supported languages, multiple allowed.
Policy:               # URL of the security policy page.
Acknowledgements:     # URL of the page acknowledging past reporters.
Hiring:               # URL for security-related job openings.
Canonical:            # URL of this `security.txt`.
Encryption:           # Location of encryption keys, such as PGP keys, for secure communication.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is an example. You can also generate one at &lt;a href="https://securitytxt.org/" rel="noopener noreferrer"&gt;https://securitytxt.org/&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Required
Contact: mailto:security@example.com
Expires: 2025-11-30T23:59:59Z

# Optional
Preferred-Languages: en, ja
Policy: https://example.com/security-policy.html
Acknowledgements: https://example.com/hall-of-fame.html
Hiring: https://example.com/security-jobs.html
Canonical: https://example.com/.well-known/security.txt
Encryption: https://example.com/pgp-key.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ROUTE06, Inc.'s security.txt file template
&lt;/h2&gt;

&lt;p&gt;At ROUTE06, Inc., we use the following template for security.txt files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contact:  # Provide the security contact email in mailto: format.
Expires:  # Specify a date within one year and update regularly.

Preferred-Languages: en, ja
Policy:     # Specify the URL of https://github.com/{org}/{repo}/security/policy or the security policy page.
Canonical:  # Specify the URL of this security.txt.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡 To prevent forgetting to update the Expires field or other fields, we use &lt;a href="https://github.com/route06/actions/blob/main/.github/workflows/create_gh_issue.yml" rel="noopener noreferrer"&gt;route06/actions/.github/workflows/create_gh_issue.yml&lt;/a&gt; to automatically create update issues periodically.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Example update issue: &lt;a href="https://github.com/giselles-ai/giselle/issues/146" rel="noopener noreferrer"&gt;[Action Required] Update security.txt - 2024/12 Maintenance · Issue #146 · giselles-ai/giselle&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ROUTE06, Inc.'s example
&lt;/h2&gt;

&lt;p&gt;Here is an example from our service, Giselle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://studio.giselles.ai/.well-known/security.txt" rel="noopener noreferrer"&gt;https://studio.giselles.ai/.well-known/security.txt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Giselle is a SaaS platform that enables no-code creation of agents and workflows leveraging generative AI. Visit the service site at &lt;a href="https://giselles.ai/" rel="noopener noreferrer"&gt;https://giselles.ai/&lt;/a&gt; for more details. The OSS repository is also available at &lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;giselles-ai/giselle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Since security.txt is just a text file, it is easy to create, and serves as a reliable point of contact for vulnerability reporters.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: Examples from Other Companies
&lt;/h2&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Supabase
&lt;/h3&gt;

&lt;p&gt;The security.txt file for &lt;a href="https://supabase.com" rel="noopener noreferrer"&gt;https://supabase.com&lt;/a&gt; doubles as a GitHub SECURITY.md.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://supabase.com/.well-known/security.txt" rel="noopener noreferrer"&gt;https://supabase.com/.well-known/security.txt&lt;/a&gt; is available&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/supabase/supabase/blob/v1.24.09/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt; in the supabase/supabase repository is a symbolic link to &lt;a href="https://github.com/supabase/supabase/blob/v1.24.09/apps/docs/public/.well-known/security.txt" rel="noopener noreferrer"&gt;apps/docs/public/.well-known/security.txt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;apps/docs/public/.well-known/security.txt is deployed as &lt;a href="https://supabase.com/.well-known/security.txt" rel="noopener noreferrer"&gt;https://supabase.com/.well-known/security.txt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Since SECURITY.md is committed in the repository, it is also accessible from the &lt;a href="https://github.com/supabase/supabase/security" rel="noopener noreferrer"&gt;Security&lt;/a&gt; tab&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Others
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.google.com/search?q=inurl:.well-known/security.txt&amp;amp;gl=us&amp;amp;hl=en&amp;amp;gws_rd=cr&amp;amp;pws=0" rel="noopener noreferrer"&gt;security.txt for various sites - Google Search&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
    </item>
    <item>
      <title>How We Started Continuous OSS License Monitoring with License Finder</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Thu, 28 Nov 2024 02:16:40 +0000</pubDate>
      <link>https://dev.to/route06/how-we-started-continuous-oss-license-monitoring-with-license-finder-a9i</link>
      <guid>https://dev.to/route06/how-we-started-continuous-oss-license-monitoring-with-license-finder-a9i</guid>
      <description>&lt;p&gt;Hey Devs!&lt;/p&gt;

&lt;p&gt;Recently, some products in our company have started the process of becoming OSS (Open Source Software). In OSS projects, conflicts between the licenses of utilized packages and the project's license can pose a risk.&lt;/p&gt;

&lt;p&gt;Today, I'd like to share how we began addressing this risk and monitoring it continuously by introducing a tool called License Finder.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Introduction to License Finder

&lt;ul&gt;
&lt;li&gt;Installation&lt;/li&gt;
&lt;li&gt;Usage&lt;/li&gt;
&lt;li&gt;Approving Dependencies&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Introducing License Finder to CI

&lt;ul&gt;
&lt;li&gt;Example Integration with GitHub Actions&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;li&gt;Supplement: About Giselle and Liam, the Repositories Mentioned in the Integration Example&lt;/li&gt;

&lt;li&gt;

Appendix: How ROUTE06, Inc. Handles It

&lt;ul&gt;
&lt;li&gt;How to Handle License Finder Failures in CI&lt;/li&gt;
&lt;li&gt;
Response Flow

&lt;ul&gt;
&lt;li&gt;Case 1: The licenses are compatible&lt;/li&gt;
&lt;li&gt;Case 2: The licenses are not compatible&lt;/li&gt;
&lt;li&gt;Case 3: The license is unknown&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Supplement: License Finder Supports Composite Licenses with &lt;code&gt;AND&lt;/code&gt; or &lt;code&gt;OR&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to License Finder
&lt;/h2&gt;

&lt;p&gt;License Finder is a Ruby-based CLI tool for license checking, developed by Pivotal (now VMware).&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pivotal" rel="noopener noreferrer"&gt;
        pivotal
      &lt;/a&gt; / &lt;a href="https://github.com/pivotal/LicenseFinder" rel="noopener noreferrer"&gt;
        LicenseFinder
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Find licenses for your project's dependencies.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;It integrates with package managers to discover dependencies and identify the licenses of each package. It supports bundler, npm, pnpm, and more.&lt;/p&gt;

&lt;p&gt;The identified licenses are compared against a list of licenses approved by the user, and the results are outputted. It can also be integrated into CI pipelines.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;Install License Finder with Ruby 2.6.0 or later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;license_finder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;Install dependency packages using tools like &lt;code&gt;bundle install&lt;/code&gt; or &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then simply run License Finder.&lt;/p&gt;

&lt;p&gt;💡 The package manager is automatically detected. If you want to configure it manually, you can do so by editing the &lt;code&gt;config/license_finder.yml&lt;/code&gt; file. For details, see &lt;a href="https://github.com/pivotal/LicenseFinder#saving-configuration" rel="noopener noreferrer"&gt;Saving Configuration in the README.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When executed, all dependency packages are output with their license names. Initially, all packages are unapproved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;license_finder
&lt;span class="go"&gt;LicenseFinder::Bundler: is active for '/Users/masutaka/work'

Dependencies that need approval:
actioncable, 7.2.1, MIT
actionmailbox, 7.2.1, MIT
(snip)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Approving Dependencies
&lt;/h3&gt;

&lt;p&gt;The goal is to have zero unapproved dependencies.&lt;/p&gt;

&lt;p&gt;Typically, you approve licenses as a whole, and if needed, approve or ignore individual packages. Here are some examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Permit MIT and Apache-2.0 licenses
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;license_finder permitted_licenses add &lt;span class="s2"&gt;"MIT"&lt;/span&gt; &lt;span class="s2"&gt;"Apache 2.0"&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Specify &lt;span class="s2"&gt;"who"&lt;/span&gt; and &lt;span class="s2"&gt;"why"&lt;/span&gt; when permitting a license
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;license_finder permitted_licenses add &lt;span class="s2"&gt;"Simplified BSD"&lt;/span&gt; &lt;span class="nt"&gt;--who&lt;/span&gt; CTO &lt;span class="nt"&gt;--why&lt;/span&gt; &lt;span class="s2"&gt;"Go ahead"&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Approve the package awesome_gpl_gem
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;license_finder approvals add awesome_gpl_gem
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Ignore the package awesome_ignore_gem
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;license_finder ignored_dependencies add awesome_ignore_gem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, the decisions are saved in &lt;code&gt;doc/dependency_decisions.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing License Finder to CI
&lt;/h2&gt;

&lt;p&gt;When there are no unapproved packages, License Finder returns an exit code of &lt;code&gt;0&lt;/code&gt;. Using this feature, it can be incorporated into CI (GitHub Actions).&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Integration with GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Here’s examples of integration in Giselle and Liam.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/giselles-ai/giselle/blob/v0.3.1/.github/workflows/license.yml" rel="noopener noreferrer"&gt;github.com/giselles-ai/giselle/.github/workflows/license.yml&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;The package management tool is bun&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/liam-hq/liam/blob/80731383c946469a6f25a890e9fbea85cc049254/.github/workflows/license.yml" rel="noopener noreferrer"&gt;github.com/liam-hq/liam/.github/workflows/license.yml&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;The package management tool is pnpm&lt;/li&gt;
&lt;li&gt;Use merge queue&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;To update the license report, we created a GitHub App&lt;sup id="fnref1"&gt;1&lt;/sup&gt; with the following permissions and installed it in the repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contents: Read and write&lt;/li&gt;
&lt;li&gt;Metadata: Read-only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; environment variable, we used a GitHub App Token for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The license_finder job is a required check before merging pull requests&lt;/li&gt;
&lt;li&gt;Even after committing via GitHub Actions, the license_finder job should still execute for the newly created commit&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is used for commits, they are considered bot operations, and CI is not triggered&lt;/li&gt;
&lt;li&gt;Personal Access Tokens have long lifetimes, so we avoid using them whenever possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you’re considering an in-house solution for monitoring OSS licenses, License Finder could be an option. However, without sufficient knowledge of licenses, there’s a risk of mistakenly approving dependencies (see appendix). Another concern is that development on License Finder is not very active.&lt;/p&gt;

&lt;p&gt;For now, we plan to continue using License Finder while keeping SaaS options like &lt;a href="https://fossa.com/" rel="noopener noreferrer"&gt;FOSSA&lt;/a&gt; in mind.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Supplement: About Giselle and Liam, the Repositories Mentioned in the Integration Example
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/giselles-ai/giselle" rel="noopener noreferrer"&gt;giselles-ai/giselle&lt;/a&gt; repository mentioned in the earlier implementation example is for Giselle, a platform that enables no-code creation of agents and workflows utilizing generative AI. Detailed information can be found on the Giselle service website: &lt;a href="https://giselles.ai/" rel="noopener noreferrer"&gt;https://giselles.ai/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;liam-hq/liam&lt;/a&gt; repository is for Liam, a tool that effortlessly generates beautiful and easy-to-read ER diagrams. Detailed information can be found on the Liam service website: &lt;a href="https://liambx.com/" rel="noopener noreferrer"&gt;https://liambx.com/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: How ROUTE06, Inc. Handles It
&lt;/h2&gt;

&lt;p&gt;Here's how we handle cases where License Finder fails in CI.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Handle License Finder Failures in CI
&lt;/h3&gt;

&lt;p&gt;License Finder fails when a package with a license not permitted in the &lt;a href="https://github.com/pivotal/LicenseFinder?tab=readme-ov-file#decisions-file" rel="noopener noreferrer"&gt;decisions file&lt;/a&gt; is added.&lt;/p&gt;

&lt;p&gt;Below is an example of CI failing because the npm package &lt;a href="https://www.npmjs.com/package/mdn-data" rel="noopener noreferrer"&gt;mdn-data&lt;/a&gt; uses the &lt;code&gt;CC0 1.0 Universal&lt;/code&gt; license, which is not permitted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki5sbhus7u938iqud2yj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki5sbhus7u938iqud2yj.png" alt="Example of a License Finder Failure" width="600" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please follow the response flow outlined below.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Response Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Check the license of the relevant package&lt;/li&gt;
&lt;li&gt;Confirm that the identified license is compatible with the license of the OSS repository in question&lt;/li&gt;
&lt;li&gt;Take action based on the following cases:

&lt;ul&gt;
&lt;li&gt;Case 1: The licenses are compatible

&lt;ul&gt;
&lt;li&gt;Example: The repository is licensed under Apache-2.0, and the package uses the Zlib license&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Case 2: The licenses are not compatible

&lt;ul&gt;
&lt;li&gt;Example: The repository is licensed under Apache-2.0, and the package uses GPL-3.0-only&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Case 3: The license is unknown&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Case 1: The licenses are compatible
&lt;/h4&gt;

&lt;p&gt;If the licenses are compatible, allow the license in License Finder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="c"&gt;# Example of permitting the Zlib license&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;license_finder permitted_licenses add &lt;span class="s1"&gt;'Zlib'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt; --why 'Compatible with Apache-2.0 license. See https://opensource.org/license/Zlib' \
 --who 'OSPO @masutaka'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[Recommended] Use the &lt;code&gt;--why&lt;/code&gt; option to specify the reason for permitting the license. Including the URL of the license can be helpful&lt;/li&gt;
&lt;li&gt;[Optional] Use the &lt;code&gt;--who&lt;/code&gt; option to specify who granted the permission&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Case 2: The licenses are not compatible
&lt;/h4&gt;

&lt;p&gt;Copyleft licenses such as GPL, LGPL, AGPL, or EPL may not be compatible with &lt;a href="https://en.wikipedia.org/wiki/Permissive_software_license" rel="noopener noreferrer"&gt;permissive licenses&lt;/a&gt; like MIT or Apache-2.0.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compatible cases:

&lt;ul&gt;
&lt;li&gt;Using an Apache-2.0 package in a GPLv3 product&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Incompatible cases:

&lt;ul&gt;
&lt;li&gt;Using a GPLv3 package in an Apache-2.0 product&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;When licenses are incompatible, you must take appropriate actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exclude GPLv3 or other incompatible packages from the product&lt;/li&gt;
&lt;li&gt;Change the entire product license to GPLv3 or similar&lt;/li&gt;
&lt;li&gt;Separate the incompatible package and use it independently&lt;/li&gt;
&lt;li&gt;Seek legal advice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Case 3: The license is unknown
&lt;/h4&gt;

&lt;p&gt;If the package registry or Git repository does not specify the license, contact the provider.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Example: The license for the npm package &lt;a href="https://www.npmjs.com/package/@vercel/flags" rel="noopener noreferrer"&gt;@vercel/flags&lt;/a&gt; was unknown

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/giselles-ai/giselle/issues/12" rel="noopener noreferrer"&gt;https://github.com/giselles-ai/giselle/issues/12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Depending on the situation, if it seems acceptable, create an issue and approve the package temporarily.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;license_finder approvals add &lt;span class="s1"&gt;'@vercel/flags'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt; --why 'The license is none. Check https://github.com/giselles-ai/giselle/issues/12 later'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the license is identified, remove the approval and re-run License Finder. If it fails again, follow the previously mentioned response flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;license_finder approvals remove &lt;span class="s1"&gt;'@vercel/flags'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt; --why 'It was released as OSS under the MIT license.'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the issue seems problematic, consider alternatives such as using a different package or temporarily holding off on merging into the main branch.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Supplement: License Finder Supports Composite Licenses with &lt;code&gt;AND&lt;/code&gt; or &lt;code&gt;OR&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;There are cases of composite licenses like &lt;code&gt;(MIT AND Zlib)&lt;/code&gt; for the npm package &lt;a href="https://www.npmjs.com/package/pako" rel="noopener noreferrer"&gt;pako&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pivotal/LicenseFinder/blob/v7.2.1/spec/lib/license_finder/decision_applier_spec.rb#L246-L311" rel="noopener noreferrer"&gt;License Finder supports &lt;code&gt;AND&lt;/code&gt; and &lt;code&gt;OR&lt;/code&gt;, so in such cases&lt;/a&gt;, you only need to permit &lt;code&gt;MIT&lt;/code&gt; and &lt;code&gt;Zlib&lt;/code&gt; individually. There is no need to permit &lt;code&gt;(MIT AND Zlib)&lt;/code&gt; explicitly.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://docs.github.com/en/apps/creating-github-apps" rel="noopener noreferrer"&gt;Creating GitHub Apps - GitHub Docs&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>opensource</category>
      <category>license</category>
      <category>ci</category>
      <category>githubactions</category>
    </item>
  </channel>
</rss>
