<?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: Han Zhe</title>
    <description>The latest articles on DEV Community by Han Zhe (@hanzhe).</description>
    <link>https://dev.to/hanzhe</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4200352%2Fb7c53104-9d15-47c5-a852-1c4a77033bac.png</url>
      <title>DEV Community: Han Zhe</title>
      <link>https://dev.to/hanzhe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hanzhe"/>
    <language>en</language>
    <item>
      <title>Before You Say Just My Socks Is Slow, Check These Three Things</title>
      <dc:creator>Han Zhe</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:38:05 +0000</pubDate>
      <link>https://dev.to/hanzhe/before-you-say-just-my-socks-is-slow-check-these-three-things-5bk3</link>
      <guid>https://dev.to/hanzhe/before-you-say-just-my-socks-is-slow-check-these-three-things-5bk3</guid>
      <description>&lt;p&gt;Most "this service got slow" complaints I see follow the same pattern: someone speed-tests once, at 9pm, on whatever node the client happened to pick, and concludes the whole service went downhill. Sometimes it did get worse. More often, one of three fixable things is going on — and all three take under five minutes to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. You're probably on the wrong node
&lt;/h2&gt;

&lt;p&gt;A Just My Socks subscription ships with several Los Angeles nodes, and they are not equal. The s1/s2 nodes run on ordinary international routing. The s3/s4 nodes use the optimized CN2 GIA / 9929 routes back to China, which is the entire reason most people buy this service instead of a random VPS.&lt;/p&gt;

&lt;p&gt;Clients don't always pick s3/s4 by default, and after a subscription refresh some people silently land back on s1. During the evening peak in China (roughly 8–11pm), the gap between the two groups is not subtle: the optimized lines hold up, the regular ones sag. Before judging the service, open your client, look at which node is actually selected, and switch to s3 or s4 manually. Or turn on the URL-test / auto-select group and let the client measure instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Test all three networks, not just your own
&lt;/h2&gt;

&lt;p&gt;Latency from China to Los Angeles depends heavily on which carrier's route the traffic takes — China Telecom (CN2 GIA), China Unicom (9929), and China Mobile (CMI) behave like three different internets on a bad night. A node that is fine on Telecom can be miserable on Mobile, and the reverse happens too.&lt;/p&gt;

&lt;p&gt;This is why a single ping screenshot proves almost nothing. When I want a quick health check of a subscription, I run a small open-source script I maintain, &lt;a href="https://github.com/justmysocks-guide/jms-speedtest" rel="noopener noreferrer"&gt;jms-speedtest&lt;/a&gt;. It goes through the Just My Socks data centers, tests latency and packet loss across the three carrier routes, and also checks whether the exit IP still unlocks ChatGPT, Claude, Gemini and the usual streaming services. The unlock check matters more than people think: a node can be fast and still useless to you if its IP has been flagged and every AI chat returns 403.&lt;/p&gt;

&lt;p&gt;Packet loss is the number to watch. Latency of 160ms vs 190ms is barely noticeable in real use; 5% packet loss at peak hour absolutely is — it shows up as stalls, retries and video rebuffering that people then describe as "slow."&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your routing rules may be wasting the good nodes
&lt;/h2&gt;

&lt;p&gt;The third cause has nothing to do with the service at all. If your Clash or sing-box setup sends everything through the proxy — including domestic sites that should go direct — you burn your monthly traffic on traffic that never needed it, and you add proxy latency to sites that were fast natively.&lt;/p&gt;

