iTranslated by AI
How I Stabilized Shared URLs, OGP, and Answer Restoration for a Static HTML Diagnostic Tool
I built a small browser-based diagnostic tool with the help of AI.
I created a page called "Thinking IQ Diagnosis," where you answer 10 or 30 questions to receive an IQ-style score and a thinker type inspired by philosophers.
You can find the Japanese version here:
And the English version here:
This is not an official IQ test but rather an entertainment-focused thinking style diagnosis. However, as I continued developing it, it evolved into something more like a small web app, incorporating features such as result images, shareable URLs, shortened URLs, OGP cards, English/Japanese toggling, GA4, and click logging, rather than just a simple diagnostic page.
In this article, I write about the hurdles I faced regarding publishing and sharing, rather than the diagnostic logic itself.
What I Built

These are the features I eventually included:
- 10-question / 30-question diagnosis versions
- Timed questions
- Automatic scoring
- IQ-style score
- Category-specific scores
- Philosopher-themed thinker types
- Diagnostic result image generation
- X (Twitter) post text generation
- Shareable result URLs
- URL shortening
- OGP cards
- Japanese / English toggling
- FAQ
- GA4
- Click logging
- Answer logging per question
Initially, it was a regular diagnostic page running on HTML and JavaScript.
However, as I started posting to X, opening it on smartphones, and building an English version, the challenges were more about "how it looks when shared," "whether the language switches correctly," and "preventing old caches from lingering" rather than the diagnosis itself.
Structure: Static HTML + JavaScript + PHP
The rough structure is as follows:
diagnosis.html
Entry point for the diagnosis application
site/app.js
Diagnostic logic, UI rendering, language switching, result display
site/data-bundle.js
Question data, type data
result.php
Shared diagnostic result page
s.php
Redirect for shortened URLs
create-short-url.php
Shortened URL generation
admin-stats.php
Checking click and diagnostic result logs
Instead of stuffing JavaScript directly into WordPress post content, I placed the diagnosis as static files.
The reason is simple: I didn't want it to interfere with WordPress theme or plugin CSS and JavaScript.
Placed in uploads rather than WordPress body
I placed the diagnostic tool under the wp-content/uploads directory as an HTML file rather than embedding it directly into the WordPress post body. By doing this, I could manage the CSS and JavaScript for the diagnostic page quite freely.
On the other hand, a different issue arose when embedding it into a WordPress static page using an iframe.
In particular, I struggled with the screen position failing to return naturally after clicking the button to view the diagnostic results. Even if the result page is switched within the iframe, the scroll position of the parent page remains as is. Therefore, on smartphones, it would sometimes remain in an awkward position instead of at the top of the result screen.
Ultimately, I implemented a solution where the diagnostic side notifies the parent page of the height and scroll position, and the parent page resets to the top of the iframe.
Separating the 10-question and 30-question versions
Initially, I reused the questions from the 10-question version as part of the 30-question version. However, from the perspective of someone who tries the 30-question version after having tried the 10-question version, having the same questions appear feels like cutting corners. Therefore, I separated the questions for the 10-question and 30-question versions.
The 10-question version is for entry, while the 30-question version uses different questions to allow users to see category-specific scores and more detailed explanations.
This aspect was more about UX than diagnostic logic. Clarifying the page's role by distinguishing between "those who want to try it briefly" and "those who want to look a bit deeper" made the page's purpose more distinct.
Making diagnostic results recoverable via URL

