<?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: amrgharz</title>
    <description>The latest articles on DEV Community by amrgharz (@amrgharzuae).</description>
    <link>https://dev.to/amrgharzuae</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%2F4110686%2F3070c8ed-ed59-4dbc-bbba-3f25e607c12c.png</url>
      <title>DEV Community: amrgharz</title>
      <link>https://dev.to/amrgharzuae</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amrgharzuae"/>
    <language>en</language>
    <item>
      <title>Why Your GA4 UTM Attribution Breaks on Next.js SPAs (And How to Fix It)</title>
      <dc:creator>amrgharz</dc:creator>
      <pubDate>Mon, 21 Sep 2026 18:04:00 +0000</pubDate>
      <link>https://dev.to/amrgharzuae/why-your-ga4-utm-attribution-breaks-on-nextjs-spas-and-how-to-fix-it-4njk</link>
      <guid>https://dev.to/amrgharzuae/why-your-ga4-utm-attribution-breaks-on-nextjs-spas-and-how-to-fix-it-4njk</guid>
      <description>&lt;p&gt;If you run paid campaigns or email newsletters driving traffic to a modern Next.js single-page application (SPA), your conversion data in Google Analytics 4 might be silently misattributing traffic to &lt;code&gt;(direct) / (none)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A user clicks an ad with complete campaign parameters (&lt;code&gt;utm_source=meta&amp;amp;utm_medium=paid_social&amp;amp;utm_campaign=launch_q3&lt;/code&gt;), lands on your site, browses a few pages via client-side routing, and suddenly their purchase event is credited to a direct visit.&lt;/p&gt;

&lt;p&gt;Here is why client-side routing causes UTM data drops in GA4, and how to preserve campaign attribution reliably in Next.js.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Single-Page Routing Trap
&lt;/h2&gt;

&lt;p&gt;In traditional multi-page websites, every navigation triggers a full browser reload (&lt;code&gt;window.location&lt;/code&gt;), refreshing URL state across page boundaries.&lt;/p&gt;