&lt;p&gt;A maintained rule set fixes this quietly: domestic traffic direct, overseas traffic through the optimized nodes, and AI services pinned to the nodes whose IPs stay unlocked. The rule pack I use and maintain is &lt;a href="https://github.com/justmysocks-guide/clash-rules" rel="noopener noreferrer"&gt;clash-rules&lt;/a&gt;; it is built for Clash Verge Rev / Mihomo and auto-updates from the repository, so it is a set-once thing rather than another subscription to babysit. The setup walkthrough is in the &lt;a href="https://justmysocks-guide.github.io/justmysocks/clash/" rel="noopener noreferrer"&gt;Clash guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A five-minute checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Check which node is selected. If it is s1/s2, switch to s3/s4 and retest.&lt;/li&gt;
&lt;li&gt;Run a three-network test and look at packet loss, not just ping.&lt;/li&gt;
&lt;li&gt;Check the AI unlock status of your exit IP if ChatGPT or Claude is the thing failing.&lt;/li&gt;
&lt;li&gt;Make sure domestic traffic is going direct in your rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all four are fine and it is still slow at peak hour, then yes — it may be your plan tier. The LA 500 plan is shared by up to 5 devices; a household quietly running three phones, two laptops and a TV box on it will feel very different from one person on a laptop. That is a sizing problem, not a service problem, and it is cheaper to fix by upgrading (you only pay the difference) than by switching providers and starting over.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I maintain the justmysocks-guide project linked above. Some links in my guides are affiliate links; they do not change the prices or any of the test methodology described here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>proxy</category>
      <category>vpn</category>
      <category>clash</category>
      <category>chatgpt</category>
    </item>
    <item>
      <title>Why Facebook Shows Your 360 Photo Flat: PhotoSphere XMP, Explained and Fixed</title>
      <dc:creator>Han Zhe</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:23:59 +0000</pubDate>
      <link>https://dev.to/hanzhe/why-facebook-shows-your-360-photo-flat-photosphere-xmp-explained-and-fixed-39a0</link>
      <guid>https://dev.to/hanzhe/why-facebook-shows-your-360-photo-flat-photosphere-xmp-explained-and-fixed-39a0</guid>
      <description>&lt;h1&gt;
  
  
  Why Facebook Shows Your 360 Photo Flat: PhotoSphere XMP, Explained and Fixed
&lt;/h1&gt;

&lt;p&gt;By Han Zhe&lt;/p&gt;

&lt;p&gt;Your render is a perfect sphere on your own screen, and a stretched-out rug on Facebook. The pixels are not the problem. Facebook never got told the file is a sphere.&lt;/p&gt;

&lt;p&gt;A 360 photo is an ordinary JPEG with one extra piece of information tucked into its header: a block of PhotoSphere XMP metadata that says "wrap this image around a sphere." If that block is missing — and renders from Blender, AI generators, and Photoshop exports routinely ship without it — Facebook treats the upload as a flat picture. The fix is not re-rendering anything. It is putting the missing tags back in and uploading the file again as a JPEG.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Facebook decide a photo is 360?
&lt;/h2&gt;

&lt;p&gt;Facebook does not guess from the shape of the image. A wide rectangle could be a panorama, a banner, or a cropped screenshot; width alone proves nothing. When a JPEG arrives, Facebook reads the XMP packet stored in the JPEG's APP1 header segment and looks for the Google PhotoSphere namespace, &lt;code&gt;GPano&lt;/code&gt;. Two fields do most of the work: &lt;code&gt;GPano:ProjectionType&lt;/code&gt; must read &lt;code&gt;equirectangular&lt;/code&gt;, and &lt;code&gt;GPano:UsePanoramaViewer&lt;/code&gt; must read &lt;code&gt;True&lt;/code&gt;. When both are present, the viewer wraps the flat rectangle around a sphere and hands you drag-to-look controls. When either is absent, you get the flat rectangle, stretched edge to edge.&lt;/p&gt;

&lt;p&gt;This is the same tag set Google documents for Photo Sphere images, and the same one Google Maps, Kuula, and VR viewers read. It is a standard, not a Facebook quirk — which is why a file fixed for Facebook usually starts working everywhere else too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did my photo lose its 360 tags?
&lt;/h2&gt;

