<?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: visiohex</title>
    <description>The latest articles on DEV Community by visiohex (@jy_wang_2c4d36f4d66adea80).</description>
    <link>https://dev.to/jy_wang_2c4d36f4d66adea80</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%2F3765913%2F6890e795-c29f-462e-a420-54ad45b80086.png</url>
      <title>DEV Community: visiohex</title>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jy_wang_2c4d36f4d66adea80"/>
    <language>en</language>
    <item>
      <title>Why I Kept My AI Video Tool to One Scene</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 11 Oct 2026 12:27:00 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/why-i-kept-my-ai-video-tool-to-one-scene-3095</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/why-i-kept-my-ai-video-tool-to-one-scene-3095</guid>
      <description>&lt;p&gt;I built &lt;a href="https://bulin47.online/" rel="noopener noreferrer"&gt;Bulin 47 AI&lt;/a&gt;, a small browser tool that takes one portrait and makes a version of the Bulin 47 freestyle clip.&lt;/p&gt;

&lt;p&gt;The result is a 10-second vertical video. The portrait guides the main rapper’s appearance, while a fixed reference video guides the performance and surrounding scene. The reference soundtrack is included.&lt;/p&gt;

&lt;p&gt;The main product decision was to keep it to that one scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding what the user actually needs to choose
&lt;/h2&gt;

&lt;p&gt;A general video generator needs room for prompts, model selection, reference footage, duration and output settings. For this particular clip, most of those choices can be made ahead of time.&lt;/p&gt;

&lt;p&gt;I’ve already prepared the reference clip, prompt and audio. The person using the site mainly needs to decide who should appear in the video.&lt;/p&gt;

&lt;p&gt;That makes the upload form smaller, but it also sets a clear boundary. The tool doesn’t offer custom choreography or a library of different performances. If you want a different scene, this isn’t the right tool for it.&lt;/p&gt;

&lt;p&gt;The photo and video reference have separate jobs. A clear portrait helps with appearance; the reference supplies the movement. There’s no need to record yourself performing or upload a full-body shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small form still needs to explain the result
&lt;/h2&gt;

&lt;p&gt;The flow is straightforward: watch the reference, sign in with Google, upload a portrait and submit a generation. You can follow its progress in &lt;strong&gt;My videos&lt;/strong&gt;, then download the finished MP4.&lt;/p&gt;

&lt;p&gt;The output is fixed at 720p, 10 seconds and a 9:16 aspect ratio. Those details belong near the upload form, because they affect whether the result will work for someone’s intended use.&lt;/p&gt;

&lt;p&gt;There are less tidy details to explain too. A face can look convincing in the first frame and change when the person turns. Hands and timing need checking. The model creates new frames, so appearance and motion can vary between generations.&lt;/p&gt;

&lt;p&gt;A completed job doesn’t necessarily mean the person will like the result. That matters when explaining credits: confirmed failed generations return the credit, while a completed video with an unsatisfying likeness is a different situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the examples honest
&lt;/h2&gt;

&lt;p&gt;The homepage includes the reference and trend examples so people can see the scene before paying. Those gallery clips come from outside creators, are credited on the site and weren’t generated by this service.&lt;/p&gt;

&lt;p&gt;That distinction needs to be visible. A reference clip shows what the tool is aiming for; it doesn’t prove what every generation will look like.&lt;/p&gt;

&lt;p&gt;Making a video requires a paid credit. There are no free generations at the moment. Credits are sold in one-time packs, with no subscription or automatic renewal. The current options are on the &lt;a href="https://bulin47.online/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few limits worth stating upfront
&lt;/h2&gt;

&lt;p&gt;Photos are kept for up to seven days and finished videos for 30 days. Download any result you want to keep. Get permission before using someone else’s portrait, and label shared clips as AI-generated.&lt;/p&gt;

&lt;p&gt;Keeping the scope small gives me a specific workflow to improve: one portrait, one scene, one downloadable result. I’m interested in whether that constraint makes the process easier to understand, and where the form still leaves people guessing.&lt;/p&gt;

&lt;p&gt;You can see it at &lt;a href="https://bulin47.online/" rel="noopener noreferrer"&gt;bulin47.online&lt;/a&gt;. If you take a look, I’d appreciate feedback on the upload flow and how clearly it explains what you’ll get.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>AI Raindance: make a 15-second music video from one or two portraits</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 11 Oct 2026 04:21:33 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/ai-raindance-make-a-15-second-music-video-from-one-or-two-portraits-33o2</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/ai-raindance-make-a-15-second-music-video-from-one-or-two-portraits-33o2</guid>
      <description>&lt;p&gt;I built &lt;a href="https://airaindance.online/" rel="noopener noreferrer"&gt;AI Raindance&lt;/a&gt;, a web app that puts people from your photos into a short video inspired by Dave and Tems' Raindance music video.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgx8s2ihl9hvqmy1jslsl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgx8s2ihl9hvqmy1jslsl.png" alt="AI Raindance: 15 seconds, solo or duo, with sound" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You choose who appears in the scene. The app supplies the reference, the waterfront setting and the original audio. There's no prompt to write: upload one or two portraits, choose the roles, and generate a 15-second MP4.&lt;/p&gt;

&lt;p&gt;Here's how it works, and what to check before spending a video credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the cast
&lt;/h2&gt;

&lt;p&gt;There are two modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Photos&lt;/th&gt;
&lt;th&gt;Who appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo&lt;/td&gt;
&lt;td&gt;One portrait&lt;/td&gt;
&lt;td&gt;The person in your photo becomes the singer; the companion is removed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duo&lt;/td&gt;
&lt;td&gt;Two separate portraits&lt;/td&gt;
&lt;td&gt;Photo 1 is the companion, and photo 2 is the singer.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Duo, you don't need a picture of both people together. Use a separate portrait for each person. If you've put the singer in the first slot, use &lt;strong&gt;Swap roles&lt;/strong&gt; before generating.&lt;/p&gt;

&lt;p&gt;Both modes cost one video credit. Adding a second person doesn't cost extra.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a photo where the face is easy to see
&lt;/h2&gt;

&lt;p&gt;A front-facing portrait or a slight angle is a good starting point. Keep the face large enough to see clearly, with even lighting and no sunglasses or heavy filters. Group shots and faces far from the camera are harder to work with.&lt;/p&gt;

&lt;p&gt;The app accepts JPEG, PNG and WebP files, up to 10 MB per photo. Use your own photos, or photos you have permission to use with the subject's consent.&lt;/p&gt;

&lt;p&gt;Sign in before selecting them. The uploaded photos aren't saved as an unfinished draft, so leaving the page before submitting can lose your selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make and download the video
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Sign in with Google. If your balance is empty, buy a one-time video pack.&lt;/li&gt;
&lt;li&gt;Choose Solo or Duo, add your portraits and check the role labels.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Generate&lt;/strong&gt; to submit the task and use one credit.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;My videos&lt;/strong&gt; to follow its progress, play the result and download the MP4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you've submitted the task, you can close the page. Return with the same Google account to find it again. You don't need to keep a browser tab open throughout generation.&lt;/p&gt;

&lt;p&gt;The finished video is &lt;strong&gt;15 seconds, 768P, in a 4:3 frame&lt;/strong&gt;, with the original reference audio included. Your portrait changes the person on screen; it doesn't turn your voice into the song.&lt;/p&gt;

&lt;p&gt;Watch the whole clip before sharing it. Facial details, expressions and singing timing can vary between AI results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look at the sample before buying
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://airaindance.online/examples" rel="noopener noreferrer"&gt;examples page&lt;/a&gt; separates the original reference footage from a generated Solo sample.&lt;/p&gt;

&lt;p&gt;That sample is a short 4.5-second clip made from a fictional AI portrait, with its model-generated soundtrack. It isn't a full preview of a purchased video. Purchased videos use the fixed 15-second template and original Raindance audio.&lt;/p&gt;

&lt;p&gt;The template is deliberately specific: you can choose the portraits and roles, but you can't change the song, scene or duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and file storage
&lt;/h2&gt;

&lt;p&gt;Generation is paid. The current packs are $9.99 for one video, $24.99 for three, or $69.99 for ten. There is no subscription, and purchased credits don't expire. If a task is confirmed as failed, its credit is returned automatically.&lt;/p&gt;

&lt;p&gt;Photos are kept for seven days and finished videos for thirty days. Download the MP4 while it's available. Uploads and finished videos stay in your account; they aren't automatically added to the public examples page.&lt;/p&gt;

&lt;p&gt;AI Raindance is an independent project, with no affiliation with Dave or Tems.&lt;/p&gt;

&lt;p&gt;You can try the flow at &lt;a href="https://airaindance.online/" rel="noopener noreferrer"&gt;airaindance.online&lt;/a&gt;. I'd welcome feedback on the Solo/Duo controls, especially whether the companion and singer roles are clear before you upload.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a one-photo Burning Bridges video generator</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 11 Oct 2026 03:59:51 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-one-photo-burning-bridges-video-generator-1ja1</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-one-photo-burning-bridges-video-generator-1ja1</guid>
      <description>&lt;p&gt;I built &lt;a href="https://drakeburningbridgesai.video/" rel="noopener noreferrer"&gt;Drake Burning Bridges AI&lt;/a&gt;, a small web app that takes one portrait and generates a personal version of the Burning Bridges party scene.&lt;/p&gt;

&lt;p&gt;You choose the main character with your photo. I’ve configured the video reference and prompt, so you don’t have to write scene instructions. The output is a 20-second, 720p, landscape MP4 with the supplied soundtrack.&lt;/p&gt;

&lt;p&gt;For this post, I wanted to share the implementation choices behind that short upload flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the input small
&lt;/h2&gt;

&lt;p&gt;The studio accepts one portrait. The photo provides the appearance reference for the main subject; the video reference guides the party setting, gestures and camera movement.&lt;/p&gt;

&lt;p&gt;I kept the scene and output settings fixed. Users can choose who appears in the foreground, but they can’t cast each guest or change the duration. That limits what you can make, and it gives the upload page a clear job: help you choose a usable photo and understand the expected result.&lt;/p&gt;