&lt;p&gt;Modern frameworks like Next.js utilize client-side navigation (&lt;code&gt;next/link&lt;/code&gt; or &lt;code&gt;useRouter&lt;/code&gt;) using the HTML5 History API:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A visitor arrives at &lt;code&gt;/landing-page?utm_source=newsletter&amp;amp;utm_medium=email&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;GA4's config snippet reads the query parameters from the initial document load and logs session attribution.&lt;/li&gt;
&lt;li&gt;The user clicks an internal link (&lt;code&gt;&amp;lt;Link href="/pricing"&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Next.js pushes the route without reloading the browser.&lt;/li&gt;
&lt;li&gt;The URL updates to &lt;code&gt;/pricing&lt;/code&gt;, stripping the query string from &lt;code&gt;window.location.search&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Subsequent virtual &lt;code&gt;page_view&lt;/code&gt; events fire with a clean path devoid of campaign parameters.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your tracking setup re-instantiates or reads the location object directly on sub-route transitions, your session continuity risks attribution degradation or event fragmentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Three Common Mistakes in Next.js SPA Tracking
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stripping query params too early:&lt;/strong&gt; Developers often clean up URLs for aesthetics using &lt;code&gt;router.replace(pathname)&lt;/code&gt; before tracking scripts initialize or server-side logs capture parameter payloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malformed parameter formats:&lt;/strong&gt; Relying on unencoded parameters or trailing slashes placed after the query string (&lt;code&gt;?utm_source=twitter/&lt;/code&gt;) invalidates GA4 auto-parsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring manual session persistence:&lt;/strong&gt; Failing to store initial acquisition parameters into &lt;code&gt;sessionStorage&lt;/code&gt; or cookies for multi-step signup flows that traverse subdomains or isolated client components.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Preserving Campaign UTMs in Next.js App Router
&lt;/h2&gt;

&lt;p&gt;Use a client component mounted in your root layout to catch and persist first-touch attribution parameters in &lt;code&gt;sessionStorage&lt;/code&gt; on initial arrival:&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="c1"&gt;// components/AttributionTracker.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;react&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSearchParams&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="s1"&gt;next/navigation&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;TRACKED_PARAMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utm_source&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="s1"&gt;utm_medium&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="s1"&gt;utm_campaign&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="s1"&gt;utm_term&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="s1"&gt;utm_content&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AttributionTracker&lt;/span&gt;&lt;span class="p"&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;searchParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearchParams&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="c1"&gt;// Only capture on initial entry if parameters exist&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;utmData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&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;hasUtm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;TRACKED_PARAMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;param&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;param&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;utmData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;param&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;hasUtm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasUtm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first_touch_utm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;utmData&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;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;Mount &lt;code&gt;&amp;lt;AttributionTracker /&amp;gt;&lt;/code&gt; inside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; wrapper in &lt;code&gt;app/layout.tsx&lt;/code&gt;. If a user completes an unrouted checkout or external conversion, you can pull attribution directly from &lt;code&gt;sessionStorage.getItem('first_touch_utm')&lt;/code&gt; to accompany server-side events.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Constructing Sanitized Tracking URLs
&lt;/h2&gt;

&lt;p&gt;Parameter capitalization and unescaped spaces trigger fragmented rows in GA4 reports (&lt;code&gt;Email&lt;/code&gt; vs &lt;code&gt;email&lt;/code&gt; will register as two distinct channels).&lt;/p&gt;

&lt;p&gt;Before deploying links across marketing channels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Campaign UTM Builder:&lt;/strong&gt; Generate standard-compliant, lowercase, and sanitized campaign links with the free &lt;a href="https://omniseotools.com/tools/campaign-utm-builder" rel="noopener noreferrer"&gt;Campaign UTM Builder&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programmatic Sanitization:&lt;/strong&gt; If generating marketing tracking links inside server-side pipelines or automated scripts, install &lt;a href="https://www.npmjs.com/package/omniseo-core" rel="noopener noreferrer"&gt;omniseo-core on npm&lt;/a&gt; and call &lt;code&gt;buildUtmUrl()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>nextjs</category>
      <category>analytics</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Why Your Open Graph Image Crops on LinkedIn &amp; Discord (And How to Fix It in Next.js)</title>
      <dc:creator>amrgharz</dc:creator>
      <pubDate>Mon, 21 Sep 2026 17:09:30 +0000</pubDate>
      <link>https://dev.to/amrgharzuae/why-your-open-graph-image-crops-on-linkedin-discord-and-how-to-fix-it-in-nextjs-32l0</link>
      <guid>https://dev.to/amrgharzuae/why-your-open-graph-image-crops-on-linkedin-discord-and-how-to-fix-it-in-nextjs-32l0</guid>
      <description>&lt;p&gt;Few things undermine a new feature release like sharing a link on LinkedIn or Discord only to find the title cropped in half or the brand logo chopped off at the bottom.&lt;/p&gt;

&lt;p&gt;If you designed your Open Graph asset at 1200x630 and assumed every platform renders it identically, you have likely hit the aspect ratio trap.&lt;/p&gt;

&lt;p&gt;Here is why it happens and how to structure your Open Graph assets and dynamic Next.js App Router metadata to look sharp across every platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Aspect Ratio Trap: 1.91:1 vs 1:1 vs 16:9
&lt;/h2&gt;

&lt;p&gt;While the standard Facebook Open Graph recommendation is 1200x630 px (1.91:1), platforms interpret cards differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LinkedIn Feed: Scales 1200x627 or 1200x630 cleanly on desktop, but dynamically center-crops on mobile viewports.&lt;/li&gt;
&lt;li&gt;Discord: If your image is under 300px or has dimensions Discord flags as a thumbnail, it forces a tiny 1:1 square crop to the right of your text instead of a full-width card. Full embeds crop to 16:9.&lt;/li&gt;
&lt;li&gt;X / Twitter (summary_large_image): Displays at 2:1 or ~16:9, frequently chopping off 15–20 pixels from the top and bottom margins compared to a 1.91:1 canvas.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Universal Fix: The 40px Safe Zone
&lt;/h3&gt;

&lt;p&gt;Always design your base image at 1200 x 630 px, but enforce an internal safe zone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canvas: 1200px x 630px&lt;/li&gt;
&lt;li&gt;Safe Area: 1080px x 520px (Center aligned)&lt;/li&gt;
&lt;li&gt;Margin: Keep all essential text, logos, and focal points at least 60px from the edges and 40px from top/bottom.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Dynamic Next.js App Router Setup
&lt;/h2&gt;

&lt;p&gt;If you use Next.js 14/15 App Router, configure your metadata object with explicit width, height, and card format tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Metadata&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="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kr"&gt;string&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="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Metadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&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;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPost&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ogUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://omniseotools.com/api/og?title=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://omniseotools.com/blog/&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="na"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OmniSEO Tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;images&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="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ogUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&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="na"&gt;twitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary_large_image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ogUrl&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;Important: Specifying width: 1200 and height: 630 inside the openGraph.images array helps platforms like LinkedIn and Facebook compute aspect ratios immediately without having to pre-fetch the full binary asset before determining the layout.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Verify Before Deploying
&lt;/h2&gt;

&lt;p&gt;Instead of deploying to production and spamming post preview links to clear caches, test your Open Graph tags and crops locally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Free Web Simulator: Test live previews for Twitter, LinkedIn, and Discord in real time using the free Social Meta &amp;amp; OpenGraph Card Simulator (&lt;a href="https://omniseotools.com/tools/open-graph-meta-generator" rel="noopener noreferrer"&gt;https://omniseotools.com/tools/open-graph-meta-generator&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;NPM Package: If you need programmatic validation or tag generation directly in your build pipelines, check out omniseo-core on npm (&lt;a href="https://www.npmjs.com/package/omniseo-core" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/omniseo-core&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>seo</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How I Built a Zero-Latency Open Graph &amp; SERP Previewer with Next.js</title>
      <dc:creator>amrgharz</dc:creator>
      <pubDate>Sat, 05 Sep 2026 05:35:39 +0000</pubDate>
      <link>https://dev.to/amrgharzuae/how-i-built-a-zero-latency-open-graph-serp-previewer-with-nextjs-4b9m</link>
      <guid>https://dev.to/amrgharzuae/how-i-built-a-zero-latency-open-graph-serp-previewer-with-nextjs-4b9m</guid>
      <description>&lt;p&gt;Most social preview debuggers and meta tag testers today are bloated, slow, or locked behind ad walls and paywalls. &lt;/p&gt;

&lt;p&gt;I wanted a clean, browser-native suite of tools that runs 100% client-side with 0ms server latency, so I built &lt;a href="https://omniseotools.com" rel="noopener noreferrer"&gt;OmniSEOtools&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Client-Side Matters
&lt;/h3&gt;

&lt;p&gt;Instead of scraping URLs on a remote server, testing your tags and title dimensions directly in the browser gives instant feedback. &lt;/p&gt;

&lt;p&gt;Here is what is currently live and open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://omniseotools.com/tools/twitter-card-preview" rel="noopener noreferrer"&gt;Twitter / X Card Previewer&lt;/a&gt; — Test 1.91:1 social card crops and text truncation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omniseotools.com/tools/meta-title-pixel-checker" rel="noopener noreferrer"&gt;Google SERP Pixel Ruler&lt;/a&gt; — Real-time 600px desktop and mobile snippet simulator.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omniseotools.com/tools/linkedin-link-preview" rel="noopener noreferrer"&gt;LinkedIn Post Inspector&lt;/a&gt; — Accurate preview of feed post cutoffs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is built using Next.js (App Router), Tailwind CSS, and is open-sourced on &lt;a href="https://dev.toYOUR_GITHUB_REPO_URL_HERE"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Would love feedback from other developers on what other SEO or meta utilities you'd find useful to have client-side!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