&lt;p&gt;In my experience there are four usual suspects, and they cover almost every "it worked from the camera but not from my edit" story:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The render never had them.&lt;/strong&gt; Blender, Unreal, and most AI image generators output pixels, not PhotoSphere metadata. A render straight out of Cycles or an AI tool is a beautiful equirectangular image with no instruction attached telling any viewer what to do with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The editor stripped them.&lt;/strong&gt; Photoshop and Lightroom rebuild the JPEG container when you export. Unless the export path explicitly preserves XMP, the GPano block is the first thing to go — the pixels survive, the instructions do not. "Save for web" style exports are repeat offenders here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A compressor or converter rebuilt the file.&lt;/strong&gt; Anything that decodes the JPEG and re-encodes it — a bulk optimizer, a format converter, a phone gallery's "save a copy" — produces a fresh container. Fresh containers carry no XMP unless the tool deliberately copies it over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file is not a JPEG at all.&lt;/strong&gt; A PNG or WebP can hold the same 2:1 equirectangular pixels, but Facebook's 360 handling keys off the JPEG+XMP combination. Uploading a PNG of a panorama and expecting it to spin is asking the uploader to read a label that was never printed on that format.&lt;/p&gt;

&lt;p&gt;One more distinction worth making: a phone's panorama mode is not a 360 photo. It covers a wide strip, not the full sphere, so even with metadata it only pans left and right. If your image is not a full 2:1 equirectangular — twice as wide as it is tall, covering 360 degrees around and 180 top to bottom — no tag will turn it into a sphere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exactly does the GPano block contain?
&lt;/h2&gt;

&lt;p&gt;The block is small. Google's Photo Sphere specification defines the fields below; for a standard full panorama, the pixel numbers are just your image's own dimensions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it says&lt;/th&gt;
&lt;th&gt;Example for a 4096×2048 image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GPano:ProjectionType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How to wrap the image&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;equirectangular&lt;/code&gt; (the only value Google products support)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GPano:UsePanoramaViewer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open this in a sphere viewer, not as a flat image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GPano:FullPanoWidthPixels&lt;/code&gt; / &lt;code&gt;FullPanoHeightPixels&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Size of the complete sphere&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;4096&lt;/code&gt; / &lt;code&gt;2048&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GPano:CroppedAreaImageWidthPixels&lt;/code&gt; / &lt;code&gt;CroppedAreaImageHeightPixels&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Size of the image actually present&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;4096&lt;/code&gt; / &lt;code&gt;2048&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GPano:CroppedAreaLeftPixels&lt;/code&gt; / &lt;code&gt;CroppedAreaTopPixels&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Where the image sits inside the full sphere&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; / &lt;code&gt;0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GPano:PoseHeadingDegrees&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compass direction of the image center (needed for Google Maps, optional for Facebook)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For an uncropped full panorama, the "full pano" and "cropped area" numbers are identical — that is normal, not an error. The cropped-area fields only differ when the image is a partial panorama cut out of a larger sphere, in which case they also carry the offset so the horizon stays level.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I check whether my file already has the tags?
&lt;/h2&gt;

&lt;p&gt;Before fixing anything, check. Guessing wastes a re-upload.&lt;/p&gt;

&lt;p&gt;If you have ExifTool installed, one command tells you the truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;exiftool &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:ProjectionType &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:UsePanoramaViewer &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:FullPanoWidthPixels yourphoto.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;ProjectionType&lt;/code&gt; comes back &lt;code&gt;equirectangular&lt;/code&gt; and the pixel fields match your image size, the file is fine and your problem is elsewhere — most likely the upload itself, or the file was converted to PNG somewhere along the way. If the fields come back empty, you have found your culprit.&lt;/p&gt;

&lt;p&gt;No command line? Open the file in any 360 viewer that reads metadata — a browser-based viewer will either wrap it into a sphere or show it flat, which is the same test with less typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I fix it without re-rendering?
&lt;/h2&gt;