&lt;p&gt;The server checks file size, image type and dimensions. A file extension or a browser-provided MIME type alone isn’t enough to validate an upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;I’m using React and TanStack Start for the app, with Better Auth for authentication. The application runs on Cloudflare Workers. I store task and credit records in D1, and uploaded photos and finished videos in R2.&lt;/p&gt;

&lt;p&gt;The app calls an external video generation service. Workers handle the application flow rather than running video inference themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treating generation as a saved job
&lt;/h2&gt;

&lt;p&gt;A video request needs to survive the user leaving the page. I store the job before processing it and track its progress through these states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queued → processing → composing → saving → succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composition step adds the supplied soundtrack. I mark a task as successful after saving the finished video, rather than when the generation provider first reports success.&lt;/p&gt;

&lt;p&gt;Users can return to My videos to check an existing task. The browser doesn’t have to stay open for the whole operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling retries and credits
&lt;/h2&gt;

&lt;p&gt;Each generation uses one credit. I use a request ID to connect a submission to its job. If the same user sends the same request again, the application returns the existing job. Reusing that ID with different input is rejected.&lt;/p&gt;

&lt;p&gt;In the database, I connect job creation to the credit debit with a trigger. If the debit can’t complete, the job insert rolls back. I also give failed-job credit returns a unique ledger reference, so processing the same failure again doesn’t add another credit.&lt;/p&gt;

&lt;p&gt;The less obvious case is a timeout while submitting to the video provider. The provider might have accepted the job even though my application didn’t receive its response. Submitting a fresh request at that point could create a second paid generation.&lt;/p&gt;

&lt;p&gt;I record that submission attempt and keep an uncertain outcome for reconciliation. A confirmed failure returns the credit; a missing response needs more investigation. This leaves some cases requiring support review, but avoids treating an unknown result as permission to submit again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting expectations for the video
&lt;/h2&gt;

&lt;p&gt;AI can change facial details or hands as the person moves. A completed job means the user received a video, not that the model preserved their appearance in every frame.&lt;/p&gt;

&lt;p&gt;I’ve included &lt;a href="https://drakeburningbridgesai.video/examples" rel="noopener noreferrer"&gt;examples&lt;/a&gt; so visitors can inspect the intended scene. Those are supplied 480p samples, not evidence of a paid generation through the app. The configured output is 720p.&lt;/p&gt;

&lt;p&gt;The site is an independent project with no affiliation with Drake. Users should upload photos they have permission to use, with the subject’s consent.&lt;/p&gt;

&lt;p&gt;You can see the project at &lt;a href="https://drakeburningbridgesai.video/" rel="noopener noreferrer"&gt;drakeburningbridgesai.video&lt;/a&gt;. I’d welcome feedback on the upload flow, or how you handle uncertain provider responses in apps that charge per task.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built AI Zombie: two photos, a 15-second zombie love story</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 11 Oct 2026 03:55:02 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-ai-zombie-two-photos-a-15-second-zombie-love-story-29pc</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-ai-zombie-two-photos-a-15-second-zombie-love-story-29pc</guid>
      <description>&lt;p&gt;I built &lt;a href="https://aizombie.online" rel="noopener noreferrer"&gt;AI Zombie&lt;/a&gt;, a web app that turns two photos into a 15-second zombie love story with sound.&lt;/p&gt;

&lt;p&gt;The story starts with a survivor facing someone they care about who has become a zombie. Instead of shooting, the survivor lowers the weapon and opens their arms. The zombie approaches, they embrace, and the scene cuts to a warm memory of them together.&lt;/p&gt;

&lt;p&gt;You can use photos of two people, or make your dog or cat the zombie and keep yourself as the human survivor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Upload a photo of the person or pet who becomes the zombie.&lt;/li&gt;
&lt;li&gt;Upload a photo of the human survivor.&lt;/li&gt;
&lt;li&gt;Choose a cabin, city, or chapel setting, and a portrait or landscape format.&lt;/li&gt;
&lt;li&gt;Start the generation, then watch and download the finished video from My videos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output is a 15-second, 720p MP4 with audio. The homepage includes example videos if you want to see the style before creating one. Generation uses paid video credits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving the model a specific story
&lt;/h2&gt;

&lt;p&gt;I kept the generator focused on one short narrative. You choose the characters and setting; the app supplies the sequence of shots.&lt;/p&gt;

&lt;p&gt;In the implementation, I build the video prompt around timed story beats: recognition, the survivor's decision, the approach, the embrace, and the memory. The transition is the part I care about most. I ask the model to keep the characters in the same positions across the cut, so the embrace connects the present to the memory.&lt;/p&gt;

&lt;p&gt;The pet version needs different instructions. I ask for a natural four-legged trot and a short jump that fits the animal's size. The prompt also specifies that the model should preserve the pet's markings and species rather than give it human limbs or gestures.&lt;/p&gt;

&lt;p&gt;These are instructions to the model, rather than guarantees. Faces, motion, and sound can vary between generations, and the model may miss a detail in the intended sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few implementation details
&lt;/h2&gt;

&lt;p&gt;I built the app with TanStack Start and React, running on Cloudflare Workers. I use D1 for generation records and R2 for media storage. The video generation itself runs through an external API.&lt;/p&gt;

&lt;p&gt;A generation takes longer than a normal page request, so I store it as a job. The UI shows its status while the backend handles submission and completion. Users can return to their video history to find the result.&lt;/p&gt;

&lt;p&gt;I also use a request ID to handle repeated submissions. If a user retries the same request, the backend returns the existing job instead of creating another one. That matters when each generation consumes a paid credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'd like your feedback
&lt;/h2&gt;

&lt;p&gt;You can see the examples and try the generator at &lt;a href="https://aizombie.online" rel="noopener noreferrer"&gt;aizombie.online&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm interested in whether the story makes sense when you watch it, and whether the upload flow makes the two character roles clear. If you're building an image-to-video app, I'd also like to hear how you handle continuity between shots.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PhotoCraft Online: trying an open-source image editor in the browser</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sat, 10 Oct 2026 10:05:31 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/photocraft-online-trying-an-open-source-image-editor-in-the-browser-28b</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/photocraft-online-trying-an-open-source-image-editor-in-the-browser-28b</guid>
      <description>&lt;p&gt;I put together &lt;a href="https://photocraftonline.org/" rel="noopener noreferrer"&gt;PhotoCraft Online&lt;/a&gt; as a place to try PhotoCraft in a browser and read through a few editing guides. You can open the editor without installing an application or creating an account. Basic editing is free.&lt;/p&gt;

&lt;p&gt;The editor itself comes from &lt;a href="https://github.com/storytold/photocraft" rel="noopener noreferrer"&gt;storytold/photocraft&lt;/a&gt;. The upstream team and contributors built it; I host the web build and maintain the surrounding website. PhotoCraft Online is an independent site, and the version hosted here is v0.5.0.&lt;/p&gt;

&lt;p&gt;For this post, I want to show the small checks behind the guides. A feature list gives you less to go on than an image that was opened, changed, and exported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with a small PNG
&lt;/h2&gt;

&lt;p&gt;The first test used a 640 × 400 PNG: a cream background with an amber rectangle. It is a boring image on purpose. There are few places for an editing mistake to hide.&lt;/p&gt;

&lt;p&gt;In the browser build, the check covered adding English text, undo and redo, cropping, and a brightness adjustment. The PNG export downloaded and displayed the edited image. These checks ran on macOS in Chromium with a 1280 × 720 viewport.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2c52eyvlzdxzdijimoj8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2c52eyvlzdxzdijimoj8.webp" alt="PhotoCraft with an English text layer above a PNG background" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have used a layered image editor, you will recognize the arrangement: tools on the left, the document in the middle, and layers on the right. For a first try, open a copy of an image, add a short line of text, then download a PNG and open it outside the editor.&lt;/p&gt;

&lt;p&gt;One detail matters at export time. The PNG download is a flattened image. You can use it as the finished picture, but you cannot reopen that PNG and expect to recover the text layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PSD check that needed a correction
&lt;/h2&gt;

&lt;p&gt;The next sample was a synthetic PSD with two raster layers named &lt;code&gt;Background&lt;/code&gt; and &lt;code&gt;Amber card&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both layers appeared in the editor, and the visibility controls worked. The initial stacking order differed from the PSD's embedded composite, though: the background covered the amber rectangle. Moving &lt;code&gt;Amber card&lt;/code&gt; above &lt;code&gt;Background&lt;/code&gt; restored the expected view.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi58dlvvoe1utcljrgo6t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi58dlvvoe1utcljrgo6t.webp" alt="PhotoCraft showing the amber card above the background after reordering PSD layers" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a useful result, with a limit. The check established that this sample could be opened and its layers manipulated. It did not establish why the initial order differed. The sample encoding and the importer both need investigation. Saving the PSD and reopening it was outside this test.&lt;/p&gt;

&lt;p&gt;I would check my own file before relying on it for a handoff. In particular, these small samples tell us little about a document with smart objects, masks, or fonts that the browser cannot find. I have put the test details and the relevant upstream documentation in the &lt;a href="https://photocraftonline.org/blog/photocraft-psd-compatibility" rel="noopener noreferrer"&gt;PSD compatibility guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save before you close the tab
&lt;/h2&gt;

&lt;p&gt;The editor processes the images you open in the browser. PhotoCraft Online does not provide cloud document storage, so download your work before reloading or closing the tab. Keep the original file as well.&lt;/p&gt;

&lt;p&gt;The website has English and Chinese guides. That refers to the site and its articles; I have not verified Chinese text rendering inside the editor. PhotoCraft remains in early alpha, and the upstream developers warn against treating it as a replacement for daily professional Photoshop work.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://photocraftonline.org/editor" rel="noopener noreferrer"&gt;open the editor here&lt;/a&gt;. The &lt;a href="https://photocraftonline.org/blog/photocraft-tutorial" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt; follows a small editing workflow if you want somewhere to start.&lt;/p&gt;