Initially, posting to X would only jump to the top of the diagnostic tool. But from the perspective of someone who sees the post, this leads to the question, "Where is this person's result?"
So, I made it possible to recover the diagnostic result from the URL. By encoding the result data and including it in the URL, the result is reconstructed when the page is opened. The concept is as follows:
Diagnosis complete
↓
Generate result data
↓
Encode result data for URL
↓
Create shareable URL
↓
Opening the shareable URL restores the result page
This allowed people who opened the shared link to go directly to that person's diagnostic result page instead of the diagnostic home page.
Created a shortened URL because the result sharing URL was too long
Naturally, including result data in the URL makes the URL very long. Placing such a long URL directly in an X post looks bad and also consumes character count. Therefore, I created a shortened URL.
Long result URL
↓
Generate shortened URL
↓
Include shortened URL in X post
↓
Opening the shortened URL redirects to the original result URL
At this time, I also made sure to capture click logs. However, the shortened URL was not as simple as just "shortening the URL."
In this diagnosis, I used r= to restore the result page, a= / d= to restore answer verification, lang= to maintain the language, and v= to avoid caching. In other words, I could not afford to drop any of these when creating the shortened URL.
r= Result restoration
a= Simple answer information
d= Detailed information for restoring answer verification
lang= Japanese / English
v= Cache prevention
In fact, at first, a= and d= would drop in some routes, and even if I opened the shared link, the answer verification section would not be displayed.
In particular, the X post button side created a shortened URL using a different process from the normal "Copy result link," resulting in a state where one was fixed but the other remained broken. When shortening a shared URL, it was necessary to confirm not just that the URL was shortened, but that the parameters required for restoration remained until the end.
Restoration routes for answer verification diverged between result links and X post buttons
After release, problems specific to X's in-app browser also emerged.
Many people who open the diagnosis page on X perform the diagnosis within the X in-app browser. After viewing the results page, they click the "Post to X" button, which takes them to the X posting screen. At this point, it becomes difficult to return to the original diagnosis result page. Consequently, a problem arose where users could not "later verify which questions they answered correctly or incorrectly."
Initially, I used the r= parameter to restore the results page.
r=
Information to restore the result type, score, number of correct answers, and category-specific scores.
However, r= alone could not restore "which choices were selected." As a result, the answer verification section could not be displayed when opened in a different browser or in incognito mode. Therefore, I first added a=.
a=
Holds the choices selected for each question as a short string.
Even so, it was not completely stable. If a= was dropped while passing through result.php or s.php, answer verification could not be restored. Furthermore, I wanted to reliably restore the classification of correct, incorrect, and unanswered questions, not just the answers themselves.
Ultimately, I added the d= parameter.
d=
Answer content
correct / incorrect / unanswered
examId
I formatted this as base64url JSON.
Regarding the shared URL, my final approach was as follows:
r=
Restores the result itself
a=
Restores simple answer information
d=
Restores the answer verification section more reliably
Another issue that occurred here was that the route for creating shortened URLs differed between the normal "Copy result link" and the "X post button." The "Copy result link" successfully maintained r= / a= / d=. However, the X post button normalized the URL before passing it to the shortened URL generator, and that normalization process caused a= and d= to be dropped.
In other words, even for the same result sharing, it was like this:
Copy result link
→ Maintained r= / a= / d=
X post button
→ a= / d= were dropped during URL normalization
This led to a state where shortened URLs created from "Copy result link" displayed answer verification, while those from the X post button did not. Ultimately, I ensured that the URL normalization process for the X post button also consistently maintained the following:
r=
a=
d=
lang=
v=
Even if "sharing results" looks the same on the surface, it easily splits into multiple routes internally. In this case, there were:
Copy result link
X post button
Shortened URL
OGP acquisition
Shared result page
Redirect to diagnosis body
Each goes through different processes. Therefore, even if a fix is applied to one route, the same information might be dropped in another. This was a significant lesson for me.
Struggles with OGP Cards for X Posting

The most tedious part was OGP. I wanted card images to appear when a URL was posted to X, but it didn't work well at first. The problems I encountered included:
- The card itself not appearing
- The card appearing but without an image
- Japanese images appearing for English URLs
- English images appearing for Japanese URLs
- Stale/old OGP images remaining
- Time lags before appearing in the posting screen
- X retaining old cache even after changing the image
OGP wasn't just a matter of writing the HTML meta tags correctly. It involves X's fetching timing, caching, image URLs, redirects, and language branching.
Separated OGP Images by Language
Ultimately, I separated the OGP images for Japanese and English.

thinking-iq-ogp-ja-v96.jpg

thinking-iq-ogp-en-v96.jpg
I detect lang=ja or lang=en on the shared results page or shortened URL side to switch which OGP image is served. If serving the English version, the card image naturally needs to be in English as well; conversely, showing an English card for the Japanese version feels off. Therefore, I made not only the page content but also the OGP images language-specific.
Conflict between localStorage and URL parameters for Japanese/English Switching
For language switching, I initially used localStorage. When a user selected Japanese, it would be in Japanese; if they chose English, it would be in English, and it would open in that language next time. I think this is convenient for a normal page, but it causes problems when using shared URLs or Pretty Links.
For example, if you open an English URL, it might display in Japanese because you opened it in Japanese previously. Or, if you open a Japanese URL, it might display in English because you were looking at it in English last time. This is problematic for external public sharing.
This stabilized the following:
/thinking-iq-diagnosis/
→ Japanese
/thinking-iq-diagnosis-en/
→ English
Stuck on 301 Caching for Pretty Links