&lt;p&gt;You have two honest options, and neither touches a single pixel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: ExifTool.&lt;/strong&gt; This is the free, no-upload route for people comfortable with a terminal. Set the projection, the viewer flag, and the pixel dimensions to match your actual image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;exiftool &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:ProjectionType&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"equirectangular"&lt;/span&gt; &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:UsePanoramaViewer&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"True"&lt;/span&gt; &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:FullPanoWidthPixels&lt;span class="o"&gt;=&lt;/span&gt;4096 &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:FullPanoHeightPixels&lt;span class="o"&gt;=&lt;/span&gt;2048 &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:CroppedAreaImageWidthPixels&lt;span class="o"&gt;=&lt;/span&gt;4096 &lt;span class="nt"&gt;-XMP-GPano&lt;/span&gt;:CroppedAreaImageHeightPixels&lt;span class="o"&gt;=&lt;/span&gt;2048 yourphoto.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the numbers to your image's real width and height. Wrong numbers here are worse than missing tags — a viewer that trusts bad dimensions will wrap the sphere with a visible pinch at the poles. Then upload the tagged JPEG to Facebook as a new post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: a browser injector.&lt;/strong&gt; Disclosure: I built one of these — the metadata injector in 360 Panorama Viewer (360panoramaviewer.com), which writes the same GPano block into the JPEG header in your browser without uploading the file. Drop in a 2:1 equirectangular JPEG, download the tagged copy, post that. The reason I mention it second is simple: ExifTool does the same job and costs nothing, and if you already have it installed, use it. The injector exists for the people who do not want a terminal involved in posting a photo.&lt;/p&gt;

&lt;p&gt;Either way, the workflow after tagging is the same: upload the tagged JPEG as a new Facebook post, and check the preview before publishing. A recognized file shows drag-to-look controls in the preview. If the preview is still flat, do not publish and hope — the file that goes up is the file people see.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if it still shows flat after the fix?
&lt;/h2&gt;

&lt;p&gt;Run this checklist in order, because the failure is almost always one of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the uploaded file actually the tagged copy?&lt;/strong&gt; The classic mistake is tagging &lt;code&gt;photo-fixed.jpg&lt;/code&gt; and then uploading the original &lt;code&gt;photo.jpg&lt;/code&gt; from the downloads folder out of habit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a JPEG?&lt;/strong&gt; A tagged PNG is still a PNG. Convert to JPEG first, then tag, then upload — in that order, since converting after tagging strips the tags again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it really 2:1?&lt;/strong&gt; Divide width by height. A 6000×3000 render is fine; a 6000×2800 crop is not a full sphere, and no metadata will make Facebook treat it as one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did anything compress it after tagging?&lt;/strong&gt; Messaging the file to yourself, or running it through an optimizer, can rebuild the container. Tag last, upload next, nothing in between.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did you test the preview?&lt;/strong&gt; If the preview drags, the post will too. If it does not, the file is still wrong — go back to the ExifTool check rather than re-uploading the same file and hoping for a different result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is the one I see people skip most. Re-uploading an unchanged file is not troubleshooting; it is the same test with the same answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-sentence version
&lt;/h2&gt;

&lt;p&gt;Facebook shows your 360 photo flat because the JPEG is missing its PhotoSphere XMP tags — inject &lt;code&gt;GPano:ProjectionType=equirectangular&lt;/code&gt; and the matching pixel fields, keep the file a JPEG, and upload the tagged copy as a new post.&lt;/p&gt;

&lt;p&gt;If you want to check a file before you post it, the injector page linked above reads the same fields it writes. And if you would rather not trust any tool, the ExifTool commands in this article are the whole fix — no account, no upload, no re-render.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm the developer of 360 Panorama Viewer (360panoramaviewer.com), a free browser-based 360 studio. The fix in this article works with any tool that writes standard PhotoSphere XMP, including ExifTool — the tags are Google's open specification, not anything proprietary to my site.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sources: Google's Photo Sphere XMP metadata specification (developers.google.com); the 360 metadata injector reference at 360panoramaviewer.com/360-metadata-injector/&lt;/em&gt;&lt;/p&gt;

</description>
      <category>photography</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