&lt;p&gt;If you try a PSD and something looks wrong, a useful report includes the browser, document dimensions, layer count, and the change you expected to see. That gives me something concrete to reproduce without asking you to share the original file.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built CompressGIF: local GIF compression, plus REST and MCP APIs</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sat, 03 Oct 2026 03:12:44 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-compressgif-local-gif-compression-plus-rest-and-mcp-apis-8jd</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-compressgif-local-gif-compression-plus-rest-and-mcp-apis-8jd</guid>
      <description>&lt;p&gt;A GIF can be useful in a README, bug report, or product demo—and still be too large to attach. Getting it below an upload limit often means trying compression settings, checking the animation, and trying again.&lt;/p&gt;

&lt;p&gt;I’m sharing &lt;a href="https://compressgif.net/" rel="noopener noreferrer"&gt;CompressGIF&lt;/a&gt;, a tool I built for that workflow. It has a browser compressor for local files and a separate cloud API for automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the browser tool
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://compressgif.net/#tool" rel="noopener noreferrer"&gt;free browser tool&lt;/a&gt; processes your GIF on your device without uploading the file for compression. It keeps the output as an animated GIF and adds no watermark.&lt;/p&gt;

&lt;p&gt;There are three modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quality first:&lt;/strong&gt; start with conservative compression and inspect the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size first:&lt;/strong&gt; try stronger palette and lossy settings when a smaller file matters more than fine detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target size:&lt;/strong&gt; try to get under a specific limit, with free presets for 100KB, 512KB, and 1MB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free usage supports three files per batch, up to 10MB per file. Pro adds larger batches, custom target sizes, and ZIP downloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  A target is a goal, not a guarantee
&lt;/h2&gt;

&lt;p&gt;CompressGIF uses Gifsicle. The useful tradeoff depends on the content: small UI text, flat illustrations, and video-like animations do not respond to compression in the same way.&lt;/p&gt;

&lt;p&gt;By default, the tool preserves dimensions, animation duration, and loop behavior rather than silently dropping motion frames or resizing. Resizing is an explicit choice.&lt;/p&gt;

&lt;p&gt;If a target cannot be reached, the tool says so. It keeps the smallest valid candidate, including the original when compression cannot improve it. I’d rather show a missed target than imply that every GIF can become 100KB without a visible tradeoff.&lt;/p&gt;

&lt;p&gt;When checking a result, look beyond the byte count: inspect text, gradients, transparent edges, and playback timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate it with REST
&lt;/h2&gt;

&lt;p&gt;For scripts and integrations, there is an asynchronous compression API. Unlike the browser tool, this is cloud processing: you upload a file or provide a public HTTPS GIF URL.&lt;/p&gt;

&lt;p&gt;Here is an upload request targeting 1MB (1,000,000 bytes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://compressgif.net/api/v1/compressions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$COMPRESSGIF_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: demo-upload-001"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@animation.gif"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'options={"mode":"target","targetBytes":1000000}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the API key in a protected environment variable, not client-side code. Use a new idempotency key for each new file-and-options request, and reuse that key when retrying the same request after an uncertain response.&lt;/p&gt;

&lt;p&gt;The response includes a top-level job &lt;code&gt;id&lt;/code&gt;. Poll &lt;code&gt;GET /api/v1/compressions/{id}&lt;/code&gt; every few seconds. Once the status is &lt;code&gt;succeeded&lt;/code&gt;, read &lt;code&gt;result.downloadUrl&lt;/code&gt; and check &lt;code&gt;targetMet&lt;/code&gt;. A completed job can still miss the requested size. Stop polling on &lt;code&gt;failed&lt;/code&gt; or &lt;code&gt;canceled&lt;/code&gt; and inspect the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or connect an MCP client
&lt;/h2&gt;

&lt;p&gt;The Streamable HTTP endpoint at &lt;code&gt;https://compressgif.net/api/mcp&lt;/code&gt; exposes three tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;compress_gif&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_compression&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_usage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It uses the same API key and credit pool as REST. MCP accepts public HTTPS GIF URLs; local files should be uploaded through REST. A successful tool call starts a job—it does not mean compression has finished.&lt;/p&gt;

&lt;p&gt;Free API access currently provides 500 credits per UTC calendar month after activation with a verified Gmail-based Google sign-in and a security check. It is subject to shared monthly compute capacity; target-size jobs cost more credits than quality/size jobs. The &lt;a href="https://compressgif.net/docs" rel="noopener noreferrer"&gt;API documentation&lt;/a&gt; covers activation, limits, retention, and examples.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy distinction:&lt;/strong&gt; local browser compression keeps the GIF on your device. REST and MCP process it in the cloud, where file access expires after 24 hours. Importing a URL in the browser also contacts the image’s host.&lt;/p&gt;

&lt;p&gt;If you work with GIFs in documentation or developer workflows, I’d appreciate feedback: which matters most to you—readable UI text, predictable file size, or batch automation?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://compressgif.net/" rel="noopener noreferrer"&gt;Try CompressGIF&lt;/a&gt; · &lt;a href="https://compressgif.net/docs" rel="noopener noreferrer"&gt;REST and MCP docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>api</category>
      <category>mcp</category>
    </item>
    <item>
      <title>PDF to Markdown: local conversion, cloud OCR, and the trade-offs</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:37:43 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/pdf-to-markdown-local-conversion-cloud-ocr-and-the-trade-offs-coj</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/pdf-to-markdown-local-conversion-cloud-ocr-and-the-trade-offs-coj</guid>
      <description>&lt;p&gt;Getting text out of a PDF is easy to demonstrate and surprisingly difficult to get right.&lt;/p&gt;

&lt;p&gt;A document can look perfectly readable while producing broken paragraphs, scrambled columns, or a table that turns into a wall of text. Scanned PDFs add another problem: there may be no text layer to extract at all.&lt;/p&gt;

&lt;p&gt;I'm Leo, the maker of &lt;a href="https://pdf-to-md.app/" rel="noopener noreferrer"&gt;PDF to MD&lt;/a&gt;. It converts documents into editable Markdown, with local browser processing for text documents and a separate cloud OCR mode for scans.&lt;/p&gt;

&lt;p&gt;Here's how the workflow works, and where it still needs human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start by distinguishing text PDFs from scans
&lt;/h2&gt;

&lt;p&gt;Before choosing a conversion method, try selecting a sentence in your PDF.&lt;/p&gt;

&lt;p&gt;If you can select individual words, the document probably has a text layer. Local extraction is a reasonable starting point, though an existing text layer doesn't guarantee correct reading order.&lt;/p&gt;

&lt;p&gt;If each page behaves like a single image, you'll generally need OCR.&lt;/p&gt;

&lt;p&gt;Some PDFs contain both types of pages, so it's worth checking more than the first page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local conversion for text documents
&lt;/h2&gt;

&lt;p&gt;PDF to MD can convert text-based PDFs, DOCX files, and HTML locally in the browser, without uploading file contents.&lt;/p&gt;

&lt;p&gt;The workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose your documents.&lt;/li&gt;
&lt;li&gt;Convert them to Markdown.&lt;/li&gt;
&lt;li&gt;Review and edit the output.&lt;/li&gt;
&lt;li&gt;Copy the Markdown, download individual files, or export a batch as a ZIP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Local conversion is free. It's useful when you want to turn a document into notes, documentation, or text you can work with in another tool.&lt;/p&gt;

&lt;p&gt;For example, an Obsidian workflow is to download the &lt;code&gt;.md&lt;/code&gt; file, place it in your vault, and add your own links and annotations. This is a file-based workflow; PDF to MD is a web app, not an Obsidian plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud OCR for scanned documents
&lt;/h2&gt;

&lt;p&gt;Scanned PDFs and images need a different processing path.&lt;/p&gt;

&lt;p&gt;PDF to MD offers an optional cloud OCR mode for those files. Unlike local conversion, this mode uploads the document for processing.&lt;/p&gt;

&lt;p&gt;The distinction matters: a tool offering local processing does not mean every feature runs locally. Choose the mode based on both the document type and whether you're comfortable uploading it.&lt;/p&gt;

&lt;p&gt;Cloud features use account quotas, with paid plans and OCR packs available for additional usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown still needs a review step
&lt;/h2&gt;

&lt;p&gt;A PDF describes a page layout. Markdown describes document structure. Converting between them involves interpretation.&lt;/p&gt;

&lt;p&gt;These are the areas I would check before using the output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reading order:&lt;/strong&gt; Do paragraphs from separate columns appear in the right sequence?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headings:&lt;/strong&gt; Did section titles become headings, or just isolated lines?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tables:&lt;/strong&gt; Are values still associated with the correct row and column?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formulas:&lt;/strong&gt; Are symbols and relationships preserved accurately?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeated content:&lt;/strong&gt; Did page headers and footers end up inside the body?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Complex tables, formulas, and multi-column layouts can still need manual correction. Local PDF conversion also does not extract embedded images.&lt;/p&gt;

&lt;p&gt;A Markdown file that renders successfully can still contain extraction errors. For technical or research material, compare important passages against the original.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the output in automated workflows
&lt;/h2&gt;

&lt;p&gt;For repeated document processing, PDF to MD also provides a REST API and a remote MCP endpoint.&lt;/p&gt;

&lt;p&gt;These offer another way to access conversion from scripts and compatible AI tools. They are separate from the free local browser workflow and use service quotas.&lt;/p&gt;

&lt;p&gt;You can find the integration details in the &lt;a href="https://pdf-to-md.app/developers" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whether the output goes into a notes app, documentation system, or AI workflow, checking the extracted text first is still worthwhile. A downstream tool cannot reliably recover a table whose columns were mixed up during conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it with a document you know well
&lt;/h2&gt;

&lt;p&gt;The easiest way to evaluate a converter is with a document whose contents you already understand. Start with a short text PDF, inspect the Markdown, and then try a more demanding layout.&lt;/p&gt;

&lt;p&gt;You can try &lt;a href="https://pdf-to-md.app/" rel="noopener noreferrer"&gt;PDF to MD here&lt;/a&gt;. Free local conversion is available alongside optional cloud features.&lt;/p&gt;

&lt;p&gt;I'd appreciate feedback from people moving documents into Markdown: which causes you the most cleanup work—tables, reading order, scanned pages, or something else?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>markdown</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a private tarot calculator with Astro and zero backend</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:45:06 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/building-a-private-tarot-calculator-with-astro-and-zero-backend-14e2</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/building-a-private-tarot-calculator-with-astro-and-zero-backend-14e2</guid>
      <description>&lt;p&gt;I recently built &lt;a href="https://meuarcanopessoal.net/" rel="noopener noreferrer"&gt;Meu Arcano Pessoal&lt;/a&gt;, a tarot calculator for Brazilian Portuguese users.&lt;/p&gt;

&lt;p&gt;The site calculates a personal Major Arcana card from a birth date, with an optional name-based method. It also includes an annual Arcana calculator, relationship compatibility readings, and pages explaining all 22 Major Arcana cards.&lt;/p&gt;

&lt;p&gt;The subject is tarot, but the project involved familiar web development questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How can an interactive calculator work without a backend?&lt;/li&gt;
&lt;li&gt;How should personal inputs be handled without collecting them?&lt;/li&gt;
&lt;li&gt;How can one calculator grow into a useful content site?&lt;/li&gt;
&lt;li&gt;How do you present symbolic results without making false predictions?&lt;/li&gt;
&lt;li&gt;How much JavaScript does the site need?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built it with Astro, TypeScript, browser APIs, and Cloudflare Pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I kept the calculation in the browser
&lt;/h2&gt;

&lt;p&gt;The calculator asks for a birth date. One optional calculation method can also use a name.&lt;/p&gt;

&lt;p&gt;Neither input needs to leave the device. A server would add complexity and create a privacy problem without improving the result.&lt;/p&gt;

&lt;p&gt;The calculation follows this flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user enters a date.&lt;/li&gt;
&lt;li&gt;The browser validates it.&lt;/li&gt;
&lt;li&gt;TypeScript calculates a number between 1 and 22.&lt;/li&gt;
&lt;li&gt;The corresponding Arcana appears on the page.&lt;/li&gt;
&lt;li&gt;The input disappears when the page closes or reloads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The site does not place birth dates or names in URLs, &lt;code&gt;localStorage&lt;/code&gt;, cookies, analytics events, or network requests.&lt;/p&gt;

&lt;p&gt;There is no account system, database, API route, or serverless function to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reduction function
&lt;/h2&gt;

&lt;p&gt;The site maps a number to one of the 22 Major Arcana cards. When a total is greater than 22, the calculator adds its digits again.&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reduceToArcano&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="kr"&gt;number&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&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;abs&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;trunc&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="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;22&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="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;result&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;)&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="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digit&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;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digit&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;For a personal Arcana calculation, the site adds the digits in the birth date before applying the reduction:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculatePersonalArcano&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="nb"&gt;Date&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&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;getDate&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;getMonth&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="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&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="nf"&gt;join&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="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="p"&gt;)&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="nb"&gt;Number&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;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digits&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="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digit&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;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;digit&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;return&lt;/span&gt; &lt;span class="nf"&gt;reduceToArcano&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&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;Input validation happens before this function runs. The form rejects impossible dates and future dates instead of letting JavaScript normalize them into another day.&lt;/p&gt;

