Every Remix fix we catalog

SEOLZ catalogs 12 fixes for Remix across 1 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for Remix, with a link to the official docs.

SEO · 12 fixes

Images missing altModerate effort

Add descriptive alt text to every product and content image so search engines and screen readers can understand what the image shows.

On Remix

  1. Locate every `<img>` tag and Remix/React component that renders images across your routes and components (e.g. `app/routes/`, `app/components/`). Run a project-wide search for `<img` and for any image component (e.g. Shopify Hydrogen's `<Image>`) to build a complete inventory.
  2. For each `<img>` tag, add a meaningful `alt` attribute that describes the image content — e.g. `<img src={product.featuredImage.url} alt={product.featuredImage.altText || product.title} />`. Never leave `alt` undefined or empty unless the image is purely decorative (in which case use `alt=""`).
  3. For Shopify Hydrogen/Remix storefronts using the `<Image>` component from `@shopify/hydrogen`, pass the `alt` prop explicitly: `<Image data={image} alt={image.altText ?? product.title} />`. The `data` prop alone does not guarantee a non-empty alt if the Storefront API returns a null `altText`.
  4. Update your GraphQL fragments (e.g. in `app/lib/fragments.ts` or inline in loaders) to always request the `altText` field alongside `url` and `width`/`height` for every image connection — product images, collection images, metafield images, and content/blog images.
  5. Add a fallback guard in your loader or component so that if `altText` is null/empty, a descriptive string (product title, collection name, or article title) is used instead: `const resolvedAlt = image.altText?.trim() || product.title;`.
  6. Run `grep -r 'alt=""' app/` and `grep -r "alt={''" app/` to catch any remaining empty alt attributes that are not intentionally decorative, then review and populate them. Commit the changes and verify rendered HTML with browser DevTools to confirm every `<img>` in the DOM has a non-empty `alt` attribute.
Code · Generic HTML (html)
<!-- BEFORE (missing alt — bad) -->
<img src="/images/red-leather-bag.jpg">

<!-- AFTER (descriptive alt — correct) -->
<img src="/images/red-leather-bag.jpg" alt="Red leather crossbody bag with gold buckle clasp">

<!-- Decorative image (intentionally empty alt — correct) -->
<img src="/images/divider-line.png" alt="">

Place this wherever an <img> tag appears in your HTML. Replace the placeholder text with your real description.

Meta description too longQuick win

Shorten your meta description to 150–160 characters so Google displays it in full instead of cutting it off with "…" in search results.

On Remix

  1. Identify the route file(s) responsible for the page(s) with the too-long meta description — e.g. `app/routes/products.$handle.tsx` or `app/routes/_index.tsx` — inside your Remix project.
  2. Locate the exported `meta` function in that route file. It will look like: `export const meta: MetaFunction = ({ data }) => { return [ ... ]; };`
  3. Find the object in the returned array where `name: 'description'` is set, for example: `{ name: 'description', content: 'Your current long description here…' }`.
  4. Edit the `content` value so it is between 150 and 160 characters (inclusive). Use a character-counting tool or your editor's status bar to verify the exact length before saving.
  5. If the meta description is dynamically generated from a loader (e.g. pulled from a CMS or `data.product.seo.description`), truncate it safely in the `meta` function: `content: data.product.seo.description.slice(0, 155) + (data.product.seo.description.length > 155 ? '…' : '')` — but prefer crafting a meaningful description rather than a hard cut.
  6. Run `npm run dev` (or `remix dev`) locally, inspect the `<meta name="description">` tag in the browser's DevTools under the `<head>` element to confirm the rendered length, then deploy the updated route file.
Code · Generic HTML (html)
<meta name="description" content="Award-winning Bang & Olufsen headphones, speakers and TVs. Visit our Malaysia showroom at The Gardens Mall, Level 2. Free shipping on orders over RM500.">

Place this inside the <head> element of every page. Replace the content value with your page-specific description (max 160 characters).

Meta description too shortModerate effort

Expand your meta description to 120–160 characters so Google displays a compelling, complete snippet in search results that drives more clicks to your store.

On Remix

  1. [
  2. "
  3. L
  4. o
  5. c
  6. a
  7. t
  8. e
  9. t
  10. h
  11. e
  12. r
  13. o
  14. u
  15. t
  16. e
  17. f
  18. i
  19. l
  20. e
  21. r
  22. e
  23. s
  24. p
  25. o
  26. n
  27. s
  28. i
  29. b
  30. l
  31. e
  32. f
  33. o
  34. r
  35. t
  36. h
  37. e
  38. p
  39. a
  40. g
  41. e
  42. w
  43. i
  44. t
  45. h
  46. t
  47. h
  48. e
  49. s
  50. h
  51. o
  52. r
  53. t
  54. m
  55. e
  56. t
  57. a
  58. d
  59. e
  60. s
  61. c
  62. r
  63. i
  64. p
  65. t
  66. i
  67. o
  68. n
  69. (
  70. e
  71. .
  72. g
  73. .
  74. a
  75. p
  76. p
  77. /
  78. r
  79. o
  80. u
  81. t
  82. e
  83. s
  84. /
  85. p
  86. r
  87. o
  88. d
  89. u
  90. c
  91. t
  92. s
  93. .
  94. $
  95. h
  96. a
  97. n
  98. d
  99. l
  100. e
  101. .
  102. t
  103. s
  104. x
  105. o
  106. r
  107. a
  108. p
  109. p
  110. /
  111. r
  112. o
  113. u
  114. t
  115. e
  116. s
  117. /
  118. _
  119. i
  120. n
  121. d
  122. e
  123. x
  124. .
  125. t
  126. s
  127. x
  128. )
  129. i
  130. n
  131. s
  132. i
  133. d
  134. e
  135. y
  136. o
  137. u
  138. r
  139. R
  140. e
  141. m
  142. i
  143. x
  144. p
  145. r
  146. o
  147. j
  148. e
  149. c
  150. t
  151. .
  152. "
  153. ,
  154. "
  155. F
  156. i
  157. n
  158. d
  159. o
  160. r
  161. a
  162. d
  163. d
  164. t
  165. h
  166. e
  167. e
  168. x
  169. p
  170. o
  171. r
  172. t
  173. e
  174. d
  175. `
  176. m
  177. e
  178. t
  179. a
  180. `
  181. f
  182. u
  183. n
  184. c
  185. t
  186. i
  187. o
  188. n
  189. i
  190. n
  191. t
  192. h
  193. a
  194. t
  195. r
  196. o
  197. u
  198. t
  199. e
  200. f
  201. i
  202. l
  203. e
  204. .
  205. I
  206. t
  207. s
  208. h
  209. o
  210. u
  211. l
  212. d
  213. l
  214. o
  215. o
  216. k
  217. l
  218. i
  219. k
  220. e
  221. :
  222. `
  223. e
  224. x
  225. p
  226. o
  227. r
  228. t
  229. c
  230. o
  231. n
  232. s
  233. t
  234. m
  235. e
  236. t
  237. a
  238. :
  239. M
  240. e
  241. t
  242. a
  243. F
  244. u
  245. n
  246. c
  247. t
  248. i
  249. o
  250. n
  251. =
  252. (
  253. {
  254. d
  255. a
  256. t
  257. a
  258. }
  259. )
  260. =
  261. >
  262. {
  263. r
  264. e
  265. t
  266. u
  267. r
  268. n
  269. [
  270. .
  271. .
  272. .
  273. ]
  274. ;
  275. }
  276. ;
  277. `
  278. "
  279. ,
  280. "
  281. I
  282. n
  283. s
  284. i
  285. d
  286. e
  287. t
  288. h
  289. e
  290. `
  291. m
  292. e
  293. t
  294. a
  295. `
  296. f
  297. u
  298. n
  299. c
  300. t
  301. i
  302. o
  303. n
  304. '
  305. s
  306. r
  307. e
  308. t
  309. u
  310. r
  311. n
  312. a
  313. r
  314. r
  315. a
  316. y
  317. ,
  318. f
  319. i
  320. n
  321. d
  322. t
  323. h
  324. e
  325. o
  326. b
  327. j
  328. e
  329. c
  330. t
  331. w
  332. i
  333. t
  334. h
  335. `
  336. n
  337. a
  338. m
  339. e
  340. :
  341. '
  342. d
  343. e
  344. s
  345. c
  346. r
  347. i
  348. p
  349. t
  350. i
  351. o
  352. n
  353. '
  354. `
  355. (
  356. e
  357. .
  358. g
  359. .
  360. `
  361. {
  362. n
  363. a
  364. m
  365. e
  366. :
  367. '
  368. d
  369. e
  370. s
  371. c
  372. r
  373. i
  374. p
  375. t
  376. i
  377. o
  378. n
  379. '
  380. ,
  381. c
  382. o
  383. n
  384. t
  385. e
  386. n
  387. t
  388. :
  389. '
  390. .
  391. .
  392. .
  393. '
  394. }
  395. `
  396. )
  397. .
  398. I
  399. f
  400. i
  401. t
  402. d
  403. o
  404. e
  405. s
  406. n
  407. o
  408. t
  409. e
  410. x
  411. i
  412. s
  413. t
  414. ,
  415. a
  416. d
  417. d
  418. i
  419. t
  420. a
  421. s
  422. a
  423. n
  424. e
  425. w
  426. o
  427. b
  428. j
  429. e
  430. c
  431. t
  432. i
  433. n
  434. t
  435. h
  436. e
  437. a
  438. r
  439. r
  440. a
  441. y
  442. .
  443. "
  444. ,
  445. "
  446. U
  447. p
  448. d
  449. a
  450. t
  451. e
  452. t
  453. h
  454. e
  455. `
  456. c
  457. o
  458. n
  459. t
  460. e
  461. n
  462. t
  463. `
  464. v
  465. a
  466. l
  467. u
  468. e
  469. t
  470. o
  471. a
  472. c
  473. o
  474. m
  475. p
  476. e
  477. l
  478. l
  479. i
  480. n
  481. g
  482. d
  483. e
  484. s
  485. c
  486. r
  487. i
  488. p
  489. t
  490. i
  491. o
  492. n
  493. b
  494. e
  495. t
  496. w
  497. e
  498. e
  499. n
  500. 1
  501. 2
  502. 0
  503. a
  504. n
  505. d
  506. 1
  507. 6
  508. 0
  509. c
  510. h
  511. a
  512. r
  513. a
  514. c
  515. t
  516. e
  517. r
  518. s
  519. .
  520. F
  521. o
  522. r
  523. d
  524. y
  525. n
  526. a
  527. m
  528. i
  529. c
  530. p
  531. a
  532. g
  533. e
  534. s
  535. (
  536. e
  537. .
  538. g
  539. .
  540. p
  541. r
  542. o
  543. d
  544. u
  545. c
  546. t
  547. p
  548. a
  549. g
  550. e
  551. s
  552. )
  553. ,
  554. i
  555. n
  556. t
  557. e
  558. r
  559. p
  560. o
  561. l
  562. a
  563. t
  564. e
  565. d
  566. a
  567. t
  568. a
  569. f
  570. r
  571. o
  572. m
  573. t
  574. h
  575. e
  576. l
  577. o
  578. a
  579. d
  580. e
  581. r
  582. —
  583. f
  584. o
  585. r
  586. e
  587. x
  588. a
  589. m
  590. p
  591. l
  592. e
  593. :
  594. `
  595. c
  596. o
  597. n
  598. t
  599. e
  600. n
  601. t
  602. :
  603. \
  604. `
  605. $
  606. {
  607. d
  608. a
  609. t
  610. a
  611. .
  612. p
  613. r
  614. o
  615. d
  616. u
  617. c
  618. t
  619. .
  620. t
  621. i
  622. t
  623. l
  624. e
  625. }
  626. —
  627. $
  628. {
  629. d
  630. a
  631. t
  632. a
  633. .
  634. p
  635. r
  636. o
  637. d
  638. u
  639. c
  640. t
  641. .
  642. s
  643. e
  644. o
  645. .
  646. d
  647. e
  648. s
  649. c
  650. r
  651. i
  652. p
  653. t
  654. i
  655. o
  656. n
  657. ?
  658. ?
  659. d
  660. a
  661. t
  662. a
  663. .
  664. p
  665. r
  666. o
  667. d
  668. u
  669. c
  670. t
  671. .
  672. d
  673. e
  674. s
  675. c
  676. r
  677. i
  678. p
  679. t
  680. i
  681. o
  682. n
  683. .
  684. s
  685. l
  686. i
  687. c
  688. e
  689. (
  690. 0
  691. ,
  692. 1
  693. 4
  694. 0
  695. )
  696. }
  697. \
  698. `
  699. `
  700. "
  701. ,
  702. "
  703. I
  704. f
  705. m
  706. u
  707. l
  708. t
  709. i
  710. p
  711. l
  712. e
  713. r
  714. o
  715. u
  716. t
  717. e
  718. s
  719. s
  720. h
  721. a
  722. r
  723. e
  724. t
  725. h
  726. e
  727. s
  728. a
  729. m
  730. e
  731. s
  732. h
  733. o
  734. r
  735. t
  736. d
  737. e
  738. s
  739. c
  740. r
  741. i
  742. p
  743. t
  744. i
  745. o
  746. n
  747. ,
  748. u
  749. p
  750. d
  751. a
  752. t
  753. e
  754. e
  755. a
  756. c
  757. h
  758. r
  759. o
  760. u
  761. t
  762. e
  763. '
  764. s
  765. `
  766. m
  767. e
  768. t
  769. a
  770. `
  771. f
  772. u
  773. n
  774. c
  775. t
  776. i
  777. o
  778. n
  779. i
  780. n
  781. d
  782. i
  783. v
  784. i
  785. d
  786. u
  787. a
  788. l
  789. l
  790. y
  791. ,
  792. o
  793. r
  794. c
  795. r
  796. e
  797. a
  798. t
  799. e
  800. a
  801. s
  802. h
  803. a
  804. r
  805. e
  806. d
  807. u
  808. t
  809. i
  810. l
  811. i
  812. t
  813. y
  814. (
  815. e
  816. .
  817. g
  818. .
  819. a
  820. p
  821. p
  822. /
  823. u
  824. t
  825. i
  826. l
  827. s
  828. /
  829. s
  830. e
  831. o
  832. .
  833. t
  834. s
  835. )
  836. t
  837. h
  838. a
  839. t
  840. b
  841. u
  842. i
  843. l
  844. d
  845. s
  846. t
  847. h
  848. e
  849. d
  850. e
  851. s
  852. c
  853. r
  854. i
  855. p
  856. t
  857. i
  858. o
  859. n
  860. s
  861. t
  862. r
  863. i
  864. n
  865. g
  866. a
  867. n
  868. d
  869. i
  870. m
  871. p
  872. o
  873. r
  874. t
  875. i
  876. t
  877. i
  878. n
  879. t
  880. o
  881. e
  882. a
  883. c
  884. h
  885. r
  886. o
  887. u
  888. t
  889. e
  890. .
  891. "
  892. ,
  893. "
  894. R
  895. u
  896. n
  897. `
  898. n
  899. p
  900. m
  901. r
  902. u
  903. n
  904. d
  905. e
  906. v
  907. `
  908. l
  909. o
  910. c
  911. a
  912. l
  913. l
  914. y
  915. a
  916. n
  917. d
  918. i
  919. n
  920. s
  921. p
  922. e
  923. c
  924. t
  925. t
  926. h
  927. e
  928. r
  929. e
  930. n
  931. d
  932. e
  933. r
  934. e
  935. d
  936. `
  937. <
  938. m
  939. e
  940. t
  941. a
  942. n
  943. a
  944. m
  945. e
  946. =
  947. \
  948. "
  949. d
  950. e
  951. s
  952. c
  953. r
  954. i
  955. p
  956. t
  957. i
  958. o
  959. n
  960. \
  961. "
  962. >
  963. `
  964. t
  965. a
  966. g
  967. i
  968. n
  969. t
  970. h
  971. e
  972. p
  973. a
  974. g
  975. e
  976. s
  977. o
  978. u
  979. r
  980. c
  981. e
  982. (
  983. C
  984. t
  985. r
  986. l
  987. +
  988. U
  989. )
  990. t
  991. o
  992. c
  993. o
  994. n
  995. f
  996. i
  997. r
  998. m
  999. t
  1000. h
  1001. e
  1002. d
  1003. e
  1004. s
  1005. c
  1006. r
  1007. i
  1008. p
  1009. t
  1010. i
  1011. o
  1012. n
  1013. i
  1014. s
  1015. b
  1016. e
  1017. t
  1018. w
  1019. e
  1020. e
  1021. n
  1022. 1
  1023. 2
  1024. 0
  1025. –
  1026. 1
  1027. 6
  1028. 0
  1029. c
  1030. h
  1031. a
  1032. r
  1033. a
  1034. c
  1035. t
  1036. e
  1037. r
  1038. s
  1039. b
  1040. e
  1041. f
  1042. o
  1043. r
  1044. e
  1045. d
  1046. e
  1047. p
  1048. l
  1049. o
  1050. y
  1051. i
  1052. n
  1053. g
  1054. .
  1055. "
  1056. ]
Code · Generic HTML (html)
<meta name="description" content="Shop the official Nike national team jerseys — advanced Dri-FIT heat tech meets bold heritage design. Free shipping on orders over $75. Explore the full collection now.">

Place this inside the <head> element of your HTML page. Replace the content value with your 120–160 character description.

Missing canonicalModerate effort

Add a self-referencing canonical tag to every page so Google knows which URL is the definitive version of that content.

On Remix

  1. In each route file (e.g. app/routes/products.$handle.tsx, app/routes/collections.$handle.tsx, app/routes/_index.tsx), export a `meta` function if one doesn't already exist. Inside it, construct the canonical URL by combining your store's base URL with the current route path — for example: `const canonical = new URL(params.handle ? `/products/${params.handle}` : '/', 'https://www.yourstore.com').href;`
  2. Return a `<link>` descriptor from the `meta` function array for the canonical tag: `return [ { tagName: 'link', rel: 'canonical', href: canonical } ];` — Remix's `meta` export supports arbitrary tag descriptors including `<link>` elements via the `tagName` field.
  3. To avoid repeating this in every route, create a shared utility in app/utils/seo.ts that accepts a `request: Request` object and returns the canonical link descriptor: `export function canonicalLink(request: Request) { const url = new URL(request.url); url.search = ''; return { tagName: 'link', rel: 'canonical', href: url.toString() }; }` — stripping query params ensures tracking/filter params don't pollute the canonical.
  4. In each route's `loader`, pass the `request` object through to the returned data (e.g. `return json({ ..., canonicalUrl: canonicalLink(request).href })`), then reference it in the `meta` function via the `data` argument: `export const meta: MetaFunction<typeof loader> = ({ data }) => [ { tagName: 'link', rel: 'canonical', href: data.canonicalUrl } ];`
  5. For the root layout (app/root.tsx), ensure no conflicting or duplicate `<link rel='canonical'>` tags are rendered inside the `<Links />` component or hardcoded in the `<head>`. Remix's `<Links />` component automatically renders all link descriptors returned by route `meta`/`links` exports — confirm only one canonical appears per page by inspecting View Source.
  6. Deploy and validate by loading a product or collection page, viewing page source, and confirming exactly one `<link rel='canonical' href='https://www.yourstore.com/...'>` tag appears in the `<head>` with the correct, query-string-free self-referencing URL.
Code · Generic HTML (html)
<link rel="canonical" href="https://www.yourstore.com/your-page/" />

Paste this inside the <head> element of every page. Replace the href value with the full, absolute preferred URL of that specific page.

Missing og descriptionModerate effort

Add an og:description meta tag to every page so that social media platforms display a compelling, accurate preview when your pages are shared.

On Remix

  1. In your Remix project, locate the route file(s) missing the tag (e.g. app/routes/products.$handle.tsx, app/routes/index.tsx, etc.). Every route that renders a full page needs its own meta export.
  2. Add or update the `meta` export function in each route file. Use the `MetaFunction` type and return an array of descriptor objects — include one with `{ property: 'og:description', content: '...' }`. Example: ```ts import type { MetaFunction } from '@remix-run/node'; export const meta: MetaFunction<typeof loader> = ({ data }) => [ { title: data?.product?.title ?? 'Store' }, { name: 'description', content: data?.product?.description ?? '' }, { property: 'og:description', content: data?.product?.description ?? '' }, ]; ```
  3. Pull the description text from your `loader` via the `data` argument so the content is dynamic and page-specific (e.g. product description, collection description, or a static string for fixed pages like /about). Never hard-code the same string across all routes.
  4. For a site-wide fallback, update app/root.tsx's `meta` export to include a default `og:description` so that any route without its own meta export still emits the tag.
  5. Run `npm run dev` (or `yarn dev`) and inspect the `<head>` of each page type in browser DevTools to confirm `<meta property="og:description" content="...">` is present and contains the correct, page-specific text.
  6. Deploy the changes and validate using the LinkedIn Post Inspector (https://www.linkedin.com/post-inspector/) or Facebook Sharing Debugger (https://developers.facebook.com/tools/debug/) to confirm social platforms read the new tag correctly.
Code · Generic HTML (html)
<head>
  <!-- Open Graph / Social Sharing -->
  <meta property="og:type"        content="website" />
  <meta property="og:url"         content="https://www.example.com/page-url/" />
  <meta property="og:title"       content="Page Title – Brand Name" />
  <meta property="og:description" content="A concise, compelling summary of this page in 100–160 characters that encourages clicks." />
  <meta property="og:image"       content="https://www.example.com/images/social-preview.jpg" />
</head>

Place all Open Graph tags inside the <head> element of every page.

Missing og imageModerate effort

Add an og:image meta tag to every page so that social media platforms and messaging apps display a rich, branded image preview whenever someone shares your store's link.

On Remix

  1. Upload your OG image (e.g. og-image.jpg, recommended size 1200×630 px) to your project's public/ directory so it is served as a static asset at a stable, absolute URL (e.g. https://yourstore.com/og-image.jpg).
  2. Open the root route file (typically app/root.tsx or app/root.jsx). Export a meta function if one does not already exist: `export const meta: MetaFunction = () => { return [...]; };`
  3. Inside the meta function's return array, add the og:image entries — include both the standard Open Graph tag and the Twitter/X card tag: ```ts { property: 'og:image', content: 'https://yourstore.com/og-image.jpg' }, { property: 'og:image:width', content: '1200' }, { property: 'og:image:height', content: '630' }, { name: 'twitter:image', content: 'https://yourstore.com/og-image.jpg' }, ``` This sets a global fallback OG image for every page that does not override it.
  4. For product, collection, or blog route files (e.g. app/routes/products.$handle.tsx), export their own meta function and use the loader data to set a page-specific og:image, for example: ```ts export const meta: MetaFunction<typeof loader> = ({ data }) => [ { property: 'og:image', content: data?.product?.featuredImage?.url ?? 'https://yourstore.com/og-image.jpg' }, ]; ``` This ensures product images appear when those URLs are shared.
  5. Confirm that app/root.tsx renders the <Meta /> component (from `@remix-run/react`) inside the <head> of the HTML document — this is what actually outputs all meta tags to the page: `import { Meta } from '@remix-run/react'; ... <head><Meta />...</head>`
  6. Deploy the changes, then validate each key page type (home, product, collection) using the Facebook Sharing Debugger (developers.facebook.com/tools/debug) and Twitter Card Validator (cards-dev.twitter.com/validator) to confirm the og:image tag is detected and the image renders correctly.
Missing og titleQuick win

Add an og:title meta tag to every page so your store's title appears correctly when shared on social media platforms like Facebook, LinkedIn, and Pinterest.

On Remix

  1. In your Remix project, open the root route file (typically `app/root.tsx` or `app/root.jsx`). This is where global meta tags should live so they apply to every page by default.
  2. Locate or add the exported `meta` function at the top level of the file. If it doesn't exist, add it: `export const meta: MetaFunction = () => { return []; };`
  3. Inside the `meta` function's return array, add an og:title entry: `{ property: 'og:title', content: 'Your Store Name' }`. Use a meaningful, brand-consistent title (e.g. your store's actual name). Example full export: ```ts export const meta: MetaFunction = () => [ { property: 'og:title', content: 'Your Store Name' }, ]; ```
  4. For individual routes (product pages, collection pages, etc.) that need unique og:title values, open each route file under `app/routes/` and add or update its own `meta` export. Use loader data to make it dynamic, for example: ```ts export const meta: MetaFunction<typeof loader> = ({ data }) => [ { property: 'og:title', content: data?.product?.title ?? 'Your Store Name' }, ]; ```
  5. Ensure your `app/root.tsx` renders the `<Meta />` component inside the `<head>` of its `Layout` or `default` export (e.g. `<head><Meta /><Links />...</head>`). This is what actually outputs the meta tags into the HTML — without it, the `meta` function exports have no effect.
  6. Run `npm run dev` (or `yarn dev`) to preview locally. Open DevTools → Elements and confirm `<meta property="og:title" content="...">` appears in `<head>` on both the root page and any route-specific pages you updated. Then deploy your changes.
Code · Generic HTML (html)
<head>
  <!-- Other head tags -->
  <meta property="og:title" content="Amazing Product Name | Your Store Name" />
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://www.yourstore.com/page-url" />
  <meta property="og:description" content="Short description of this page for social sharing." />
  <!-- Other head tags -->
</head>

Place inside the <head> section of every HTML page. Replace the content value with your actual page title.

Multiple h1Moderate effort

Remove all but one H1 tag from each page so every page has exactly one clear, descriptive primary heading.

On Remix

  1. Identify all Remix route files that render H1 tags — check every file under `app/routes/` (e.g. `app/routes/_index.tsx`, `app/routes/products.$handle.tsx`, `app/routes/collections.$handle.tsx`) and any shared layout components in `app/components/` or `app/root.tsx`. Search the codebase for `<h1` to get a full list.
  2. For each route, audit the full render tree: open the route file, trace through every component it imports (including shared headers, hero banners, page-title components, or CMS-driven content blocks), and list every `<h1>` that could be rendered simultaneously on a single page. Pay special attention to `app/root.tsx` — any `<h1>` placed there will appear on every page.
  3. Decide on the single, authoritative `<h1>` per page — typically the main page title (product name, collection name, blog post title, or homepage headline). Demote all other `<h1>` tags to the appropriate heading level (`<h2>`, `<h3>`, etc.) to preserve visual hierarchy without duplicating the primary heading signal.
  4. If a shared layout component (e.g. a `<PageHeader>` or `<Hero>` component) renders an `<h1>`, and individual route files also render their own `<h1>`, refactor so only one source of truth exists — for example, pass a `level` prop to the shared component (`level={2}` from routes that supply their own `<h1>`) or remove the `<h1>` from the layout and always render it inside the route-level component.
  5. For dynamic routes (e.g. `products.$handle.tsx`), confirm the `<h1>` is populated from the route's `loader` data (e.g. `product.title`, `collection.title`) and is rendered exactly once in the JSX — not both inside a hero section and again in a product-detail section within the same route.
  6. After making changes, run `npm run dev` and visit each page type in the browser. Use DevTools (Elements panel → Ctrl+F `h1`) or a browser extension to verify exactly one `<h1>` exists in the DOM per page, including pages rendered via Remix's nested layouts (`<Outlet>` chains), then deploy and re-crawl to confirm the fix.
Code · Generic HTML (html)
<!-- CORRECT: One H1 per page -->
<h1>Blue Running Shoes – Men's Lightweight Trainers</h1>

<!-- Major sections use H2 -->
<h2>Product Details</h2>
<p>Breathable mesh upper, responsive foam midsole...</p>

<h2>Customer Reviews</h2>
<!-- Sub-sections within a major section use H3 -->
<h3>Top Review: 5 Stars</h3>
<p>Best shoes I've ever bought!</p>

<!-- WRONG: Multiple H1 tags — do NOT do this -->
<!--
<h1>Blue Running Shoes</h1>
<h1>Product Details</h1>
<h1>Customer Reviews</h1>
-->

Correct semantic heading structure for an ecommerce page — one H1, everything else H2 or lower.

Seo category description missingModerate effort

Write a 100–200 word description for every category page explaining what products it contains and who they are for, so Google can rank it for category-level keywords.

On Remix

  1. In your Remix project, locate the route file that renders category pages — typically something like `app/routes/categories.$handle.tsx` (or `.jsx`). This is where you'll add the description content.
  2. Fetch the category description from your data source (CMS, Shopify Storefront API, custom DB, etc.) inside the route's `loader` function. If no description field exists yet, add one to your data model first. Example: `const category = await getCategoryByHandle(params.handle); return json({ category });`
  3. In the route's page component, destructure the `category` from `useLoaderData()` and render the description in a visible `<p>` (or `<div>`) tag placed directly below the category heading — not hidden via CSS — so both users and Googlebot can read it. Example: `<p className="category-description">{category.description}</p>`
  4. Using Remix's `meta` export, also surface the description as the page's `<meta name="description">` tag so Google can use it in SERPs. Example: `export const meta = ({ data }) => [{ name: 'description', content: data.category.description }];`
  5. For every category that currently has no description, write a 100–200 word text in your CMS/data source explaining what products the category contains and who they are for, naturally incorporating the target category-level keyword (e.g. 'women's running shoes'). Save and redeploy if descriptions are hard-coded, or publish via the CMS if content is fetched at runtime.
  6. After deploying, use `curl -A Googlebot <your-category-URL>` or Google Search Console's URL Inspection tool to confirm the description text appears in the server-rendered HTML (Remix SSR/SSG ensures it is present before JS executes), then request re-indexing for each updated category URL.
Code · Generic HTML (html)
<section class="category-description">
  <h1>Women's Waterproof Hiking Boots</h1>
  <p>
    Our women's waterproof hiking boots are built for trail runners, weekend
    trekkers, and serious mountaineers who refuse to let wet weather slow them
    down. Every pair in this collection features a fully sealed waterproof
    membrane, aggressive rubber outsoles for grip on loose or muddy terrain,
    and reinforced ankle support to protect against sprains on uneven ground.
    Whether you're day-hiking a local forest trail or taking on a multi-day
    alpine route, you'll find boots here rated for every conditions and
    experience level. Lightweight options keep pack weight low on fast-and-light
    adventures, while insulated models keep feet warm below freezing. Browse the
    full range below to find your perfect fit.
  </p>
</section>

Place this block near the top of your category page, above the product grid.

Seo category emptyQuick win

Either populate the empty category with products, or add a noindex tag and remove it from your navigation so Google does not waste crawl budget on a page with no content.

On Remix

  1. Identify the empty category route file in your Remix project (e.g. app/routes/collections/$category.tsx or app/routes/category/$handle.tsx) and locate the loader function that fetches products for that category.
  2. In the loader, detect when the category returns zero products — for example: `if (products.length === 0) { return json({ products: [], empty: true }); }`.
  3. In the same route file, update the exported `meta` function to conditionally inject a `noindex` robots tag when the category is empty: ```ts export const meta: MetaFunction = ({ data }) => { if (data?.empty) { return [{ name: 'robots', content: 'noindex, nofollow' }]; } return [{ title: data?.category?.title }]; }; ```
  4. Remove empty category links from your navigation by checking the `empty` flag in any nav-building loader or component (e.g. in app/root.tsx or a shared Header component) — filter out categories whose product count is 0 before rendering `<Link>` elements.
  5. If you prefer to populate rather than noindex, seed real products into the empty category via your CMS or commerce backend (e.g. Shopify, Medusa, Commerce.js) and verify the loader now returns at least one product before removing any noindex logic.
  6. Deploy the changes, then submit the affected URLs for recrawl in Google Search Console (Inspect URL → Request Indexing) and confirm the `noindex` header or tag appears in the page source for any remaining empty categories.
Code · Generic HTML (html)
<meta name="robots" content="noindex, follow">

Place inside the <head> element of an empty category page to prevent Google from indexing it.

Title too longQuick win

Shorten your page title tag to 60 characters or fewer so it displays fully — without truncation — in Google search results.

On Remix

  1. Identify the route file responsible for the page with the too-long title (e.g. app/routes/products.$handle.tsx or app/routes/_index.tsx) inside your Remix project.
  2. Open that route file and locate the exported `meta` function — it will look like: `export const meta: MetaFunction = () => [ { title: '...' }, ... ];`
  3. Count the characters in the current title string. Rewrite it to be 60 characters or fewer, keeping the most important keyword(s) near the front (e.g. 'Keyword-Rich Product Name | Brand' instead of a verbose sentence).
  4. If a shared/global title is set in app/root.tsx via its own `meta` export or an `<Links>` / `<Meta>` component, check that file too and shorten any default or fallback title template to stay within 60 characters including the brand suffix.
  5. If you use a title template pattern (e.g. `${pageTitle} | ${siteName}`), calculate the worst-case length: `pageTitle` max chars = 60 minus separator and brand name length, then enforce that limit when generating `pageTitle` dynamically.
  6. Run `npm run dev`, open the page in a browser, inspect the `<title>` tag in DevTools to confirm the rendered title is 60 characters or fewer, then deploy the change.
Code · Generic HTML (html)
<head>
  <meta charset="UTF-8">
  <title>Running Shoes for Men | YourBrand</title>
</head>

Place inside the <head> element of every HTML page. Keep the total character count at or under 60.

Title too shortQuick win

Expand your page title to between 30 and 60 characters so Google displays it fully in search results and shoppers can immediately understand what the page is about.

On Remix

  1. Identify the route file responsible for the page with the short title (e.g. app/routes/products.$handle.tsx or app/routes/collections.$handle.tsx) inside your Remix project.
  2. Open that route file and locate the exported `meta` function (or add one if it is missing). In Remix v2+ it uses the signature: `export const meta: MetaFunction = ({ data }) => { return [...]; };`
  3. Inside the `meta` function, find or add the `{ title: '...' }` object in the returned array. Rewrite the title string so it is between 30 and 60 characters, is descriptive, and includes your primary keyword — for example: `{ title: 'Organic Arabica Coffee Beans | Free Shipping | BrandName' }`
  4. If the title relies on dynamic data (e.g. a product name fetched in the `loader`), compose it with template literals and pad it with your store name or a value proposition to reach the 30-character minimum: e.g. `` `${data.product.title} – Shop at BrandName` ``
  5. Run `npm run dev` (or `yarn dev`) locally and inspect the `<title>` tag in the browser's page source to confirm the rendered character count is between 30 and 60 characters.
  6. Deploy the updated route file. Re-crawl the URL in Google Search Console (Inspect URL → Request Indexing) to prompt Google to pick up the new title.
Code · Generic HTML (html)
<head>
  <meta charset="UTF-8">
  <title>Men's Merino Wool Beanies – BrandName</title>
</head>

Place inside the <head> element of every page. Replace the example with your actual descriptive title (30–60 chars).