I came across a blog recently that had something under the post title I hadn't seen before: a small "Summarize with:" bar with five options – ChatGPT, Perplexity, Claude, Gemini, Grok. You click one and you're in that chat with a ready-made prompt: "summarize this article".
My first thought: integration, API, keys. I opened DevTools and it turned out to be five plain links. No API, no backend, and no JavaScript required.
Below is everything you need to put this on your own site: five URLs, a working component, and the traps I ran into. There's a live demo and the full file at the end, if you'd rather just grab it and move on.
What it looks like
A label, five pills with icons, flex-wrap so it wraps on mobile. That's it.
What's underneath
The whole thing boils down to a single q query parameter. At the time of writing, the URLs below let you pass a prompt through q.
Let me flag this right away, because it's the most important caveat in the article: these are not official, documented APIs. This is undocumented frontend behavior that happens to be supported today, and it can disappear without any warning. I've already seen reports that ?q= gets ignored depending on session state. Treat this list as something you'll need to refresh one day, not as a contract.
The static version is a dozen lines of markup:
<div class="ai-summary">
<p>Summarize with:</p>
<ul>
<li>
<a href="https://chatgpt.com/?q=Read+https%3A%2F%2F..." target="_blank"
>ChatGPT</a
>
</li>
...
</ul>
</div>
The prompt is the same for every button – only the domain you land on changes:
Read {ARTICLE_URL} and give me the key takeaways in a few bullet points.
Cite that URL as the source.
The second sentence tells the model which source to point at in its answer. I wouldn't sell this as a GEO hack – the model isn't going to index your domain because of it, and it won't start recommending you in future conversations. You're simply being explicit about where the content came from, in this one conversation.
I've seen a variant that puts the blog's root URL in that second sentence instead of the article. That doesn't convince me: the source of this particular summary is this particular post, so pointing at it is the more accurate thing to do.
The URLs that work right now
This is basically all the domain knowledge this solution needs:
-
ChatGPT –
https://chatgpt.com/ -
Perplexity –
https://www.perplexity.ai/search/new/ -
Claude –
https://claude.ai/new -
Gemini –
https://www.google.com/search?udm=50&aep=11 -
Grok –
https://grok.com/
You add q with the prompt to each of them. Two notes:
-
Gemini is not
gemini.google.comhere, it's Google AI Mode.udm=50switches search into AI mode,aep=11is an entry-point param. It looks odd, but that's the URL that actually opens AI Mode with your query filled in. -
ChatGPT –
chat.openai.comstill works, but it redirects tochatgpt.com. Use the destination and save yourself a redirect.
Keep this list in one place in your code. When one of these services changes its behavior, you fix a single line instead of five templates.
The code
Static HTML is fine as long as you have one article. On a blog you want to generate it, so here are a few small functions.
1. The list of services. Just the URL, no string concatenation – adding a sixth one is a single line:
const AI_TARGETS = [
{ id: "chatgpt", label: "ChatGPT", endpoint: "https://chatgpt.com/" },
{
id: "perplexity",
label: "Perplexity",
endpoint: "https://www.perplexity.ai/search/new/",
},
{ id: "claude", label: "Claude", endpoint: "https://claude.ai/new" },
{
id: "gemini",
label: "Gemini",
endpoint: "https://www.google.com/search?udm=50&aep=11",
},
{ id: "grok", label: "Grok", endpoint: "https://grok.com/" },
];
2. The prompt. One place, one source of truth:
const buildPrompt = (pageUrl) =>
`Read ${pageUrl} and give me the key takeaways in a few bullet points. Cite that URL as the source.`;
3. The article URL. This is the trap that's easy to fall into. It's tempting to grab location.href – don't. Your reader arrives at the post through something like:
/blog/my-post?utm_source=linkedin&utm_campaign=post-42#comments
and all of that ends up in the prompt. Best case the model gets an ugly link. Worst case, you're forwarding parameters to a third-party service that were never meant to leave your site.
Use the canonical URL:
const getCanonicalUrl = () =>
document.querySelector('link[rel="canonical"]')?.href ??
`${location.origin}${location.pathname}`;
The fallback drops the query string and the fragment, which covers the case where the page has no canonical tag.
4. Building the URL. I don't concatenate strings, I use URL:
const buildUrl = (endpoint, prompt) => {
const url = new URL(endpoint);
url.searchParams.set("q", prompt);
return url.href;
};
Three things you get for free here:
-
Correct encoding. An
&, a?, or non-ASCII characters in the URL won't break the query string. If you encode this by hand with string replacements, you'll eventually miss an edge case – and you'll find out when somebody clicks. -
Space as
+.URLSearchParamsserializes asapplication/x-www-form-urlencoded, so spaces come out as+rather than%20. Both are valid, but+is shorter and it's what these services use themselves. -
Appending to existing params. Gemini already carries
udmandaepin its URL.searchParams.set()addsqalongside them and you never have to think about whether it's a?or an&.
5. Rendering:
const renderAiSummary = (mount, { pageUrl, heading = "Summarize with:" }) => {
const prompt = buildPrompt(pageUrl);
const items = AI_TARGETS.map(
({ id, label, endpoint }) => `
<li>
<a href="${buildUrl(endpoint, prompt)}" target="_blank" rel="noopener nofollow"
title="Summarize with ${label}">${ICONS[id]} ${label}</a>
</li>`,
).join("");
mount.innerHTML = `
<div class="ai-summary">
<p class="ai-summary__label">${heading}</p>
<ul class="ai-summary__list">${items}</ul>
</div>`;
};
renderAiSummary(document.querySelector("#ai-summary"), {
pageUrl: getCanonicalUrl(),
});
One caveat about innerHTML: every interpolated value here comes from trusted config, so it's safe in this example. If heading or the labels could ever carry user-edited content, don't drop them in directly.
On a static blog (Astro, Next, Hugo) you do the same thing in a template and you don't need a single line of JavaScript in the browser – you already know the canonical URL at build time.
Styling
Nothing fancy, pills on a flex row:
.ai-summary__list {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
list-style: none;
margin: 0;
padding: 0;
}
.ai-summary__list a {
display: flex;
align-items: center;
gap: 6px;
background: #fff;
padding: 8px 14px;
border-radius: 8px;
border: 1px solid #e5e7f3;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
color: #14151d;
font-weight: 500;
text-decoration: none;
}
flex-wrap: wrap is the important part – five buttons don't fit on one line on a phone. On narrow screens it's also worth dropping the padding to 7px 10px.
Keep the icons as inline SVG. You avoid five extra requests and the whole component stays one file that you just paste in. At this icon size that's the natural choice.
Grab the logos from each company's brand kit instead of pulling them off somebody else's site. All five have a public page with brand assets and usage rules.
The preview panel
While building this I put a small panel under the component that prints exactly what each button points at:
It's worth keeping that panel around while you're developing. You immediately see whether the URL was encoded correctly and whether the prompt actually got the canonical URL rather than one with utm_source on it.
BONUS – things that are easy to miss
Logged-out readers. I tested claude.ai/new?q=... without a session. You get redirected to the login page, but the prompt survives – it sits in returnTo:
https://claude.ai/logout?involuntary=1&returnTo=%2Flogin%3F...%252Fnew%253Fq%253DRead+https...
After logging in, the prompt is still there. You don't have to handle anything.
rel on the links. Keep rel="noopener nofollow". Modern browsers already apply noopener for target="_blank", but writing it out tells whoever reads the code what's going on. nofollow because you don't want five links in every post to read as an editorial endorsement of those services.
URL length. The prompt with one URL in it is around 200 characters once encoded, so length isn't a practical problem here. I still wouldn't grow it indefinitely – the more you push into the query string, the more you depend on someone else's frontend staying the same.
Google may show you a CAPTCHA. An AI Mode link opened by automation, or from a suspicious network, sometimes lands on /sorry/index. From a normal browser, clicked by a human, it's fine – but don't count on testing that particular button with a script.
Wrapping up
The whole thing is 40 lines and zero dependencies. The main thing that can break here is one of these services changing its behavior – which is exactly why the URLs live in a single array instead of being scattered across templates.
For your audience it's a real convenience. Plenty of readers already paste article URLs into AI chats to decide whether a piece is worth their time, so this isn't an invented need – it's a shortcut for something they do by hand anyway.
For the author, the trade-off is less obvious. The same button that saves your reader time can cost you a read: somebody skims five bullets in a chat instead of your text. If you're measured on time on page or you run ads, that's a cost you should actually count, not assume it isn't there.
I'd ship it. But deliberately — as a convenience for the reader, not as a traffic trick.
Would you ship this on your own blog?
Happy coding! 🚀
Full example – live demo and source
Everything above is enough to build the component yourself. If you'd rather skip the assembly, the finished index.html sits as a single file in my examples repo — icons included, no dependencies, nothing to install.
Live demo – click the buttons and see where each one takes you.
Source on GitHub – one file, MIT licensed, take what you need.
Swap the <link rel="canonical"> tag in <head> for your own article URL and you're done.


Top comments (0)