&lt;p&gt;For example, JavaScript can turn April 31 into a date in May. The calculator should not produce a result from a date the user never entered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling tarot numbering differences
&lt;/h2&gt;

&lt;p&gt;Tarot decks do not all use the same numbering.&lt;/p&gt;

&lt;p&gt;In the Rider-Waite system, Strength is number 8 and Justice is number 11. Some traditions reverse those numbers. The Fool may appear as 0, 22, or remain unnumbered.&lt;/p&gt;

&lt;p&gt;The site uses this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8  A Força
11 A Justiça
22 O Louco
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I documented this decision on the methodology page instead of hiding it inside the calculation.&lt;/p&gt;

&lt;p&gt;Two calculators can receive the same birth date and show different card names because they use different numbering systems. The formula may be working correctly in both cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a name-based calculation
&lt;/h2&gt;

&lt;p&gt;The optional name method uses a Pythagorean letter mapping.&lt;/p&gt;

&lt;p&gt;Before assigning values, the browser normalizes the input:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeName&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="kr"&gt;string&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFD&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0300-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;036f&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/ç/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/gi&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&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;This lets names containing &lt;code&gt;á&lt;/code&gt;, &lt;code&gt;ã&lt;/code&gt;, &lt;code&gt;ê&lt;/code&gt;, or &lt;code&gt;ç&lt;/code&gt; use the same mapping as their base Latin letters.&lt;/p&gt;

&lt;p&gt;I kept the date-only method available because asking for a full name creates friction and changes the interpretation. The interface explains what each method uses before the user chooses one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annual Arcana without requesting a birth year
&lt;/h2&gt;

&lt;p&gt;The annual calculator needs a birth day, birth month, and target year. It does not need the user’s birth year.&lt;/p&gt;

&lt;p&gt;Requesting the full birth date would collect an extra piece of personal information for no reason.&lt;/p&gt;

&lt;p&gt;The calculation looks like this:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateAnnualArcano&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;day&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="nx"&gt;month&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="nx"&gt;year&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="kr"&gt;number&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;reduceToArcano&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;year&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;A person born on September 14 calculating the result for 2026 gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;14 + 9 + 2026 = 2049
2 + 0 + 4 + 9 = 15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is Arcana 15.&lt;/p&gt;

&lt;p&gt;The annual tool supports 2026 and 2027. Each result includes prompts for relationships, work, change, and one practical action. It does not claim to predict a specific event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compatibility without fake precision
&lt;/h2&gt;

&lt;p&gt;Relationship calculators often return scores such as “87% compatible.” I did not have a defensible way to calculate that number, so this tool does not produce one.&lt;/p&gt;

&lt;p&gt;The compatibility page calculates two individual Arcana cards and describes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what each person may bring to the relationship;&lt;/li&gt;
&lt;li&gt;differences in communication;&lt;/li&gt;
&lt;li&gt;what each person may need;&lt;/li&gt;
&lt;li&gt;likely points of friction;&lt;/li&gt;
&lt;li&gt;a question or exercise they can try together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users can select love, friendship, or work as the context.&lt;/p&gt;

&lt;p&gt;Swapping the two birth dates changes the display order but not the shared interpretation. Equal Arcana results receive a combined explanation so the page does not repeat the same paragraph twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  One data source for 22 pages
&lt;/h2&gt;

&lt;p&gt;Each Arcana lives in a typed data object:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Arcano&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;number&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;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;slug&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;summary&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;love&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;work&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;strengths&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;cautions&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;related&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;image&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Astro uses this data to generate the 22 detail pages at build time:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getStaticPaths&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;arcanos&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;arcano&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="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;slug&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;arcano&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="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;arcano&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&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;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;arcano&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;The browser downloads no card database after the page loads. Search engines and visitors receive complete HTML.&lt;/p&gt;

&lt;p&gt;The same data powers the calculator results, card directory, related-card links, metadata, and share images. Keeping those features attached to one source prevents card names and descriptions from drifting between pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating share cards with Canvas
&lt;/h2&gt;

&lt;p&gt;Users can export their result as a 1080 by 1350 image. That size works well for vertical social posts.&lt;/p&gt;

&lt;p&gt;The browser draws the card with the Canvas API:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1350&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&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;context&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;Canvas is not available&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#0c0618&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ead08b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;48px serif&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete version also loads the Arcana image and adds a short meaning.&lt;/p&gt;

&lt;p&gt;If the browser supports file sharing, the site uses the Web Share API. Otherwise, it downloads the PNG.&lt;/p&gt;

&lt;p&gt;The image contains the result, but no name or birth date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics without personal inputs
&lt;/h2&gt;

&lt;p&gt;I use Plausible to record a few product events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculo_concluido
resultado_aberto
compartilhamento
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Events can include fixed categories such as the tool name, selected year, relationship mode, or sharing method.&lt;/p&gt;

&lt;p&gt;They do not include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;names;&lt;/li&gt;
&lt;li&gt;birth dates;&lt;/li&gt;
&lt;li&gt;Arcana numbers;&lt;/li&gt;
&lt;li&gt;generated interpretations;&lt;/li&gt;
&lt;li&gt;form values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I want to know whether visitors finish a calculation and whether the sharing tool works. I do not need to know what they entered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static deployment on Cloudflare
&lt;/h2&gt;

&lt;p&gt;Astro builds the site into static files. Cloudflare Pages serves those files behind the production domain.&lt;/p&gt;

&lt;p&gt;The deployment has no runtime application server. Cloudflare handles HTTPS, caching, DNS, response headers, and the redirect from &lt;code&gt;www&lt;/code&gt; to the root domain.&lt;/p&gt;

&lt;p&gt;The site also includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;canonical URLs;&lt;/li&gt;
&lt;li&gt;Open Graph metadata;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sitemap.xml&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;breadcrumb structured data;&lt;/li&gt;
&lt;li&gt;FAQ structured data where the questions are visible;&lt;/li&gt;
&lt;li&gt;an &lt;code&gt;llms.txt&lt;/code&gt; file;&lt;/li&gt;
&lt;li&gt;a privacy policy and methodology page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Static generation does not remove the need for JavaScript. It limits JavaScript to the places where it earns its bytes: calculators, search, result interaction, and image sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that took longer than the calculator
&lt;/h2&gt;

&lt;p&gt;The formula was a small part of the project.&lt;/p&gt;

&lt;p&gt;Writing useful interpretations for 22 cards took longer. Each page needed to answer the searches that brought someone there, including &lt;code&gt;arcano 11&lt;/code&gt;, &lt;code&gt;arcano pessoal 17&lt;/code&gt;, and &lt;code&gt;significado do arcano 18&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The card illustrations also needed review. A polished image can still be wrong if it misses the symbols that identify a Rider-Waite card. I checked the characters, posture, objects, animals, background elements, and card-specific composition instead of treating the images as decoration.&lt;/p&gt;

