{"record":{"id":"57894144c89e4e4a","repo":"antiwork/gumroad","slug":"cannot-switch-to-shared-content-multiple-variants","errorCode":null,"errorMessage":"Cannot switch to shared content: multiple variants have distinct content. Remove variant content first, or send replacement rich_content in the same request.","messagePattern":"Cannot switch to shared content: multiple variants have distinct content\\. Remove variant content first, or send replacement rich_content in the same request\\.","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":200,"severity":"error","filePath":"app/controllers/api/v2/links_controller.rb","lineNumber":981,"sourceCode":"      if @product.has_same_rich_content_for_all_variants?\n        migrate_to_shared_rich_content!\n      else\n        migrate_to_per_variant_rich_content!\n      end\n    end\n\n    def migrate_to_shared_rich_content!\n      variants_with_content = @product.alive_variants.select { |v| v.alive_rich_contents.any? }\n\n      if @product.alive_rich_contents.any? && variants_with_content.any?\n        raise Link::LinkInvalid, \"Cannot switch to shared content: both product-level and variant-level content exist. Remove one side first, or send replacement rich_content in the same request.\"\n      end\n\n      if variants_with_content.length > 1\n        canonical = canonicalize_rich_contents(variants_with_content.first)\n        all_identical = variants_with_content.all? { |v| canonicalize_rich_contents(v) == canonical }\n        if !all_identical\n          raise Link::LinkInvalid, \"Cannot switch to shared content: multiple variants have distinct content. Remove variant content first, or send replacement rich_content in the same request.\"\n        end\n      end\n\n      source_variant = nil\n      if @product.alive_rich_contents.empty? && variants_with_content.any?\n        source_variant = variants_with_content.first\n        source_variant.alive_rich_contents.sort_by(&:position).each_with_index do |rc, index|\n          @product.alive_rich_contents.create!(title: rc.title, description: rc.description_without_stale_dead_cross_product_file_embeds, position: index)\n        end\n      end\n\n      @product.alive_variants.each do |variant|\n        retire_upsells_from_rich_contents!(variant.alive_rich_contents) if variant != source_variant\n        variant.alive_rich_contents.each(&:mark_deleted!)\n        variant.product_files = []\n      end\n    end\n","sourceCodeStart":963,"sourceCodeEnd":999,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/api/v2/links_controller.rb#L963-L999","documentation":"Link::LinkInvalid raised by migrate_to_shared_rich_content! when switching to shared content: more than one variant has alive rich contents and they are not identical after canonicalization (canonicalize_rich_contents normalizes for comparison). Shared mode renders one content set for all variants, so distinct variant contents cannot be auto-merged — the API names the conflict and requires deduplication or replacement content.","triggerScenarios":"PATCH /api/v2/links/:id flipping has_same_rich_content_for_all_variants to true (without replacement rich_content) on a product whose two-plus variants each have content that differs in any canonicalized way (text, embeds, ordering, page count).","commonSituations":"Sellers who customized content per variant (the whole point of per-variant mode) later merging; identical-looking pages that differ by an embed id or trailing whitespace caught by canonicalization; scripts assuming 'variants have same content' without comparing.","solutions":["Pick the winning variant's content: delete the other variants' content, then switch the flag (the remaining single variant migrates up).","Or send rich_content in the same switch request to explicitly define the shared content.","If you believe the contents are equivalent, diff them canonically (embeds ids, page breaks, titles) to find the real difference.","Bulk tooling: only auto-switch products where variants_with_content.length <= 1 or contents canonicalize equal."],"exampleFix":"# before: distinct variant contents + flag flip -> rejected\nLinkApi.update(link_id, has_same_rich_content_for_all_variants: true)\n# after: choose winner explicitly\nvariant_b_contents.each(&:delete)\nLinkApi.update(link_id, has_same_rich_content_for_all_variants: true) # variant A's content migrates up","handlingStrategy":"validation","validationCode":"contents = product.alive_variants.map(&:alive_rich_contents)\ncanonical = contents.map { |set| canonicalize(set) }.uniq\nauto_switch_only_if(contents.length <= 1 || canonical.length == 1)","typeGuard":"def variant_contents_identical?(product)\n  sets = product.alive_variants.map(&:alive_rich_contents).reject(&:empty?)\n  return true if sets.length <= 1\n  sets.map { |s| canonicalize_rich_contents(s) }.uniq.length == 1\nend","tryCatchPattern":null,"preventionTips":["Before switching to shared, canonicalize and compare variant contents; only auto-switch when identical.","Explicitly choose the winning variant's content otherwise.","Include replacement rich_content in the switch request to skip auto-merge entirely."],"tags":["api","rich-content","variants","migration","products","validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}