[{"data":1,"prerenderedAt":893},["ShallowReactive",2],{"$navigation":3,"$settings":40,"articles/how-i-replaced-the-deprecated-git-up-gem-with-a-na":73,"$latestArticles":275},{"id":4,"uid":5,"url":5,"type":6,"href":7,"tags":8,"first_publication_date":9,"last_publication_date":10,"slugs":11,"linked_documents":12,"lang":13,"alternate_languages":14,"data":15},"Z60ahxIAACUARzT5",null,"navigation","https://antoninpleskac.cdn.prismic.io/api/v2/documents/search?ref=ajDlVxEAAHypkWW4&q=%5B%5B%3Ad+%3D+at%28document.id%2C+%22Z60ahxIAACUARzT5%22%29+%5D%5D",[],"2025-02-12T22:02:49+0000","2025-02-12T22:03:20+0000",[6],[],"en-us",[],{"homepageLabel":16,"links":22},[17],{"type":18,"text":19,"spans":20,"direction":21},"heading3","Articles",[],"ltr",[23],{"label":24,"link":28},[25],{"type":18,"text":26,"spans":27,"direction":21},"About ",[],{"id":29,"type":30,"tags":31,"lang":13,"slug":32,"first_publication_date":33,"last_publication_date":34,"uid":35,"url":36,"link_type":37,"key":38,"isBroken":39},"Z60aXhIAACUARzS6","page",[],"about-antonin","2025-02-12T22:02:08+0000","2025-02-12T22:09:51+0000","about-me","/about-me","Document","bf201bad-6e98-4261-ac1c-d25ae140dea0",false,{"id":41,"uid":5,"url":5,"type":42,"href":43,"tags":44,"first_publication_date":45,"last_publication_date":46,"slugs":47,"linked_documents":48,"lang":13,"alternate_languages":49,"data":50},"Z60ZTRIAACgARzM5","settings","https://antoninpleskac.cdn.prismic.io/api/v2/documents/search?ref=ajDlVxEAAHypkWW4&q=%5B%5B%3Ad+%3D+at%28document.id%2C+%22Z60ZTRIAACgARzM5%22%29+%5D%5D",[],"2025-02-12T21:57:50+0000","2025-02-12T22:00:22+0000",[42],[],[],{"name":51,"description":56,"profilePicture":61,"newsletterDescription":71,"newsletterDisclaimer":72},[52],{"type":53,"text":54,"spans":55,"direction":21},"heading1","Antoín Pleskač",[],[57],{"type":58,"text":59,"spans":60,"direction":21},"paragraph","Programmer exploring Vue, Rails, and PostgreSQL development, along with insights into 3D printing....",[],{"dimensions":62,"alt":64,"copyright":5,"url":65,"id":66,"edit":67},{"width":63,"height":63},2000,"Antonin","https://images.prismic.io/antoninpleskac/Z60Z6ZbqstJ9-jF8_DALL%C2%B7E2025-02-1222.59.40-Createabrightandpositivepixelartavatarbasedonagivenphoto%2Cfeaturingaprogrammeraesthetic.Theavatarshouldhavearetrogamingstylewi.webp?auto=format,compress&rect=0,0,1024,1024&w=2000&h=2000","Z60Z6ZbqstJ9-jF8",{"x":68,"y":68,"zoom":69,"background":70},0,1,"transparent",[],[],{"id":74,"uid":75,"url":76,"type":77,"href":78,"tags":79,"first_publication_date":80,"last_publication_date":80,"slugs":81,"linked_documents":83,"lang":13,"alternate_languages":84,"data":85},"ajDYvhEAAHypkVKW","how-i-replaced-the-deprecated-git-up-gem-with-a-na","/articles/how-i-replaced-the-deprecated-git-up-gem-with-a-na","article","https://antoninpleskac.cdn.prismic.io/api/v2/documents/search?ref=ajDlVxEAAHypkWW4&q=%5B%5B%3Ad+%3D+at%28document.id%2C+%22ajDYvhEAAHypkVKW%22%29+%5D%5D",[],"2026-06-16T05:55:35+0000",[82],"how-i-replaced-the-deprecated-git-up-gem-with-a-native-git-alias",[],[],{"title":86,"publishDate":5,"featuredImage":90,"slices":97},[87],{"type":53,"text":88,"spans":89,"direction":21},"How I Replaced the Deprecated git-up Gem With a Native Git Alias",[],{"dimensions":91,"alt":88,"copyright":5,"url":93,"id":94,"edit":95},{"width":63,"height":92},1500,"https://images.prismic.io/antoninpleskac/ajDlJY1P9HI4UjH7_ChatGPTImageJun16%2C2026%2C07_54_30AM.png?auto=format,compress&rect=85,0,1365,1024&w=2000&h=1500","ajDlJY1P9HI4UjH7",{"x":96,"y":68,"zoom":69,"background":70},85,[98,112],{"variation":99,"version":100,"items":101,"primary":102,"id":110,"slice_type":111,"slice_label":5},"wide","sktwi1xtmkfgx8626",[],{"image":103,"caption":109},{"dimensions":104,"alt":88,"copyright":5,"url":107,"id":94,"edit":108},{"width":105,"height":106},1536,1024,"https://images.prismic.io/antoninpleskac/ajDlJY1P9HI4UjH7_ChatGPTImageJun16%2C2026%2C07_54_30AM.png?auto=format,compress",{"x":68,"y":68,"zoom":69,"background":70},[],"image$7b2f5f6d-91ac-4766-b2c3-467d01cd5195","image",{"variation":113,"version":100,"items":114,"primary":115,"id":273,"slice_type":274,"slice_label":5},"default",[],{"text":116},[117,123,134,139,142,149,155,158,164,169,172,176,179,184,190,195,199,204,213,218,223,226,229,232,235,238,241,246,251,256,260,265,270],{"type":18,"text":118,"spans":119,"direction":21},"Introduction",[120],{"start":68,"end":121,"type":122},12,"strong",{"type":58,"text":124,"spans":125,"direction":21},"When you work across several feature branches at once, keeping them all in sync with the remote is a small but constant chore. For years my answer was git-up — a tidy little Ruby gem that fetches and rebases every locally-tracked branch in one command. Then I went to set it up on a fresh machine and remembered: the gem is deprecated, and has been for a while. So what do you reach for when the tool you relied on is gone, but the problem it solved is still there?",[126],{"start":127,"end":128,"type":129,"data":130},151,157,"hyperlink",{"link_type":131,"url":132,"target":133},"Web","https://github.com/aanand/git-up","_blank",{"type":18,"text":135,"spans":136,"direction":21},"The Problem",[137],{"start":68,"end":138,"type":122},11,{"type":58,"text":140,"spans":141,"direction":21},"git pull has two long-standing annoyances that git-up originally existed to fix:",[],{"type":143,"text":144,"spans":145,"direction":21},"list-item","It merges upstream changes by default, leaving your history full of noisy merge commits instead of a clean rebase.",[146],{"start":147,"end":148,"type":122},3,9,{"type":143,"text":150,"spans":151,"direction":21},"It only updates the branch you're currently on. So the moment you switch back to master, it's stale — and git push starts complaining about branches you weren't even thinking about.",[152],{"start":153,"end":154,"type":122},34,46,{"type":58,"text":156,"spans":157,"direction":21},"The gem's own author says it best in the README: as of Git 2.0 and 2.9, the core of what git-up did is now built into git itself. git pull --rebase --autostash covers the single-branch case completely. Installing a Ruby gem (and carrying a Ruby dependency) just for this no longer makes sense.",[],{"type":58,"text":159,"spans":160,"direction":21},"But there was one part I genuinely missed: updating the branches I'm not on. Specifically, I want my local master to be current the instant I switch to it — without leaving my feature branch to go fetch it.",[161],{"start":162,"end":163,"type":122},69,72,{"type":18,"text":165,"spans":166,"direction":21},"The Solution: One Alias That Does It All",[167],{"start":68,"end":168,"type":122},40,{"type":58,"text":170,"spans":171,"direction":21},"Instead of a gem, I now define git up as a git alias. It fetches once, rebases the branch I'm on, and fast-forwards every other branch that can be fast-forwarded — master included — all without a single checkout.",[],{"type":173,"text":174,"spans":175,"direction":21},"preformatted","git config --global alias.up '!f() {\n  git fetch origin --prune\n  cur=$(git symbolic-ref --short HEAD)\n  git for-each-ref --format=\"%(refname:short) %(upstream:short)\" refs/heads |\n  while read b u; do\n    [ -z \"$u\" ] && continue                                      # no upstream → skip\n    git rev-parse --verify --quiet \"$u\" >/dev/null || continue   # upstream gone on origin → skip\n    lb=$(git rev-parse \"$b\"); lu=$(git rev-parse \"$u\")\n    [ \"$lb\" = \"$lu\" ] && continue                                # up to date → silent\n    base=$(git merge-base \"$b\" \"$u\")\n    [ \"$base\" = \"$lu\" ] && continue                              # ahead of upstream → silent\n    if [ \"$b\" = \"$cur\" ]; then\n      git pull --rebase --autostash >/dev/null 2>&1 && echo \"↻ $b (rebased)\"\n    elif [ \"$base\" = \"$lb\" ]; then\n      git fetch . \"$u:$b\" >/dev/null 2>&1 && echo \"✓ $b\"          # fast-forward, no checkout\n    else\n      echo \"⚠ $b (diverged, skipped)\"\n    fi\n  done\n}; f'\n",[],{"type":58,"text":177,"spans":178,"direction":21},"In ~/.gitconfig this lands on a single line — that's just how git config stores it. The wrapped version above is only for reading.",[],{"type":58,"text":180,"spans":181,"direction":21},"What This Alias Does",[182],{"start":68,"end":183,"type":122},20,{"type":185,"text":186,"spans":187,"direction":21},"o-list-item","Fetches origin once with --prune, so deleted remote branches stop lingering as stale tracking refs.",[188],{"start":68,"end":189,"type":122},19,{"type":185,"text":191,"spans":192,"direction":21},"Walks every local branch and looks up its upstream.",[193],{"start":68,"end":194,"type":122},24,{"type":185,"text":196,"spans":197,"direction":21},"Skips the noise silently — branches with no upstream, branches whose remote was deleted, branches already up to date, and branches ahead of their upstream.",[198],{"start":68,"end":194,"type":122},{"type":185,"text":200,"spans":201,"direction":21},"Rebases the current branch with pull --rebase --autostash, exactly like the old single-branch git up.",[202],{"start":68,"end":203,"type":122},26,{"type":185,"text":205,"spans":206,"direction":21},"Fast-forwards the rest using git fetch . origin/\u003Cbranch>:\u003Cbranch> — a neat trick that moves a local branch up to its upstream without checking it out. This is what keeps master fresh while you stay on your feature branch.",[207,209],{"start":68,"end":208,"type":122},22,{"start":210,"end":211,"type":212},126,149,"em",{"type":185,"text":214,"spans":215,"direction":21},"Reports diverged branches as ⚠ and leaves them untouched, because automatically rebasing a branch you're not watching is asking for trouble.",[216],{"start":68,"end":217,"type":122},25,{"type":18,"text":219,"spans":220,"direction":21},"Important Notes",[221],{"start":68,"end":222,"type":122},15,{"type":143,"text":224,"spans":225,"direction":21},"The single most useful trick here is git fetch . origin/master:master. It only succeeds as a fast-forward — if the histories have diverged, git refuses the refspec. That refusal is a feature: it means the alias can never silently rewrite or clobber a branch.",[],{"type":143,"text":227,"spans":228,"direction":21},"The reason the output stays short on a repo with hundreds of branches is one line: git rev-parse --verify --quiet \"$u\" || continue. It mirrors what the gem did internally — only act on branches whose origin/\u003Cbranch> still exists. Without it, every old merged branch prints a \"can't fast-forward\" line and the result is unreadable.",[],{"type":143,"text":230,"spans":231,"direction":21},"If you only ever care about the current branch, you don't need any of this. The minimal replacement is genuinely a one-liner:",[],{"type":143,"text":233,"spans":234,"direction":21},"git config --global alias.up 'pull --rebase --autostash'",[],{"type":143,"text":236,"spans":237,"direction":21},"Or make it the default for every pull:",[],{"type":143,"text":239,"spans":240,"direction":21},"git config --global pull.rebase true git config --global rebase.autoStash true",[],{"type":18,"text":242,"spans":243,"direction":21},"Safety Considerations",[244],{"start":68,"end":245,"type":122},21,{"type":143,"text":247,"spans":248,"direction":21},"Diverged branches are never touched. They're reported and skipped — you decide when and how to rebase them.",[249],{"start":68,"end":250,"type":122},36,{"type":143,"text":252,"spans":253,"direction":21},"Fast-forward only for non-current branches. Because git fetch . src:dst rejects non-fast-forward updates, there's no way for the alias to lose commits on a branch you're not on.",[254],{"start":68,"end":255,"type":122},17,{"type":143,"text":257,"spans":258,"direction":21},"Autostash protects your work tree. If you run it with uncommitted changes, --autostash stashes them before the rebase and restores them after.",[259],{"start":68,"end":153,"type":122},{"type":143,"text":261,"spans":262,"direction":21},"It still only rebases the branch you have checked out, so the one operation that does rewrite history happens where you can see it.",[263],{"start":264,"end":96,"type":212},81,{"type":18,"text":266,"spans":267,"direction":21},"Conclusion",[268],{"start":68,"end":269,"type":122},10,{"type":58,"text":271,"spans":272,"direction":21},"Sometimes a deprecated tool is a gift: it nudges you to find out the problem was already solved by something you already have. git-up was great, but everything it did for me now lives in three lines of git config — no Ruby, no gem, no dependency to keep alive. If you've been keeping the gem around out of habit, drop it, paste the alias, and let git up keep your whole local repo — master and all — fresh from a single command.",[],"text$092eda5c-7757-4cdb-bb84-fc219560455f","text",[276,389,628],{"id":74,"uid":75,"url":76,"type":77,"href":78,"tags":277,"first_publication_date":80,"last_publication_date":80,"slugs":278,"linked_documents":279,"lang":13,"alternate_languages":280,"data":281},[],[82],[],[],{"title":282,"publishDate":5,"featuredImage":285,"slices":288},[283],{"type":53,"text":88,"spans":284,"direction":21},[],{"dimensions":286,"alt":88,"copyright":5,"url":93,"id":94,"edit":287},{"width":63,"height":92},{"x":96,"y":68,"zoom":69,"background":70},[289,296],{"variation":99,"version":100,"items":290,"primary":291,"id":110,"slice_type":111,"slice_label":5},[],{"image":292,"caption":295},{"dimensions":293,"alt":88,"copyright":5,"url":107,"id":94,"edit":294},{"width":105,"height":106},{"x":68,"y":68,"zoom":69,"background":70},[],{"variation":113,"version":100,"items":297,"primary":298,"id":273,"slice_type":274,"slice_label":5},[],{"text":299},[300,303,307,310,312,315,318,320,323,326,328,330,332,335,338,341,344,347,351,354,357,359,361,363,365,367,369,372,375,378,381,384,387],{"type":18,"text":118,"spans":301,"direction":21},[302],{"start":68,"end":121,"type":122},{"type":58,"text":124,"spans":304,"direction":21},[305],{"start":127,"end":128,"type":129,"data":306},{"link_type":131,"url":132,"target":133},{"type":18,"text":135,"spans":308,"direction":21},[309],{"start":68,"end":138,"type":122},{"type":58,"text":140,"spans":311,"direction":21},[],{"type":143,"text":144,"spans":313,"direction":21},[314],{"start":147,"end":148,"type":122},{"type":143,"text":150,"spans":316,"direction":21},[317],{"start":153,"end":154,"type":122},{"type":58,"text":156,"spans":319,"direction":21},[],{"type":58,"text":159,"spans":321,"direction":21},[322],{"start":162,"end":163,"type":122},{"type":18,"text":165,"spans":324,"direction":21},[325],{"start":68,"end":168,"type":122},{"type":58,"text":170,"spans":327,"direction":21},[],{"type":173,"text":174,"spans":329,"direction":21},[],{"type":58,"text":177,"spans":331,"direction":21},[],{"type":58,"text":180,"spans":333,"direction":21},[334],{"start":68,"end":183,"type":122},{"type":185,"text":186,"spans":336,"direction":21},[337],{"start":68,"end":189,"type":122},{"type":185,"text":191,"spans":339,"direction":21},[340],{"start":68,"end":194,"type":122},{"type":185,"text":196,"spans":342,"direction":21},[343],{"start":68,"end":194,"type":122},{"type":185,"text":200,"spans":345,"direction":21},[346],{"start":68,"end":203,"type":122},{"type":185,"text":205,"spans":348,"direction":21},[349,350],{"start":68,"end":208,"type":122},{"start":210,"end":211,"type":212},{"type":185,"text":214,"spans":352,"direction":21},[353],{"start":68,"end":217,"type":122},{"type":18,"text":219,"spans":355,"direction":21},[356],{"start":68,"end":222,"type":122},{"type":143,"text":224,"spans":358,"direction":21},[],{"type":143,"text":227,"spans":360,"direction":21},[],{"type":143,"text":230,"spans":362,"direction":21},[],{"type":143,"text":233,"spans":364,"direction":21},[],{"type":143,"text":236,"spans":366,"direction":21},[],{"type":143,"text":239,"spans":368,"direction":21},[],{"type":18,"text":242,"spans":370,"direction":21},[371],{"start":68,"end":245,"type":122},{"type":143,"text":247,"spans":373,"direction":21},[374],{"start":68,"end":250,"type":122},{"type":143,"text":252,"spans":376,"direction":21},[377],{"start":68,"end":255,"type":122},{"type":143,"text":257,"spans":379,"direction":21},[380],{"start":68,"end":153,"type":122},{"type":143,"text":261,"spans":382,"direction":21},[383],{"start":264,"end":96,"type":212},{"type":18,"text":266,"spans":385,"direction":21},[386],{"start":68,"end":269,"type":122},{"type":58,"text":271,"spans":388,"direction":21},[],{"id":390,"uid":391,"url":392,"type":77,"href":393,"tags":394,"first_publication_date":395,"last_publication_date":395,"slugs":396,"linked_documents":397,"lang":13,"alternate_languages":398,"data":399},"aUPCvBEAACkAHz8u","implementing-react-server-side-rendering-with-vite-in-ruby-on-rails-heroku-deployment","/articles/implementing-react-server-side-rendering-with-vite-in-ruby-on-rails-heroku-deployment","https://antoninpleskac.cdn.prismic.io/api/v2/documents/search?ref=ajDlVxEAAHypkWW4&q=%5B%5B%3Ad+%3D+at%28document.id%2C+%22aUPCvBEAACkAHz8u%22%29+%5D%5D",[],"2025-12-18T09:19:16+0000",[391],[],[],{"title":400,"publishDate":5,"featuredImage":404,"slices":410},[401],{"type":53,"text":402,"spans":403,"direction":21},"Implementing React Server-Side Rendering with Vite in Ruby on Rails (Heroku Deployment)",[],{"dimensions":405,"alt":402,"copyright":5,"url":406,"id":407,"edit":408},{"width":63,"height":92},"https://images.prismic.io/antoninpleskac/aUPGx3NYClf9oYhL_ChatGPTImageDec18%2C2025%2C10_17_43AM.png?auto=format,compress&rect=45,0,711,533&w=2000&h=1500","aUPGx3NYClf9oYhL",{"x":409,"y":68,"zoom":69,"background":70},45,[411,424],{"variation":99,"version":100,"items":412,"primary":413,"id":423,"slice_type":111,"slice_label":5},[],{"image":414,"caption":420},{"dimensions":415,"alt":402,"copyright":5,"url":418,"id":407,"edit":419},{"width":416,"height":417},800,533,"https://images.prismic.io/antoninpleskac/aUPGx3NYClf9oYhL_ChatGPTImageDec18%2C2025%2C10_17_43AM.png?auto=format,compress",{"x":68,"y":68,"zoom":69,"background":70},[421],{"type":58,"text":402,"spans":422,"direction":21},[],"image$1ee0626f-f793-4883-8b18-745aca73f284",{"variation":113,"version":100,"items":425,"primary":426,"id":627,"slice_type":274,"slice_label":5},[],{"text":427},[428,446,450,453,457,461,466,469,472,475,478,481,484,487,490,493,496,502,505,508,511,514,517,520,523,526,529,532,535,538,543,546,549,552,555,558,561,564,567,570,573,576,579,582,585,588,591,594,597,600,603,606,609,612,615],{"type":58,"text":429,"spans":430,"direction":21},"This article walks through a full, production-ready setup for Server-Side Rendering (SSR) of a React application embedded in a Ruby on Rails project. The solution uses Vite as the build tool, Apollo Client for GraphQL data handling, and a dedicated Node.js SSR server, with deployment targeting Heroku.",[431,434,437,440,443],{"start":432,"end":433,"type":122},62,89,{"start":435,"end":436,"type":122},168,172,{"start":438,"end":439,"type":122},192,205,{"start":441,"end":442,"type":122},249,267,{"start":444,"end":445,"type":122},295,301,{"type":447,"text":448,"spans":449,"direction":21},"heading2","Overall Architecture",[],{"type":58,"text":451,"spans":452,"direction":21},"The SSR implementation is composed of three clearly separated layers:",[],{"type":185,"text":454,"spans":455,"direction":21},"Rails (Backend)\nHandles incoming HTTP requests, forwards rendering requests to the SSR server, and returns the generated HTML.",[456],{"start":68,"end":222,"type":122},{"type":185,"text":458,"spans":459,"direction":21},"Node.js (SSR Server)\nRuns an Express server responsible for executing React code and producing static HTML output.",[460],{"start":68,"end":183,"type":122},{"type":185,"text":462,"spans":463,"direction":21},"React (Frontend)\nThe actual UI application, including a dedicated entrypoint designed specifically for server-side rendering.",[464],{"start":68,"end":465,"type":122},16,{"type":447,"text":467,"spans":468,"direction":21},"1. Rails Layer: SsrRenderer Concern",[],{"type":58,"text":470,"spans":471,"direction":21},"On the Rails side, we encapsulate all SSR-related logic inside a controller concern. This keeps controllers clean and makes SSR optional and configurable.",[],{"type":18,"text":473,"spans":474,"direction":21},"app/controllers/concerns/ssr_renderer.rb",[],{"type":173,"text":476,"spans":477,"direction":21},"module SsrRenderer\n  extend ActiveSupport::Concern\n\n  private\n\n  def render_ssr(url)\n    # SSR can be disabled (e.g., in development)\n    return [nil, nil, nil] unless ENV.fetch('SSR_ENABLED', 'true') == 'true'\n\n    ssr_host = ENV.fetch('SSR_HOST', '127.0.0.1')\n    ssr_port = ENV.fetch('SSR_PORT', '4000')\n\n    # Determine protocol and host for GraphQL endpoint\n    protocol = request.headers['X-Forwarded-Proto']&.split(',')&.first&.strip || request.protocol\n    host = request.headers['X-Forwarded-Host']&.split(',')&.first&.strip || request.host\n    graphql_uri = \"#{protocol}://#{host}/graphql\"\n\n    uri = URI.parse(\n      \"http://#{ssr_host}:#{ssr_port}/render\" \\\n      \"?url=#{CGI.escape(url)}\" \\\n      \"&graphql=#{CGI.escape(graphql_uri)}\" \\\n      \"&locale=#{I18n.locale}\"\n    )\n\n    http = Net::HTTP.new(uri.host, uri.port)\n    http.read_timeout = 6\n\n    req = Net::HTTP::Get.new(uri.request_uri)\n    req['Cookie'] = request.headers['Cookie'] if request.headers['Cookie'].present?\n    req['X-CSRF-Token'] = form_authenticity_token\n\n    response = http.request(req)\n    return [nil, nil, nil] unless response.is_a?(Net::HTTPSuccess)\n\n    json = JSON.parse(response.body)\n    [json['html'], json['state'], json['head']]\n  rescue StandardError => e\n    Rails.logger.error(\"[SSR HTTP] Failed: #{e.class}: #{e.message}\")\n    [nil, nil, nil]\n  end\nend\n",[],{"type":58,"text":479,"spans":480,"direction":21},"This design ensures that if SSR fails for any reason, the application gracefully falls back to client-side rendering.",[],{"type":447,"text":482,"spans":483,"direction":21},"2. Node.js Layer: SSR Server (Express + Vite)",[],{"type":58,"text":485,"spans":486,"direction":21},"The SSR server is a lightweight Express application that renders React either via Vite (development) or via a prebuilt bundle (production).",[],{"type":18,"text":488,"spans":489,"direction":21},"server/ssr-server.mjs",[],{"type":173,"text":491,"spans":492,"direction":21},"import express from 'express';\nimport { createServer } from 'vite';\nimport path from 'node:path';\n\nconst isProd = process.env.NODE_ENV === 'production';\nconst rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');\nconst app = express();\n\nlet vite;\nif (!isProd) {\n  vite = await createServer({\n    root: rootDir,\n    server: { middlewareMode: true, hmr: false },\n    appType: 'custom'\n  });\n  app.use(vite.middlewares);\n}\n\napp.get('/render', async (req, res) => {\n  try {\n    const { url, graphql, locale } = req.query;\n    let render;\n\n    if (isProd) {\n      const bundlePath = path.resolve(\n        rootDir,\n        'server/ssr-build/application-server.cjs'\n      );\n      render = (await import(bundlePath)).render;\n    } else {\n      render = (\n        await vite.ssrLoadModule(\n          'app/frontend/entrypoints/application-server.tsx'\n        )\n      ).render;\n    }\n\n    const ssrContext = {\n      headers: {\n        cookie: req.headers.cookie,\n        'x-csrf-token': req.headers['x-csrf-token'],\n        'x-locale': locale\n      },\n      graphqlUri: graphql\n    };\n\n    const result = await render(url, {}, ssrContext);\n    res.json(result);\n  } catch (e) {\n    console.error('[SSR Error]', e);\n    res.status(500).json({ error: e.message });\n  }\n});\n\napp.listen(4000, () => {\n  console.log('SSR Server running on port 4000');\n});\n",[],{"type":447,"text":494,"spans":495,"direction":21},"3. React Layer: Server-Side Entrypoint",[],{"type":58,"text":497,"spans":498,"direction":21},"To support GraphQL-based SSR, data must be fetched before rendering the component tree.",[499],{"start":500,"end":501,"type":122},51,57,{"type":18,"text":503,"spans":504,"direction":21},"app/frontend/entrypoints/application-server.tsx",[],{"type":173,"text":506,"spans":507,"direction":21},"import React from 'react';\nimport { renderToString } from 'react-dom/server';\nimport { ApolloProvider } from '@apollo/client';\nimport { getDataFromTree } from '@apollo/client/react/ssr';\nimport { StaticRouter } from 'react-router-dom/server';\nimport { HelmetProvider } from 'react-helmet-async';\nimport AppRoutes from '../app/AppRoutes';\nimport { createApolloClient } from '../config/apolloClient';\n\nexport async function render(url: string, _initialData: any, ssrContext: any) {\n  const client = createApolloClient({\n    ssrHeaders: ssrContext.headers,\n    ssrUri: ssrContext.graphqlUri\n  });\n\n  const helmetContext: any = {};\n\n  const app = (\n    \u003CApolloProvider client={client}>\n      \u003CHelmetProvider context={helmetContext}>\n        \u003CStaticRouter location={url}>\n          \u003CAppRoutes />\n        \u003C/StaticRouter>\n      \u003C/HelmetProvider>\n    \u003C/ApolloProvider>\n  );\n\n  await getDataFromTree(app);\n\n  const html = renderToString(app);\n  const initialApolloState = client.extract();\n\n  const { helmet } = helmetContext;\n  const head =\n    `${helmet.title.toString()}` +\n    `${helmet.meta.toString()}` +\n    `${helmet.link?.toString() || ''}`;\n\n  return {\n    html,\n    head,\n    state: `\u003Cscript>window.__APOLLO_STATE__=${JSON.stringify(initialApolloState)}\u003C/script>`\n  };\n}\n",[],{"type":447,"text":509,"spans":510,"direction":21},"4. Vite Configuration for SSR",[],{"type":58,"text":512,"spans":513,"direction":21},"Vite must be explicitly configured to produce a Node-compatible SSR bundle.",[],{"type":18,"text":515,"spans":516,"direction":21},"vite.ssr.config.mjs",[],{"type":173,"text":518,"spans":519,"direction":21},"import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n  plugins: [react()],\n  ssr: {\n    noExternal: true\n  },\n  build: {\n    ssr: true,\n    copyPublicDir: false,\n    rollupOptions: {\n      input: 'app/frontend/entrypoints/application-server.tsx',\n      output: {\n        format: 'cjs',\n        entryFileNames: 'application-server.cjs',\n        inlineDynamicImports: true\n      }\n    }\n  }\n});\n",[],{"type":447,"text":521,"spans":522,"direction":21},"5. Client-Side Hydration",[],{"type":58,"text":524,"spans":525,"direction":21},"Once HTML is rendered on the server, the browser must “take over” using hydration.",[],{"type":18,"text":527,"spans":528,"direction":21},"app/frontend/entrypoints/application.tsx",[],{"type":173,"text":530,"spans":531,"direction":21},"import { hydrateRoot, createRoot } from 'react-dom/client';\nimport { createApolloClient } from '../config/apolloClient';\n\nconst client = createApolloClient({\n  initialApolloState: window.__APOLLO_STATE__\n});\n\nconst container = document.getElementById('react_app');\n\nif (container && container.hasChildNodes()) {\n  hydrateRoot(container, (\n    \u003CApolloProvider client={client}>\n      \u003CBrowserRouter>\n        \u003CAppRoutes />\n      \u003C/BrowserRouter>\n    \u003C/ApolloProvider>\n  ));\n} else if (container) {\n  const root = createRoot(container);\n  root.render(\u003CApp />);\n}\n",[],{"type":447,"text":533,"spans":534,"direction":21},"6. Localization (i18n) and SSR",[],{"type":58,"text":536,"spans":537,"direction":21},"Hydration mismatches often occur when the server and client disagree on language.",[],{"type":58,"text":539,"spans":540,"direction":21},"Solution overview:",[541],{"start":68,"end":542,"type":122},18,{"type":185,"text":544,"spans":545,"direction":21},"Rails determines I18n.locale.",[],{"type":185,"text":547,"spans":548,"direction":21},"The locale is sent to the SSR server.",[],{"type":185,"text":550,"spans":551,"direction":21},"React initializes i18n with this locale before rendering.",[],{"type":185,"text":553,"spans":554,"direction":21},"The client reads the same locale during hydration.",[],{"type":58,"text":556,"spans":557,"direction":21},"This guarantees consistent language output.",[],{"type":447,"text":559,"spans":560,"direction":21},"7. Best Practices and Common Pitfalls",[],{"type":18,"text":562,"spans":563,"direction":21},"Always use timeouts",[],{"type":58,"text":565,"spans":566,"direction":21},"Rails should never wait indefinitely for SSR. A short timeout ensures graceful degradation to SPA rendering.",[],{"type":18,"text":568,"spans":569,"direction":21},"Avoid browser-only globals",[],{"type":58,"text":571,"spans":572,"direction":21},"Objects like window or document do not exist on the server:",[],{"type":173,"text":574,"spans":575,"direction":21},"if (typeof window !== 'undefined') {\n  // browser-only logic\n}\n",[],{"type":18,"text":577,"spans":578,"direction":21},"Prevent state leakage",[],{"type":58,"text":580,"spans":581,"direction":21},"Never reuse Apollo or i18n instances between requests. Each SSR request must create fresh instances to avoid leaking user data.",[],{"type":447,"text":583,"spans":584,"direction":21},"8. Deployment and Operations (Heroku)",[],{"type":58,"text":586,"spans":587,"direction":21},"In production, the SSR server runs alongside Rails as a parallel process.",[],{"type":18,"text":589,"spans":590,"direction":21},"Procfile",[],{"type":173,"text":592,"spans":593,"direction":21},"web: ./bin/web-start\n",[],{"type":18,"text":595,"spans":596,"direction":21},"bin/web-start",[],{"type":173,"text":598,"spans":599,"direction":21},"#!/usr/bin/env bash\n\nexport SSR_PORT=${SSR_PORT:-4000}\n\nnode server/ssr-server.mjs & SSR_PID=$!\nsleep 2\nbundle exec puma -C config/puma.rb & PUMA_PID=$!\n\nwait -n $PUMA_PID $SSR_PID\n",[],{"type":447,"text":601,"spans":602,"direction":21},"Environment Variables",[],{"type":58,"text":604,"spans":605,"direction":21},"SSR_ENABLED = true #Enables or disables SSRSSR_PORT4000SSR server port\nSSR_HOST = 127.0.0.1 #SSR server host\nSSR_DEBUG = 0 #Verbose logging\nSSR_CACHE_ENABLED = true #Enables HTML caching\nNODE_ENV = production #Production mode",[],{"type":447,"text":607,"spans":608,"direction":21},"Build Step on Heroku",[],{"type":58,"text":610,"spans":611,"direction":21},"Make sure the SSR bundle is built during deploy:",[],{"type":173,"text":613,"spans":614,"direction":21},"yarn frontend:build:ssr\n",[],{"type":58,"text":616,"spans":617,"direction":21},"This architecture delivers fast initial page loads, excellent SEO, and a modern React developer experience, all while staying deeply integrated with Ruby on Rails.",[618,621,624],{"start":619,"end":620,"type":122},27,50,{"start":622,"end":623,"type":122},52,65,{"start":625,"end":626,"type":122},73,106,"text$81d5e1de-43df-4c57-9ed9-79c04c5dbd4c",{"id":629,"uid":630,"url":631,"type":77,"href":632,"tags":633,"first_publication_date":634,"last_publication_date":635,"slugs":636,"linked_documents":637,"lang":13,"alternate_languages":638,"data":639},"aRDX2hEAACwAkaDw","repairing-a-twinkly-festoon-controller-identifying-and-replacing-a-faulty-bulk-converter","/articles/repairing-a-twinkly-festoon-controller-identifying-and-replacing-a-faulty-bulk-converter","https://antoninpleskac.cdn.prismic.io/api/v2/documents/search?ref=ajDlVxEAAHypkWW4&q=%5B%5B%3Ad+%3D+at%28document.id%2C+%22aRDX2hEAACwAkaDw%22%29+%5D%5D",[],"2025-11-09T18:10:21+0000","2025-12-18T09:20:48+0000",[630],[],[],{"title":640,"publishDate":5,"featuredImage":644,"slices":649},[641],{"type":53,"text":642,"spans":643,"direction":21},"Repairing a Twinkly Festoon Controller: Identifying and Replacing a Faulty bulk Converter",[],{"dimensions":645,"alt":642,"copyright":5,"url":646,"id":647,"edit":648},{"width":63,"height":92},"https://images.prismic.io/antoninpleskac/aUPHY3NYClf9oYhm_ChatGPTImageDec18%2C2025%2C10_20_20AM.png?auto=format,compress&rect=45,0,711,533&w=2000&h=1500","aUPHY3NYClf9oYhm",{"x":409,"y":68,"zoom":69,"background":70},[650,662],{"variation":99,"version":100,"items":651,"primary":652,"id":661,"slice_type":111,"slice_label":5},[],{"image":653,"caption":657},{"dimensions":654,"alt":642,"copyright":5,"url":655,"id":647,"edit":656},{"width":416,"height":417},"https://images.prismic.io/antoninpleskac/aUPHY3NYClf9oYhm_ChatGPTImageDec18%2C2025%2C10_20_20AM.png?auto=format,compress",{"x":68,"y":68,"zoom":69,"background":70},[658],{"type":58,"text":659,"spans":660,"direction":21},"Repairing a Twinkly Festoon Controller: Identifying and Replacing a Faulty DC/DC Converter",[],"image$8d1d41b6-ed95-4183-8418-e3e9daff7b8d",{"variation":113,"version":100,"items":663,"primary":664,"id":892,"slice_type":274,"slice_label":5},[],{"text":665},[666,668,671,674,677,680,683,686,689,692,695,698,701,704,707,710,713,716,719,722,725,728,731,734,737,740,745,748,754,757,762,765,768,771,774,777,781,786,790,795,799,804,807,810,813,816,819,822,825,828,831,834,837,840,843,846,849,852,855,858,861,864,867,870,873,876,879,882,887],{"type":447,"text":118,"spans":667,"direction":21},[],{"type":58,"text":669,"spans":670,"direction":21},"Twinkly smart lights are known for their vibrant colors and ESP32-based WiFi control. However, like many consumer electronics, their power circuitry can fail. In this article, we walk through the repair of a Twinkly controller that appeared dead despite receiving 24V input. The root cause? A failed, unmarked DC/DC buck converter.",[],{"type":58,"text":672,"spans":673,"direction":21},"We’ll cover how to identify the chip, find a compatible replacement (CX8508), and successfully bring the controller back to life — all with basic tools and a bit of reverse engineering.",[],{"type":447,"text":675,"spans":676,"direction":21},"Symptoms of Failure",[],{"type":143,"text":678,"spans":679,"direction":21},"24V input confirmed, but ESP32 does not boot",[],{"type":143,"text":681,"spans":682,"direction":21},"No LEDs, no Wi-Fi signal",[],{"type":143,"text":684,"spans":685,"direction":21},"8-pin SMD chip with thermal damage visible",[],{"type":143,"text":687,"spans":688,"direction":21},"No output voltage from the DC/DC converter",[],{"type":58,"text":690,"spans":691,"direction":21},"This clearly pointed to a power stage failure, most likely the step-down regulator.",[],{"type":447,"text":693,"spans":694,"direction":21},"Reverse Engineering the Unknown Chip",[],{"type":18,"text":696,"spans":697,"direction":21},"Physical Clues",[],{"type":143,"text":699,"spans":700,"direction":21},"8-pin SOP package",[],{"type":143,"text":702,"spans":703,"direction":21},"Connected to an inductor (L3), capacitors, and resistor divider (R3/R5)",[],{"type":143,"text":705,"spans":706,"direction":21},"Layout consistent with a buck (step-down) converter",[],{"type":18,"text":708,"spans":709,"direction":21},"Voltage Divider Analysis",[],{"type":58,"text":711,"spans":712,"direction":21},"Measured:",[],{"type":143,"text":714,"spans":715,"direction":21},"R3 = 100 kΩ",[],{"type":143,"text":717,"spans":718,"direction":21},"R5 = 10 kΩ",[],{"type":58,"text":720,"spans":721,"direction":21},"Using the buck formula:",[],{"type":173,"text":723,"spans":724,"direction":21},"Vout = Vref × (1 + R3 / R5)\n",[],{"type":58,"text":726,"spans":727,"direction":21},"Vref Calculated Vout Valid? 0.8 V 8.8 V ❌ Too high 0.6 V 6.6 V ✅ Match",[],{"type":58,"text":729,"spans":730,"direction":21},"This narrowed the search to buck converters with a 0.6 V reference.",[],{"type":18,"text":732,"spans":733,"direction":21},"Pin Mapping from PCB",[],{"type":58,"text":735,"spans":736,"direction":21},"By removing the chip and probing the pads, the following mapping was observed:",[],{"type":58,"text":738,"spans":739,"direction":21},"Pin Connected To Function 1 R3/R5 FB (Feedback) 2 Resistor to VIN EN (Enable) 3 Capacitor to GND SS (Soft Start) 4 Floating COMP / NC 5 VIN (24 V) VIN 6 Inductor (L3) SW (Switch) 7 GND plane GND 8 GND plane GND",[],{"type":58,"text":741,"spans":742,"direction":21},"This layout matches the CX8508 family of buck regulators.",[743],{"start":194,"end":744,"type":122},30,{"type":447,"text":746,"spans":747,"direction":21},"Final Identification: CX8508-ADJ",[],{"type":58,"text":749,"spans":750,"direction":21},"After further research and comparison with images of other Twinkly boards, the chip was confirmed to be CX8508-ADJ:",[751],{"start":752,"end":753,"type":122},104,114,{"type":58,"text":755,"spans":756,"direction":21},"Parameter Value Package SOP-8 Input Voltage Up to 36 V Reference Voltage 0.6 V Frequency 1 MHz Output Voltage Adjustable",[],{"type":58,"text":758,"spans":759,"direction":21},"Confirmed output: 6.6 V → Powers LDO (3.3V) and LED driver.",[760],{"start":542,"end":761,"type":122},23,{"type":447,"text":763,"spans":764,"direction":21},"Repair Procedure",[],{"type":18,"text":766,"spans":767,"direction":21},"Observations",[],{"type":143,"text":769,"spans":770,"direction":21},"Burnt/oxidized area near pin 2 (VIN)",[],{"type":143,"text":772,"spans":773,"direction":21},"Pads intact and salvageable",[],{"type":18,"text":775,"spans":776,"direction":21},"Steps",[],{"type":185,"text":778,"spans":779,"direction":21},"Clean PCB: IPA + brush + fiberglass pen",[780],{"start":68,"end":148,"type":122},{"type":185,"text":782,"spans":783,"direction":21},"Remove carbonized mask and oxide",[784],{"start":68,"end":785,"type":122},32,{"type":185,"text":787,"spans":788,"direction":21},"Re-tin pads: SnPb solder",[789],{"start":68,"end":138,"type":122},{"type":185,"text":791,"spans":792,"direction":21},"Solder new CX8508 using hot air",[793],{"start":68,"end":794,"type":122},31,{"type":185,"text":796,"spans":797,"direction":21},"Continuity check: VIN, GND, SW, FB, EN",[798],{"start":68,"end":465,"type":122},{"type":185,"text":800,"spans":801,"direction":21},"First power-up on lab PSU (limit 0.3 A)",[802],{"start":68,"end":803,"type":122},14,{"type":18,"text":805,"spans":806,"direction":21},"First Boot Test",[],{"type":58,"text":808,"spans":809,"direction":21},"Test Point Measured Value Vout (after L3) 6.6 V FB pin 0.60 V 3.3 V rail 3.33 V Boot current draw ~140 mA",[],{"type":58,"text":811,"spans":812,"direction":21},"✅ ESP32 booted up, Wi-Fi active, LEDs blinking — controller fully restored.",[],{"type":18,"text":814,"spans":815,"direction":21},"Load Test",[],{"type":143,"text":817,"spans":818,"direction":21},"1 A load, several minutes",[],{"type":143,"text":820,"spans":821,"direction":21},"No overheating, output stable, ripple minimal",[],{"type":447,"text":823,"spans":824,"direction":21},"Summary",[],{"type":58,"text":826,"spans":827,"direction":21},"Question Answer What was faulty? CX8508 buck converter Was R/L replacement needed? No Micro jumper wire needed? No, pads were recoverable Replacement part? CX8508-ADJ SOP-8 Result? Fully working Twinkly controller",[],{"type":447,"text":829,"spans":830,"direction":21},"Downloads (Recommended for Blog Attachment)",[],{"type":143,"text":832,"spans":833,"direction":21},"CX8508 Datasheet (PDF)",[],{"type":143,"text":835,"spans":836,"direction":21},"PCB photo (before/after)",[],{"type":143,"text":838,"spans":839,"direction":21},"Annotated pinout image",[],{"type":143,"text":841,"spans":842,"direction":21},"Power block diagram",[],{"type":447,"text":844,"spans":845,"direction":21},"Bonus: How to Spot a Dead Buck Converter",[],{"type":143,"text":847,"spans":848,"direction":21},"No 3.3V rail",[],{"type":143,"text":850,"spans":851,"direction":21},"ESP32 won’t boot",[],{"type":143,"text":853,"spans":854,"direction":21},"0 V across output cap",[],{"type":143,"text":856,"spans":857,"direction":21},"Burn marks or corrosion near inductor or SMD chip",[],{"type":143,"text":859,"spans":860,"direction":21},"SOP-8 chip with physical damage",[],{"type":143,"text":862,"spans":863,"direction":21},"Low/variable resistance between VIN–GND",[],{"type":447,"text":865,"spans":866,"direction":21},"Who Can Do This Repair?",[],{"type":58,"text":868,"spans":869,"direction":21},"Anyone with:",[],{"type":143,"text":871,"spans":872,"direction":21},"Hot air rework tool",[],{"type":143,"text":874,"spans":875,"direction":21},"Flux",[],{"type":143,"text":877,"spans":878,"direction":21},"Patience",[],{"type":58,"text":880,"spans":881,"direction":21},"No expensive BGA rework station required.",[],{"type":58,"text":883,"spans":884,"direction":21},"Total repair cost: approx. 8 CZK (~$0.35) ✅",[885],{"start":68,"end":886,"type":122},41,{"type":58,"text":888,"spans":889,"direction":21},"This guide demonstrates how a bit of electronics detective work and soldering can save a smart device from e-waste. Got a similar dead board? Don’t toss it — fix it!",[890],{"start":68,"end":891,"type":212},165,"text$0daf0104-4216-4f27-81c2-9e35ed660748",1781589379378]