&lt;p&gt;The methodology page became another important part of the site. It explains why calculators disagree, how names are normalized, which numbering system the site uses, and what to do when an interpretation does not fit.&lt;/p&gt;

&lt;p&gt;Those answers help users more than another paragraph filled with broad spiritual language.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build next
&lt;/h2&gt;

&lt;p&gt;I plan to watch which tools people use before adding more pages.&lt;/p&gt;

&lt;p&gt;The current site covers personal, annual, and relationship calculations. A future addition should answer a recurring user question or improve an existing result. Publishing hundreds of thin combinations would increase the page count without making the site better.&lt;/p&gt;

&lt;p&gt;You can try the project here:&lt;/p&gt;

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

&lt;p&gt;The methodology and formulas are available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://meuarcanopessoal.net/metodologia/" rel="noopener noreferrer"&gt;https://meuarcanopessoal.net/metodologia/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>sideprojects</category>
      <category>astro</category>
    </item>
    <item>
      <title>How I Built a Source-Constrained Game Guide with Astro and Cloudflare Pages</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:29:17 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/how-i-built-a-source-constrained-game-guide-with-astro-and-cloudflare-pages-2o37</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/how-i-built-a-source-constrained-game-guide-with-astro-and-cloudflare-pages-2o37</guid>
      <description>&lt;p&gt;I recently built &lt;a href="https://ballscratchsimulator.wiki/" rel="noopener noreferrer"&gt;Ball Scratch Simulator Guide&lt;/a&gt;, an independent information site for an upcoming Steam game from 10ft Games.&lt;/p&gt;

&lt;p&gt;The technical work was straightforward. The difficult part was deciding what the site should say when the game has not been released and much of the information people search for does not exist yet.&lt;/p&gt;

&lt;p&gt;Instead of filling those gaps with guesses, I built the site around a simple editorial constraint:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every factual claim must be traceable to an official public source. Anything that has not been announced must be described as unknown.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That decision shaped the content model, page structure, SEO setup, and deployment architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with covering an unreleased game
&lt;/h2&gt;

&lt;p&gt;Searches for an upcoming game often appear before enough information exists to support a conventional guide.&lt;/p&gt;

&lt;p&gt;Players may already be looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The release date&lt;/li&gt;
&lt;li&gt;The price&lt;/li&gt;
&lt;li&gt;A playable demo&lt;/li&gt;
&lt;li&gt;Controls&lt;/li&gt;
&lt;li&gt;VR support&lt;/li&gt;
&lt;li&gt;System requirements&lt;/li&gt;
&lt;li&gt;Locations&lt;/li&gt;
&lt;li&gt;Collectibles&lt;/li&gt;
&lt;li&gt;Achievements&lt;/li&gt;
&lt;li&gt;Codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Ball Scratch Simulator, the official Steam listing currently confirms several useful details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is a Windows game developed by 10ft Games.&lt;/li&gt;
&lt;li&gt;Steam lists an August 2026 release window.&lt;/li&gt;
&lt;li&gt;Desktop play is supported.&lt;/li&gt;
&lt;li&gt;Seated PC VR is optional through OpenXR and SteamVR.&lt;/li&gt;
&lt;li&gt;The game includes six public locations.&lt;/li&gt;
&lt;li&gt;Cards, Chibis, high scores, and leaderboards are part of the progression system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other details remain unannounced, including the exact release day, launch price, demo availability, location names, and complete controls.&lt;/p&gt;

&lt;p&gt;The site answers those questions directly. “Not announced” is more useful than a fabricated answer hidden inside a long article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I launched with one indexable page
&lt;/h2&gt;

&lt;p&gt;A common SEO approach is to create a page for every possible query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/release-date/
/codes/
/controls/
/locations/
/collectibles/
/vr/
/system-requirements/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure would have created several thin pages containing the same limited facts. Some pages, such as codes or walkthroughs, would have no factual basis at all.&lt;/p&gt;

&lt;p&gt;I chose one indexable homepage that covers the current search intent in depth. It includes release information, gameplay, progression, VR support, PC requirements, languages, content warnings, and frequently asked questions.&lt;/p&gt;

&lt;p&gt;The site also has About, Privacy, Terms, Editorial Policy, and Disclaimer pages. Those pages are useful to visitors, but they are marked &lt;code&gt;noindex,follow&lt;/code&gt; and excluded from the sitemap.&lt;/p&gt;

&lt;p&gt;The sitemap therefore contains one URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://ballscratchsimulator.wiki/&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More pages can be added after the game launches, when there is enough verified information for each page to serve a distinct purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small Astro architecture
&lt;/h2&gt;

&lt;p&gt;The site uses Astro with static output. There is no client-side application framework, CMS, database, or runtime API.&lt;/p&gt;

&lt;p&gt;The core configuration is deliberately small:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&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;astro/config&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;sitemap&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;@astrojs/sitemap&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;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://ballscratchsimulator.wiki&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="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;static&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;trailingSlash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;always&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&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;url&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;url&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;site&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="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;Static output makes the site easy to inspect. The main content, navigation, metadata, and links are present in the generated HTML without requiring JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping game facts in one typed object
&lt;/h2&gt;

&lt;p&gt;Important identity data lives in a typed object instead of being repeated across templates:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GameIdentity&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ball Scratch Simulator&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;steamAppId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4951590&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10ft Games&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10ft Games&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Windows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;releaseStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upcoming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;releaseWindow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;August 2026&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;August 7, 2026&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;vr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;required&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="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Seated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runtimes&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="s2"&gt;OpenXR&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;SteamVR&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;The page copy, metadata, calls to action, and &lt;code&gt;VideoGame&lt;/code&gt; structured data read from this object.&lt;/p&gt;

&lt;p&gt;This reduces the chance of publishing conflicting release windows or platform information in different parts of the site. When an official detail changes, there is one primary place to update it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canonical URLs and structured data
&lt;/h2&gt;

&lt;p&gt;Every page receives a self-referencing canonical URL:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canonical&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Astro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SITE_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared layout also creates Open Graph metadata, Twitter card metadata, &lt;code&gt;WebSite&lt;/code&gt; structured data, and page-specific structured data.&lt;/p&gt;

&lt;p&gt;The homepage includes a &lt;code&gt;VideoGame&lt;/code&gt; JSON-LD object using the same identity data shown to visitors. I avoided adding review scores, prices, release dates, or offers that are not supported by the official sources.&lt;/p&gt;

&lt;p&gt;Structured data should describe the visible page. It should not become a second, more optimistic version of the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treating official information carefully
&lt;/h2&gt;

&lt;p&gt;Source-constrained publishing still requires judgment.&lt;/p&gt;

&lt;p&gt;The Steam system requirements, for example, currently list GPU recommendations in an order that may look surprising. It would be easy to “correct” them based on an assumption.&lt;/p&gt;

&lt;p&gt;The site displays the values as Steam currently lists them and adds a short note explaining where they came from.&lt;/p&gt;

&lt;p&gt;That preserves the source instead of silently rewriting it.&lt;/p&gt;

&lt;p&gt;The same rule applies to the six confirmed locations. The site can explain that six public settings are planned, but it does not invent names for those locations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing promotional-image cropping
&lt;/h2&gt;

&lt;p&gt;The first version placed official promotional art inside fixed-ratio containers using &lt;code&gt;object-fit: cover&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That worked visually for generic photographs, but it cropped important parts of the game artwork. Titles and characters near the edges disappeared on wider screens.&lt;/p&gt;

&lt;p&gt;The fix was native image sizing:&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="nc"&gt;.hero-art&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="nb"&gt;block&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="nb"&gt;auto&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;--line&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;Preserving the image’s intrinsic aspect ratio was better than trying to find a universal crop position.&lt;/p&gt;

&lt;p&gt;For artwork containing text, logos, or characters, &lt;code&gt;cover&lt;/code&gt; often solves the container’s problem by creating an image-content problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to Cloudflare Pages
&lt;/h2&gt;

&lt;p&gt;The generated site is deployed to Cloudflare Pages through Wrangler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npx wrangler pages deploy dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The custom domain is:&lt;/p&gt;

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

&lt;p&gt;The &lt;code&gt;www&lt;/code&gt; hostname redirects to the root domain with a permanent redirect. Cloudflare also serves the site’s security headers, including a Content Security Policy.&lt;/p&gt;

&lt;p&gt;Because the site includes Google Analytics and Plausible, their external script origins must be explicitly allowed by the CSP. Adding analytics without updating the policy would cause the browser to block the scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification before deployment
&lt;/h2&gt;

&lt;p&gt;The project uses a small verification chain:&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="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;"check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"astro check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node --test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"astro build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verify"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run check &amp;amp;&amp;amp; npm test &amp;amp;&amp;amp; npm run build"&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;The tests check practical publishing requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every planned route exists.&lt;/li&gt;
&lt;li&gt;Each page has one H1.&lt;/li&gt;
&lt;li&gt;The homepage has a valid canonical URL.&lt;/li&gt;
&lt;li&gt;Structured data matches visible game information.&lt;/li&gt;
&lt;li&gt;Trust and legal pages use &lt;code&gt;noindex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The sitemap contains only the homepage.&lt;/li&gt;
&lt;li&gt;Internal links resolve.&lt;/li&gt;
&lt;li&gt;Core answers remain available without JavaScript.&lt;/li&gt;
&lt;li&gt;Draft instructions and placeholder text do not leak into public pages.&lt;/li&gt;
&lt;li&gt;External scripts are limited to known analytics providers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also check the generated &lt;code&gt;robots.txt&lt;/code&gt;, &lt;code&gt;sitemap.xml&lt;/code&gt;, and &lt;code&gt;llms.txt&lt;/code&gt; instead of assuming the build integration produced the expected files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The strongest content decision was allowing “unknown” to be a complete answer.&lt;/p&gt;

&lt;p&gt;If the price has not been announced, the page says so. If no demo is available, it does not offer a fake download button. If location names are unavailable, it does not turn speculation into a list.&lt;/p&gt;

&lt;p&gt;Page count should also follow available evidence. A single useful page is a better starting point than ten pages repeating the same paragraph.&lt;/p&gt;