I used Pretty Links for the public-facing URLs. Initially, I used URLs like these:
https://pilnmeness.com/thinking-iq-diagnosis/
https://pilnmeness.com/thinking-iq-diagnosis/?lang=en
(I later separated the English version into /thinking-iq-diagnosis-en/)
Each redirects to the actual HTML file:
/wp-content/uploads/iq-diagnosis/diagnosis.html?lang=ja&v=106
/wp-content/uploads/iq-diagnosis/diagnosis.html?lang=en&v=106
I hit a snag because I initially used 301 redirects. Since 301 is a permanent redirect, browsers can remember old forwarding destinations. Even after fixing the forwarding destination in the Pretty Link settings, the browsers seemed to continue jumping to the old URL.
In this case, the URL finally opened was:
/wp-content/uploads/iq-diagnosis/diagnosis.html
Because lang=en was dropped, the page referred to the localStorage language setting. As a result, even the English URL was displayed in the language used last time.
My solution was as follows:
- Change the redirect from 301 to 307
- Fix
lang=ja/lang=enin the forwarding destination - Include a version number like
v=106in the forwarding destination - Confirm with secret windows and mobile devices
Separated Roles for GA4 and Click Logs
I also added GA4 for access analysis. However, I didn't put GA4 in every PHP file. I only put it in these two:
diagnosis.htmlresult.php
I excluded it from the shortened URL redirect side (s.php). The reason is that X's card-fetching bots and crawlers also visit the shortened URL side. If I put GA4 there, human traffic and bot traffic would easily get mixed up. Therefore, I separated their roles:
-
GA4
→ Monitor page views of the diagnosis and results pages -
Custom Click Logs
→ Separately monitor clicks on shortened URLs and bot access
When implementing analysis, it was better to separate them based on "what the log is for" rather than just putting everything in one place.
Added an FAQ to Reduce Anxiety Before 'Show HN'
Since I plan to release it in English-speaking regions as well, I added an FAQ. I especially wanted to address these points in advance:
- Is this a real IQ test?
- Which should I choose, the 10-question or 30-question version?
- How are the results calculated?
- Can the results be shared?
- Do you collect personal information?
Specifically, the word "IQ" is easily misunderstood. Therefore, I explicitly stated in the FAQ that "This is not an official IQ test." I wrote the same thing in the English version:
No. This is not a real IQ test.
Things That Were Important When Building with AI
AI can build the prototype of a diagnosis page very quickly. However, to bring it to a state where it can be released, I needed the process of actually touching it and finding where it was broken.
This was particularly difficult:
- SNS sharing
- OGP cards
- Caching
- Mobile display
- X in-app browser
- iframe
- Language switching
- Shortened URLs
- Log design
If you tell AI to "build it," it can produce a prototype that works. But you won't know if the card doesn't appear when posted to X, if it's in Japanese despite the English URL, if the behavior is different only on iPhone, or if old JavaScript remains, unless you actually check it.
This time, I consulted with AI about the implementation, but in the end, it was a repetition of very detailed checks and fixes. (The v=106 at the end of the URL mentioned in the redirect section is the result of continuous fine-tuning, reaching version 106 now.)
Summary
Even for a static HTML diagnosis tool, there are surprisingly many things to do when preparing it for public release. The diagnosis logic itself isn't that difficult. However, to make it ready for the outside world, I had to consider shared URLs, OGP cards, shortened URLs, language switching, caching, and access analysis.
What worked well this time was:
- Separating the diagnosis core as static HTML
- Making it possible to restore results via URL
- Converting long result URLs into shortened URLs
- Separating OGP images by JA/EN
- Prioritizing URL
langoverlocalStorage - Using 307 instead of 301 for Pretty Links during adjustment
- Separating the roles of GA4 and custom logs
- Adding an FAQ before Show HN
What was most troublesome this time was not the diagnosis result itself, but maintaining the restoration information across each sharing route. Since the result link copy, X post button, shortened URL, OGP fetch, diagnosis.html, result.php, and s.php all go through different processes, if r=, a=, d=, lang=, or v= is dropped in any one place, results or answer verification cannot be restored at the destination.
Even when building with AI, it was more realistic to find and fix broken parts through actual usage rather than trying to complete it in one go.
The diagnosis page is here.
Japanese version:
English version:
Discussion