&lt;p&gt;Static generation made these constraints easier to enforce. The final HTML can be tested directly, and the deployed output has fewer moving parts.&lt;/p&gt;

&lt;p&gt;The site will expand when the game is released and official information supports dedicated guides. Until then, its job is narrower: collect the confirmed details, answer current questions, and make the boundaries clear.&lt;/p&gt;

&lt;p&gt;You can see the finished site at &lt;a href="https://ballscratchsimulator.wiki/" rel="noopener noreferrer"&gt;ballscratchsimulator.wiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I would be interested in feedback on the source-constrained editorial model, the Astro implementation, or the decision to keep the initial sitemap to one page.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>cloudflarechallenge</category>
    </item>
    <item>
      <title>How I Built a Source-Tracked Roblox Wiki with Astro and No Database</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:16:27 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/how-i-built-a-source-tracked-roblox-wiki-with-astro-and-no-database-1610</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/how-i-built-a-source-tracked-roblox-wiki-with-astro-and-no-database-1610</guid>
      <description>&lt;p&gt;I built &lt;a href="https://anime-expeditions-wiki.org/" rel="noopener noreferrer"&gt;Anime Expeditions Wiki&lt;/a&gt; as an independent guide for the Roblox experience Anime Expeditions.&lt;/p&gt;

&lt;p&gt;The site helps players find current codes, compare units, plan early progression, and verify that they are joining the correct Roblox experience. It now contains 47 indexable routes across seven languages.&lt;/p&gt;

&lt;p&gt;The technical stack is small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Astro and TypeScript&lt;/li&gt;
&lt;li&gt;Static deployment on Cloudflare Pages&lt;/li&gt;
&lt;li&gt;No database&lt;/li&gt;
&lt;li&gt;No CMS&lt;/li&gt;
&lt;li&gt;No server functions&lt;/li&gt;
&lt;li&gt;No public API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The harder part was handling information that changes faster than the pages around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The maintenance problem: uncertainty
&lt;/h2&gt;

&lt;p&gt;Game guides become outdated fast. Codes expire, updates change upgrade requirements, and community tier lists often disagree.&lt;/p&gt;

&lt;p&gt;A page can show a recent publication date while its tables still describe an older game version. Copying the newest value into a database would not solve that problem. I needed to record where each claim came from and the conditions under which it was useful.&lt;/p&gt;

&lt;p&gt;I treated changing gameplay claims as dated evidence records.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;EvidenceRecord&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;sourceIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&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;evidenceLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EvidenceLevel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;verifiedAt&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&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;reported&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;disputed&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;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;applicableVersion&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each record answers five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which sources support this claim?&lt;/li&gt;
&lt;li&gt;What kind of source supplied it?&lt;/li&gt;
&lt;li&gt;When did someone check it?&lt;/li&gt;
&lt;li&gt;Do current sources agree?&lt;/li&gt;
&lt;li&gt;Which game version does it describe?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The public page displays this information beside the value. Readers do not have to search a general sources page to learn whether a code or recipe comes from an official record, a community guide, or conflicting reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping disagreements visible
&lt;/h2&gt;

&lt;p&gt;The code list shows why the status field matters.&lt;/p&gt;

&lt;p&gt;At the July 21 snapshot, the wiki tracked 15 codes. Eight had matching reports that they were active. Seven had conflicting reports.&lt;/p&gt;

&lt;p&gt;I kept the disputed codes visible and added notes describing which sources disagreed. Choosing one answer would hide useful information. Combining several reports into a new answer would create a claim that no source published.&lt;/p&gt;

&lt;p&gt;Tier lists have the same problem. The wiki compares three public rankings side by side instead of calculating a new score.&lt;/p&gt;

&lt;p&gt;If one publisher does not list a unit, the table displays “Not listed.” It does not convert the missing entry into the lowest grade.&lt;/p&gt;

&lt;p&gt;The data model allows disagreement to remain a valid state. Editors can update a record later when stronger evidence appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  One dataset for 47 routes
&lt;/h2&gt;

&lt;p&gt;Gameplay records live in TypeScript data modules. Astro components decide how to present them.&lt;/p&gt;

&lt;p&gt;The current dataset includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 tracked codes&lt;/li&gt;
&lt;li&gt;33 units&lt;/li&gt;
&lt;li&gt;17 traits&lt;/li&gt;
&lt;li&gt;Evolution recipes&lt;/li&gt;
&lt;li&gt;Five story maps&lt;/li&gt;
&lt;li&gt;Equipment and resource records&lt;/li&gt;
&lt;li&gt;35 source entries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The static build produces 47 indexable routes. English has 17 pages. Spanish, Brazilian Portuguese, German, French, Japanese, and Korean each have five core pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Homepage&lt;/li&gt;
&lt;li&gt;Codes&lt;/li&gt;
&lt;li&gt;Beginner guide&lt;/li&gt;
&lt;li&gt;Tier list&lt;/li&gt;
&lt;li&gt;Official links&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All seven languages share the same gameplay records. Only interface text and explanatory copy vary by locale.&lt;/p&gt;

&lt;p&gt;When a code changes status, I update one record instead of editing seven tables. This also prevents a translated page from keeping an older reward after the English page changes.&lt;/p&gt;

&lt;p&gt;Astro generates canonical URLs, &lt;code&gt;hreflang&lt;/code&gt; links, metadata, and the sitemap from the route definitions. A missing locale route fails during verification instead of producing an orphaned page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the correct Roblox experience
&lt;/h2&gt;

&lt;p&gt;Roblox uses several identifiers for one experience. Anime Expeditions has a Place ID, Universe ID, and creator Group ID.&lt;/p&gt;

&lt;p&gt;A title search cannot prove that a result belongs to the expected creator, especially when several experiences use similar names.&lt;/p&gt;

&lt;p&gt;The project fixes these identifiers in its configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Place ID: &lt;code&gt;84515722934860&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Universe ID: &lt;code&gt;7613921865&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creator: &lt;code&gt;Expeditions Entertainment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Group ID: &lt;code&gt;35929511&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A synchronization script checks the Place-to-Universe relationship through Roblox. It finds the matching search record, verifies the creator, and confirms that approved promotional images belong to the expected universe.&lt;/p&gt;

&lt;p&gt;The script stops without writing when an identity field does not match.&lt;/p&gt;

&lt;p&gt;A changed title may be a legitimate update. A different creator or Universe ID means the automation may have found the wrong experience.&lt;/p&gt;

&lt;p&gt;The site also keeps a small whitelist of official Roblox and YouTube destinations. A data update cannot introduce an unreviewed game, group, or media URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating stable facts
&lt;/h2&gt;

&lt;p&gt;A daily GitHub Actions workflow checks official Roblox metadata.&lt;/p&gt;

&lt;p&gt;It compares stable fields with a stored snapshot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name&lt;/li&gt;
&lt;li&gt;Description&lt;/li&gt;
&lt;li&gt;Canonical path&lt;/li&gt;
&lt;li&gt;Content maturity&lt;/li&gt;
&lt;li&gt;Age recommendation&lt;/li&gt;
&lt;li&gt;Creator&lt;/li&gt;
&lt;li&gt;Place and Universe IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow ignores player counts, vote totals, promotion state, and thumbnail URLs. Those values change often and do not justify a content update.&lt;/p&gt;

&lt;p&gt;The identity check passes its verified search result to the snapshot step. Reusing that result avoids calling Roblox’s rate-limited search endpoint twice during one workflow run.&lt;/p&gt;

&lt;p&gt;When stable official metadata changes, the workflow can update the snapshot and deploy the static site.&lt;/p&gt;

&lt;p&gt;It does not rewrite tier lists, code statuses, evolution recipes, or gameplay advice.&lt;/p&gt;

&lt;p&gt;Community information still needs review. Two guides may disagree because one tested an older server version, one misunderstood a requirement, or both copied the same incorrect source. A script cannot settle that question from the published values alone.&lt;/p&gt;

&lt;p&gt;Automation handles identity and stable platform metadata. A human reviews gameplay claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the site stays static
&lt;/h2&gt;

&lt;p&gt;Astro is the project’s only application dependency. Cloudflare Pages serves the generated files.&lt;/p&gt;

&lt;p&gt;A database would add another moving part without improving the evidence behind the content. The dataset remains small enough to review in Git, and the site rebuilds when a record changes.&lt;/p&gt;

&lt;p&gt;Static output also makes verification easier. The build process can inspect the HTML that will reach production.&lt;/p&gt;

&lt;p&gt;The validator checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page titles and descriptions&lt;/li&gt;
&lt;li&gt;Canonical URLs&lt;/li&gt;
&lt;li&gt;Alternate language links&lt;/li&gt;
&lt;li&gt;Internal links&lt;/li&gt;
&lt;li&gt;Source references&lt;/li&gt;
&lt;li&gt;Structured data&lt;/li&gt;
&lt;li&gt;Local media&lt;/li&gt;
&lt;li&gt;Security requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser JavaScript handles a short list of enhancements, including table search, filters, code copying, mobile navigation, and analytics consent.&lt;/p&gt;

&lt;p&gt;Readers can still access the main content when those enhancements fail.&lt;/p&gt;

&lt;p&gt;The official trailer uses a click-to-load YouTube embed. The browser does not contact YouTube until the visitor starts the video. Google Analytics also stays unloaded until a visitor opts in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing content relationships
&lt;/h2&gt;

&lt;p&gt;The test suite checks content assumptions that TypeScript cannot protect on its own.&lt;/p&gt;

&lt;p&gt;It verifies that every source ID is unique and that every citation points to an existing source. It also checks that tier comparisons cover the known unit roster without inventing grades for omitted units.&lt;/p&gt;

&lt;p&gt;Other tests protect the update and publishing workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The official Roblox identity must remain fixed.&lt;/li&gt;
&lt;li&gt;The update check must reuse verified records.&lt;/li&gt;
&lt;li&gt;Roblox requests must back off after rate limits.&lt;/li&gt;
&lt;li&gt;Failed identity checks must stop without writing.&lt;/li&gt;
&lt;li&gt;Media must stay local and attributed.&lt;/li&gt;
&lt;li&gt;YouTube and analytics must remain inactive until requested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current suite contains 13 tests. The Astro build generates 48 pages, including the 404 page, and the validator checks all 47 indexable routes.&lt;/p&gt;

&lt;p&gt;A valid TypeScript object can still cite a missing source, publish the wrong canonical domain, or send a translated page to an English URL. These checks catch those mistakes before deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this approach
&lt;/h2&gt;

&lt;p&gt;The static workflow makes editorial changes slower than editing a database through an admin panel. Each change passes through the repository, tests, and deployment.&lt;/p&gt;

&lt;p&gt;That cost fits the current project. A review step helps when one incorrect value could cause players to waste limited resources.&lt;/p&gt;

&lt;p&gt;Only five core pages have complete translations. Translating all 17 English pages would add substantial review work. Shared gameplay data prevents values from drifting, but translated explanations still need language-specific review.&lt;/p&gt;

&lt;p&gt;The evidence model also stays small. It records the source, status, date, and applicable version without calculating a confidence percentage.&lt;/p&gt;

&lt;p&gt;A confidence score would suggest more precision than the source material supports. Showing that two current guides disagree gives readers clearer information than assigning the claim a score of 62 percent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Dates and versions belong beside changing claims. A site-wide “last updated” date says little about an individual code or recipe.&lt;/p&gt;

&lt;p&gt;Source disagreement deserves its own state. Treating conflict as an error encourages maintainers to choose an answer before they have enough evidence.&lt;/p&gt;

&lt;p&gt;Automation works best on facts with stable identifiers and clear failure conditions. Editorial judgment needs a review boundary.&lt;/p&gt;

&lt;p&gt;The project remains a static Astro site because its current scale does not need more infrastructure. The useful part of the system is deciding what the build should refuse to publish.&lt;/p&gt;

&lt;p&gt;You can explore the finished project at &lt;a href="https://anime-expeditions-wiki.org/" rel="noopener noreferrer"&gt;Anime Expeditions Wiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Anime Expeditions Wiki is an independent fan project and is not affiliated with Roblox, Expeditions Entertainment, or any anime rights holder.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Installing Verity Mod Safely: Java vs Bedrock Setup Guide</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:14:16 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/installing-verity-mod-safely-java-vs-bedrock-setup-guide-482c</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/installing-verity-mod-safely-java-vs-bedrock-setup-guide-482c</guid>
      <description>&lt;p&gt;Most Verity Mod installation problems begin before Minecraft even launches.&lt;/p&gt;

&lt;p&gt;A player searches for “Verity Mod,” opens the first familiar-looking result, and installs it without checking the Minecraft edition, author, Project ID, loader, or game version.&lt;/p&gt;

&lt;p&gt;That is risky because &lt;strong&gt;Verity is not one interchangeable download&lt;/strong&gt;. There is a Java project, multiple Bedrock adaptations, modpacks, and unrelated projects using similar names.&lt;/p&gt;

&lt;p&gt;This guide provides a practical way to identify the correct project and troubleshoot it without relying on unofficial file mirrors.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Project information in this article was last checked on July 21, 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is Verity Mod?
&lt;/h2&gt;

&lt;p&gt;Verity is a Minecraft horror-companion concept associated with ThatMob’s video series.&lt;/p&gt;

&lt;p&gt;Playable adaptations introduce a yellow helper character that can answer questions, interact with the player, and eventually produce unsettling behavior or horror events.&lt;/p&gt;

&lt;p&gt;The important detail is that the Java and Bedrock versions are separate implementations.&lt;/p&gt;

&lt;p&gt;They do not share the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File format&lt;/li&gt;
&lt;li&gt;Mod loader&lt;/li&gt;
&lt;li&gt;Dependencies&lt;/li&gt;
&lt;li&gt;AI configuration&lt;/li&gt;
&lt;li&gt;World settings&lt;/li&gt;
&lt;li&gt;Connection method&lt;/li&gt;
&lt;li&gt;Troubleshooting steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Java tutorial cannot safely be applied to a Bedrock add-on simply because both projects use the name Verity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify the project before installing it
&lt;/h2&gt;

&lt;p&gt;A project title is not a reliable identifier. Names and readable URL slugs can be copied, reused, or changed.&lt;/p&gt;

&lt;p&gt;Before installing anything, record these five values:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minecraft edition&lt;/li&gt;
&lt;li&gt;Project author&lt;/li&gt;
&lt;li&gt;Numeric Project ID&lt;/li&gt;
&lt;li&gt;Exact Minecraft version&lt;/li&gt;
&lt;li&gt;Loader or import method&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These values work like a package identity. If one of them differs from the tutorial you are following, assume it is a different project until proven otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Projects checked for this guide
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edition&lt;/th&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Author&lt;/th&gt;
&lt;th&gt;Project ID&lt;/th&gt;
&lt;th&gt;Checked release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;Verity JE&lt;/td&gt;
&lt;td&gt;VarmiteYT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1591438&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;5.7.3&lt;/code&gt; for Minecraft &lt;code&gt;1.20.1&lt;/code&gt; with Forge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock&lt;/td&gt;
&lt;td&gt;Verity BE&lt;/td&gt;
&lt;td&gt;Undertaletalelover&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1574632&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1.0.8&lt;/code&gt; for Bedrock &lt;code&gt;26.30&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock&lt;/td&gt;
&lt;td&gt;Verity - Bedrock Edition&lt;/td&gt;
&lt;td&gt;PnTMC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1575941&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2.1.0&lt;/code&gt; for Bedrock &lt;code&gt;26.30&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Always recheck the project page before installing. Releases and supported Minecraft versions can change after this article is published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Java or Bedrock
&lt;/h2&gt;

&lt;p&gt;Choose &lt;strong&gt;Java Edition&lt;/strong&gt; if you play Minecraft Java on a desktop computer and can create a Forge or NeoForge profile.&lt;/p&gt;

&lt;p&gt;Choose &lt;strong&gt;Bedrock Edition&lt;/strong&gt; if you play the current Minecraft release on Android, iOS, or a compatible Windows Bedrock installation.&lt;/p&gt;

&lt;p&gt;“Minecraft PE” is now commonly used as an informal name for Minecraft Bedrock on mobile. A Java mod file cannot be imported into Minecraft Bedrock or PE.&lt;/p&gt;

&lt;p&gt;A more detailed edition comparison is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://veritymod.org/java-vs-bedrock/" rel="noopener noreferrer"&gt;Verity Mod: Java vs Bedrock&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Verity JE
&lt;/h2&gt;

&lt;p&gt;The checked Java project is &lt;strong&gt;Verity JE by VarmiteYT&lt;/strong&gt;, Project ID &lt;code&gt;1591438&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its current main release is Verity JE &lt;code&gt;5.7.3&lt;/code&gt; for Minecraft &lt;code&gt;1.20.1&lt;/code&gt; with Forge.&lt;/p&gt;

&lt;p&gt;The project also lists a Minecraft &lt;code&gt;1.21.1&lt;/code&gt; NeoForge branch, but its project description warns that this branch is deprecated and has many bugs. A higher Minecraft version number does not automatically make it the better choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended Java setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a separate Minecraft &lt;code&gt;1.20.1&lt;/code&gt; profile.&lt;/li&gt;
&lt;li&gt;Install the correct Forge version for that profile.&lt;/li&gt;
&lt;li&gt;Launch Minecraft once before adding any mods.&lt;/li&gt;
&lt;li&gt;Close Minecraft and add Verity JE.&lt;/li&gt;
&lt;li&gt;Add only the dependencies listed for the selected release.&lt;/li&gt;
&lt;li&gt;Launch the small test profile before combining Verity with a larger modpack.&lt;/li&gt;
&lt;li&gt;Configure one supported AI option.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project supports an online Groq configuration and a local Ollama option.&lt;/p&gt;

&lt;p&gt;Keep API keys private. Never place a key in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public screenshots&lt;/li&gt;
&lt;li&gt;GitHub repositories&lt;/li&gt;
&lt;li&gt;Blog comments&lt;/li&gt;
&lt;li&gt;Discord messages&lt;/li&gt;
&lt;li&gt;Shared configuration files&lt;/li&gt;
&lt;li&gt;Support requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Minecraft fails before reaching the main menu, investigate the Minecraft version, Forge version, Java runtime, and dependencies before debugging the AI configuration.&lt;/p&gt;

&lt;p&gt;Official Java project page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.curseforge.com/minecraft/mc-mods/verity-je" rel="noopener noreferrer"&gt;Verity JE on CurseForge&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Verity on Bedrock
&lt;/h2&gt;

&lt;p&gt;Bedrock users must make an additional choice because there is more than one maintained adaptation.&lt;/p&gt;

&lt;p&gt;The two projects covered here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verity BE by Undertaletalelover&lt;/strong&gt;, Project ID &lt;code&gt;1574632&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verity - Bedrock Edition by PnTMC&lt;/strong&gt;, Project ID &lt;code&gt;1575941&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are separate projects with different releases, gameplay behavior, and support instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended Bedrock setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Choose one project by author and Project ID.&lt;/li&gt;
&lt;li&gt;Confirm that its release matches your exact Bedrock build.&lt;/li&gt;
&lt;li&gt;Import the add-on using Minecraft’s normal import process.&lt;/li&gt;
&lt;li&gt;Create a new test world.&lt;/li&gt;
&lt;li&gt;Activate the required behavior and resource packs.&lt;/li&gt;
&lt;li&gt;Enable the experimental settings required by that project.&lt;/li&gt;
&lt;li&gt;Follow the connection and interaction instructions from the same project page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not copy a connection command from one Bedrock project into the other. Do not assume that a tutorial for Project ID &lt;code&gt;1574632&lt;/code&gt; also applies to Project ID &lt;code&gt;1575941&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Official Bedrock project pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.curseforge.com/minecraft-bedrock/addons/verity-be" rel="noopener noreferrer"&gt;Verity BE on CurseForge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.curseforge.com/minecraft-bedrock/addons/verity-bedrock-edition" rel="noopener noreferrer"&gt;Verity - Bedrock Edition on CurseForge&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A simple troubleshooting decision tree
&lt;/h2&gt;

&lt;p&gt;When Verity does not work, identify the first stage that fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minecraft does not start
&lt;/h3&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minecraft version&lt;/li&gt;
&lt;li&gt;Forge or NeoForge version&lt;/li&gt;
&lt;li&gt;Java runtime&lt;/li&gt;
&lt;li&gt;Required dependencies&lt;/li&gt;
&lt;li&gt;Conflicting mods&lt;/li&gt;
&lt;li&gt;The profile’s actual mod directory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test with only Verity and its required dependencies before adding other mods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minecraft starts, but Verity does not appear
&lt;/h3&gt;

&lt;p&gt;For Java, confirm that the mod is loaded in the correct profile.&lt;/p&gt;

&lt;p&gt;For Bedrock, check that the behavior and resource packs are active in that world. Also confirm that the add-on supports the installed Bedrock version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verity appears but does not respond
&lt;/h3&gt;

&lt;p&gt;For Java, verify the Groq or Ollama configuration.&lt;/p&gt;

&lt;p&gt;For Bedrock, follow the connection instructions for the exact Project ID you installed. Check required experimental options and current service notices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verity only displays dots
&lt;/h3&gt;

&lt;p&gt;This may indicate an incomplete connection step or a temporary project service problem.&lt;/p&gt;

&lt;p&gt;Repeatedly reinstalling the same file usually does not solve a provider or backend issue. Check the project page and its official support channel before changing unrelated settings.&lt;/p&gt;

&lt;p&gt;The complete troubleshooting checklist is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://veritymod.org/not-working/" rel="noopener noreferrer"&gt;Fix Verity Mod Not Responding&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid unsafe and incorrect downloads
&lt;/h2&gt;

&lt;p&gt;Do not select a Verity file based only on its title, thumbnail, or download count.&lt;/p&gt;

&lt;p&gt;Before installing, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author&lt;/li&gt;
&lt;li&gt;Project ID&lt;/li&gt;
&lt;li&gt;Minecraft edition&lt;/li&gt;
&lt;li&gt;Exact game version&lt;/li&gt;
&lt;li&gt;Loader&lt;/li&gt;
&lt;li&gt;Release date&lt;/li&gt;
&lt;li&gt;Changelog&lt;/li&gt;
&lt;li&gt;Required dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid downloading mod files from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared drives&lt;/li&gt;
&lt;li&gt;URL shorteners&lt;/li&gt;
&lt;li&gt;Chat attachments&lt;/li&gt;
&lt;li&gt;Repost websites&lt;/li&gt;
&lt;li&gt;Unknown mirrors&lt;/li&gt;
&lt;li&gt;Comments claiming to preserve a removed version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A removed project is not automatically malicious. However, an untraceable copy has no reliable source, update history, or trustworthy relationship to the original author.&lt;/p&gt;

&lt;p&gt;The following guide links to project pages instead of hosting mod files:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://veritymod.org/download/" rel="noopener noreferrer"&gt;Verity Mod Download Sources&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Back up important worlds
&lt;/h2&gt;

&lt;p&gt;Horror mods and experimental add-ons may intentionally alter normal gameplay.&lt;/p&gt;

&lt;p&gt;Before testing Verity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use a separate launcher profile for Java.&lt;/li&gt;
&lt;li&gt;Create a new test world for Bedrock.&lt;/li&gt;
&lt;li&gt;Back up any world you care about.&lt;/li&gt;
&lt;li&gt;Do not test experimental releases on your only copy of a world.&lt;/li&gt;
&lt;li&gt;Add other mods gradually after the base setup works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A clean test environment is not only safer. It also makes troubleshooting much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;p&gt;Before launching Verity, confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You selected Java or Bedrock correctly.&lt;/li&gt;
&lt;li&gt;The author matches the project you intended to install.&lt;/li&gt;
&lt;li&gt;The numeric Project ID matches your tutorial.&lt;/li&gt;
&lt;li&gt;The exact Minecraft version is supported.&lt;/li&gt;
&lt;li&gt;The loader or add-on import method is correct.&lt;/li&gt;
&lt;li&gt;Required dependencies and world settings are enabled.&lt;/li&gt;
&lt;li&gt;API keys remain private.&lt;/li&gt;
&lt;li&gt;Important worlds are backed up.&lt;/li&gt;
&lt;li&gt;The file came from the project page rather than a mirror.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most Verity installation failures can be prevented by checking identity and compatibility before downloading anything.&lt;/p&gt;

&lt;p&gt;For maintained comparisons, setup instructions, and safety notes, visit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://veritymod.org/" rel="noopener noreferrer"&gt;Verity Mod Guide&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;This is an independent, unofficial guide. It is not affiliated with Mojang, Microsoft, CurseForge, ThatMob, or the developers of the referenced Verity projects.&lt;/p&gt;

</description>
      <category>minecraft</category>
      <category>gaming</category>
      <category>tutorial</category>
      <category>security</category>
    </item>
    <item>
      <title>I built Img2Gen, a GPT Image 2 studio on Cloudflare Workers</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Fri, 05 Jun 2026 04:14:57 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-img2gen-a-gpt-image-2-studio-on-cloudflare-workers-1m0e</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-img2gen-a-gpt-image-2-studio-on-cloudflare-workers-1m0e</guid>
      <description>&lt;p&gt;I recently built &lt;a href="https://img2gen.com" rel="noopener noreferrer"&gt;Img2Gen&lt;/a&gt;, a focused web studio for GPT Image 2 generation.&lt;/p&gt;

&lt;p&gt;The idea was simple: I wanted something more practical than a single prompt box. For real creative work, I often need to try multiple aspect ratios, upload reference images, generate several variants, download the results, and keep a history of what worked.&lt;/p&gt;

&lt;p&gt;So Img2Gen supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text-to-image generation&lt;/li&gt;
&lt;li&gt;Reference-image edits with up to 5 uploaded images&lt;/li&gt;
&lt;li&gt;1, 2, or 4 outputs per request&lt;/li&gt;
&lt;li&gt;Multiple aspect ratios&lt;/li&gt;
&lt;li&gt;Low, medium, and high quality modes&lt;/li&gt;
&lt;li&gt;Daily free generations&lt;/li&gt;
&lt;li&gt;Credit-based paid usage&lt;/li&gt;
&lt;li&gt;Generation history and downloads&lt;/li&gt;
&lt;li&gt;Automatic refunds when a generation fails&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;The app is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TanStack Start&lt;/li&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;Cloudflare Workers&lt;/li&gt;
&lt;li&gt;Cloudflare D1 with Drizzle ORM&lt;/li&gt;
&lt;li&gt;Cloudflare R2 for generated image storage&lt;/li&gt;
&lt;li&gt;Better Auth&lt;/li&gt;
&lt;li&gt;Stripe / Creem for payments&lt;/li&gt;
&lt;li&gt;A server-side GPT Image 2 generation provider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted the whole thing to run close to the edge, avoid a traditional server, and keep the architecture small enough that I could iterate quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cloudflare Workers?
&lt;/h2&gt;

&lt;p&gt;Image generation itself is slow compared with a normal web request, so the main app has to handle queued jobs, polling, persistence, and failures cleanly.&lt;/p&gt;

&lt;p&gt;The flow looks roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User submits a prompt and optional reference images&lt;/li&gt;
&lt;li&gt;The server validates the request and checks moderation&lt;/li&gt;
&lt;li&gt;Free daily usage or credits are reserved&lt;/li&gt;
&lt;li&gt;A generation job is submitted to the provider&lt;/li&gt;
&lt;li&gt;The client polls for status&lt;/li&gt;
&lt;li&gt;Successful images are downloaded, moderated, and persisted to R2&lt;/li&gt;
&lt;li&gt;The generation record is saved in D1&lt;/li&gt;
&lt;li&gt;Credits are refunded if the job fails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hardest part was not the UI. It was making the generation workflow feel reliable when the underlying job can take a while or fail halfway through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit Handling
&lt;/h2&gt;

&lt;p&gt;I ended up using a credit model because image quality and output count change the cost.&lt;/p&gt;

&lt;p&gt;Currently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low quality: 1 credit per image&lt;/li&gt;
&lt;li&gt;Medium quality: 4 credits per image&lt;/li&gt;
&lt;li&gt;High quality: 16 credits per image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free users get daily generations, and paid users can use monthly credits or credit packs.&lt;/p&gt;

&lt;p&gt;One small detail I cared about: failed generations should not silently burn credits. If a job fails after credits are reserved, the system refunds them automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Images
&lt;/h2&gt;

&lt;p&gt;Reference-image editing was one of the main reasons I built this.&lt;/p&gt;

&lt;p&gt;Img2Gen accepts PNG, JPG, and WebP references, up to 5 images per request. This makes it useful for product shots, ad creatives, portraits, style exploration, and iterative edits where you want the output to stay anchored to existing visuals.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;A few takeaways from building it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI image apps need good job recovery, not just good prompts.&lt;/li&gt;
&lt;li&gt;Credit systems need to be boring and predictable.&lt;/li&gt;
&lt;li&gt;Storing generated outputs matters because provider URLs are not always a long-term product experience.&lt;/li&gt;
&lt;li&gt;A focused workflow can be more useful than exposing every possible model setting.&lt;/li&gt;
&lt;li&gt;Edge/serverless architectures work well here, but long-running generation flows need careful state design.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;You can try it here:&lt;/p&gt;

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

&lt;p&gt;I would love feedback from other builders, especially on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the generation flow feels clear&lt;/li&gt;
&lt;li&gt;Whether the credit model is understandable&lt;/li&gt;
&lt;li&gt;What reference-image workflows you would expect from a tool like this&lt;/li&gt;
&lt;li&gt;Any architecture suggestions for long-running AI generation jobs on serverless platforms&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>cloudflarechallenge</category>
      <category>react</category>
    </item>
  </channel>
</rss>
