[{"data":1,"prerenderedAt":1240},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fwrite-metadata-txt-qgis-plugin":3},{"id":4,"title":5,"body":6,"description":1233,"extension":1234,"meta":1235,"navigation":419,"path":1236,"seo":1237,"stem":1238,"__hash__":1239},"docs\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fwrite-metadata-txt-qgis-plugin\u002Findex.md","Write metadata.txt for a QGIS Plugin",{"type":7,"value":8,"toc":1220},"minimark",[9,13,30,37,42,64,68,75,85,102,106,109,183,187,190,294,298,301,387,598,616,619,892,916,920,957,966,970,978,1042,1048,1052,1064,1075,1089,1101,1113,1127,1131,1142,1146,1165,1177,1186,1198,1202,1216],[10,11,5],"h1",{"id":12},"write-metadatatxt-for-a-qgis-plugin",[14,15,16,20,21,26,27,29],"p",{},[17,18,19],"code",{},"metadata.txt"," is the single file QGIS and the official repository read to identify, version, and display your plugin. Get one key wrong and the plugin manager silently hides your extension, or plugins.qgis.org refuses the upload with a terse error. This task page, part of ",[22,23,25],"a",{"href":24},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002F","Publishing to the QGIS Plugin Repository",", gives you a complete, field-by-field annotated ",[17,28,19],{}," and the exact rules behind every key so your first upload passes validation.",[14,31,32,33,36],{},"The file is a plain INI document with a single ",[17,34,35],{},"[general]"," section. There is no schema enforcement inside QGIS beyond the required keys, but the repository's parser is strict — it is the parser, not QGIS Desktop, that decides whether your archive is accepted.",[38,39,41],"h2",{"id":40},"prerequisites","Prerequisites",[43,44,45,54,61],"ul",{},[46,47,48,49,53],"li",{},"A plugin folder created from a generator or by hand — see ",[22,50,52],{"href":51},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fcreate-qgis-plugin-with-plugin-builder\u002F","Create a QGIS Plugin with Plugin Builder",".",[46,55,56,60],{},[57,58,59],"strong",{},"QGIS 3.34 LTR"," for validation (bundles Python 3.12).",[46,62,63],{},"A text editor that preserves UTF-8 and does not convert tabs\u002Fspaces unexpectedly.",[38,65,67],{"id":66},"the-complete-annotated-metadatatxt","The Complete Annotated metadata.txt",[14,69,70,71,74],{},"Below is a full, valid file. Every key the repository understands appears, with required keys listed first. The inline ",[17,72,73],{},"#"," comments are for this guide only — keep real comments to a minimum, as the repository parser tolerates them but they add noise.",[76,77,83],"pre",{"className":78,"code":80,"language":81,"meta":82},[79],"language-text","[general]\n# ---- REQUIRED KEYS ----\nname=Parcel Validator\n# Human-readable display name shown in the manager. Free text, can contain spaces.\n\nqgisMinimumVersion=3.34\n# Oldest QGIS that will offer this plugin. MAJOR.MINOR (or MAJOR.MINOR.PATCH).\n\ndescription=Validates parcel polygons for gaps, overlaps, and invalid geometries.\n# One short line. Shown in the manager list. No line breaks.\n\nversion=1.0.0\n# Plugin version. Use semantic versioning MAJOR.MINOR.PATCH.\n\nauthor=Jane Cartographer\n# Author or organisation name. Plain text.\n\nemail=jane@example.com\n# Contact address. Hidden from the public listing but required.\n\nabout=Parcel Validator scans a cadastral layer and reports topology\n    problems: sliver gaps between parcels, overlapping boundaries, and\n    self-intersecting rings. Results are written to a styled error layer\n    you can review and fix before submission.\n# Longer paragraph shown on the plugin's page. Continuation lines are indented.\n\n# ---- OPTIONAL BUT STRONGLY RECOMMENDED ----\nrepository=https:\u002F\u002Fgithub.com\u002Fjane\u002Fparcel-validator\n# Public source URL. Required for trust; reviewers check it.\n\ntracker=https:\u002F\u002Fgithub.com\u002Fjane\u002Fparcel-validator\u002Fissues\n# Bug tracker URL.\n\nhomepage=https:\u002F\u002Fjane.example.com\u002Fparcel-validator\n# Documentation or landing page.\n\ncategory=Vector\n# One of: Raster, Vector, Database, Web. Controls the menu placement hint.\n\ntags=topology,cadastre,validation,parcels,quality\n# Comma-separated, no spaces preferred. Improves search on the repository.\n\nicon=icons\u002Ficon.png\n# Path relative to the plugin folder, or a Qt resource path (:\u002F...).\n\nexperimental=False\n# True hides the version unless the user opts into experimental plugins.\n\ndeprecated=False\n# True marks the whole plugin as unmaintained.\n\nqgisMaximumVersion=3.99\n# Newest QGIS that will offer the plugin. Set high to stay visible.\n\nchangelog=1.0.0 - Initial release\n# Multi-line, newest first. Rendered on the plugin page.\n\nplugin_dependencies=Processing,QuickMapServices\n# Other plugins this one needs, by display name, comma-separated.\n","text","",[17,84,80],{"__ignoreMap":82},[14,86,87,88,91,92,95,96,99,100,53],{},"The ",[17,89,90],{},"experimental"," flag here is ",[17,93,94],{},"False","; flip it to ",[17,97,98],{},"True"," when you ship a risky pre-release. The publishing workflow that consumes this file is covered in ",[22,101,25],{"href":24},[38,103,105],{"id":104},"required-keys-in-detail","Required Keys in Detail",[14,107,108],{},"These seven keys must be present and non-empty or the upload fails outright.",[43,110,111,127,139,147,159,167,175],{},[46,112,113,118,119,123,124,126],{},[57,114,115],{},[17,116,117],{},"name"," — The display name. It does ",[120,121,122],"em",{},"not"," have to match the folder\u002Fpackage name (the folder name must be a valid Python identifier; ",[17,125,117],{}," can be any text).",[46,128,129,134,135,138],{},[57,130,131],{},[17,132,133],{},"qgisMinimumVersion"," — A version string like ",[17,136,137],{},"3.34",". The manager hides the plugin from any older QGIS. Do not pad it to a patch you have not tested.",[46,140,141,146],{},[57,142,143],{},[17,144,145],{},"description"," — A single line, no newlines. This is the teaser shown in the plugin list, so make it specific.",[46,148,149,154,155,158],{},[57,150,151],{},[17,152,153],{},"version"," — ",[17,156,157],{},"MAJOR.MINOR.PATCH",". The repository compares this against the live release and rejects anything not strictly greater.",[46,160,161,166],{},[57,162,163],{},[17,164,165],{},"author"," — Plain text name or organisation.",[46,168,169,174],{},[57,170,171],{},[17,172,173],{},"email"," — A valid address. It is hidden publicly but must parse as an email.",[46,176,177,182],{},[57,178,179],{},[17,180,181],{},"about"," — A fuller description. Use indented continuation lines for multiple paragraphs.",[38,184,186],{"id":185},"optional-keys-in-detail","Optional Keys in Detail",[14,188,189],{},"Optional keys are not enforced by the repository's required-field check, but several materially affect discoverability and trust.",[43,191,192,200,208,216,238,246,258,270,278,286],{},[46,193,194,199],{},[57,195,196],{},[17,197,198],{},"repository"," — Public VCS URL. Reviewers expect it; omitting it slows first approval.",[46,201,202,207],{},[57,203,204],{},[17,205,206],{},"tracker"," — Where users file bugs.",[46,209,210,215],{},[57,211,212],{},[17,213,214],{},"homepage"," — Documentation site or project page.",[46,217,218,154,223,226,227,226,230,233,234,237],{},[57,219,220],{},[17,221,222],{},"category",[17,224,225],{},"Raster",", ",[17,228,229],{},"Vector",[17,231,232],{},"Database",", or ",[17,235,236],{},"Web",". A hint for where the plugin's tools belong.",[46,239,240,245],{},[57,241,242],{},[17,243,244],{},"tags"," — Comma-separated keywords. The repository search indexes these, so include the terms users actually type.",[46,247,248,154,252,254,255,257],{},[57,249,250],{},[17,251,90],{},[17,253,98],{},"\u002F",[17,256,94],{},". Per-version visibility toggle.",[46,259,260,154,265,254,267,269],{},[57,261,262],{},[17,263,264],{},"deprecated",[17,266,98],{},[17,268,94],{},". Marks the plugin unmaintained.",[46,271,272,277],{},[57,273,274],{},[17,275,276],{},"changelog"," — Multi-line release notes, newest first, rendered on the plugin page and shown when an update is available.",[46,279,280,285],{},[57,281,282],{},[17,283,284],{},"icon"," — Relative path to a PNG\u002FSVG, or a Qt resource path. A missing icon shows a generic placeholder.",[46,287,288,293],{},[57,289,290],{},[17,291,292],{},"plugin_dependencies"," — Comma-separated display names of other plugins. The manager prompts the user to install them.",[38,295,297],{"id":296},"formatting-rules","Formatting Rules",[14,299,300],{},"The parser is unforgiving about a few things. Internalize these and most validation errors disappear:",[302,303,304,313,332,338,347,368],"ol",{},[46,305,306,312],{},[57,307,308,309,311],{},"Exactly one ",[17,310,35],{}," section header",", on its own line, before any keys.",[46,314,315,324,325,328,329,331],{},[57,316,317,320,321],{},[17,318,319],{},"key=value"," with no spaces around ",[17,322,323],{},"="," is the safe convention. ",[17,326,327],{},"name = X"," works in some parsers but ",[17,330,319],{}," is what every example and the validator expect.",[46,333,334,337],{},[57,335,336],{},"Continuation lines must be indented"," (leading spaces or a tab). An un-indented second line is read as a new, invalid key.",[46,339,340,343,344,346],{},[57,341,342],{},"UTF-8 encoding",", no byte-order mark. A BOM can make the first key (",[17,345,35],{},") unparseable.",[46,348,349,356,357,226,360,363,364,367],{},[57,350,351,352,254,354],{},"Booleans are ",[17,353,98],{},[17,355,94],{}," (capitalized). ",[17,358,359],{},"true",[17,361,362],{},"yes",", and ",[17,365,366],{},"1"," are not reliably interpreted.",[46,369,370,378,379,382,383,386],{},[57,371,372,374,375,377],{},[17,373,153],{}," and ",[17,376,133],{}," are dotted numerics."," Do not prefix with ",[17,380,381],{},"v"," (",[17,384,385],{},"v1.0.0"," is invalid).",[76,388,392],{"className":389,"code":390,"language":391,"meta":82,"style":82},"language-python shiki shiki-themes github-dark","# Quick local sanity check before uploading\nimport configparser\n\ncfg = configparser.ConfigParser()\ncfg.read(\"parcel_validator\u002Fmetadata.txt\", encoding=\"utf-8\")\n\nrequired = [\"name\", \"qgisMinimumVersion\", \"description\",\n            \"version\", \"author\", \"email\", \"about\"]\nmissing = [k for k in required if not cfg[\"general\"].get(k)]\nprint(\"Missing required keys:\", missing or \"none\")\nprint(\"Declared version:\", cfg[\"general\"][\"version\"])\n","python",[17,393,394,403,414,421,432,456,461,488,512,550,574],{"__ignoreMap":82},[395,396,399],"span",{"class":397,"line":398},"line",1,[395,400,402],{"class":401},"sAwPA","# Quick local sanity check before uploading\n",[395,404,406,410],{"class":397,"line":405},2,[395,407,409],{"class":408},"snl16","import",[395,411,413],{"class":412},"s95oV"," configparser\n",[395,415,417],{"class":397,"line":416},3,[395,418,420],{"emptyLinePlaceholder":419},true,"\n",[395,422,424,427,429],{"class":397,"line":423},4,[395,425,426],{"class":412},"cfg ",[395,428,323],{"class":408},[395,430,431],{"class":412}," configparser.ConfigParser()\n",[395,433,435,438,442,444,448,450,453],{"class":397,"line":434},5,[395,436,437],{"class":412},"cfg.read(",[395,439,441],{"class":440},"sU2Wk","\"parcel_validator\u002Fmetadata.txt\"",[395,443,226],{"class":412},[395,445,447],{"class":446},"s9osk","encoding",[395,449,323],{"class":408},[395,451,452],{"class":440},"\"utf-8\"",[395,454,455],{"class":412},")\n",[395,457,459],{"class":397,"line":458},6,[395,460,420],{"emptyLinePlaceholder":419},[395,462,464,467,469,472,475,477,480,482,485],{"class":397,"line":463},7,[395,465,466],{"class":412},"required ",[395,468,323],{"class":408},[395,470,471],{"class":412}," [",[395,473,474],{"class":440},"\"name\"",[395,476,226],{"class":412},[395,478,479],{"class":440},"\"qgisMinimumVersion\"",[395,481,226],{"class":412},[395,483,484],{"class":440},"\"description\"",[395,486,487],{"class":412},",\n",[395,489,491,494,496,499,501,504,506,509],{"class":397,"line":490},8,[395,492,493],{"class":440},"            \"version\"",[395,495,226],{"class":412},[395,497,498],{"class":440},"\"author\"",[395,500,226],{"class":412},[395,502,503],{"class":440},"\"email\"",[395,505,226],{"class":412},[395,507,508],{"class":440},"\"about\"",[395,510,511],{"class":412},"]\n",[395,513,515,518,520,523,526,529,532,535,538,541,544,547],{"class":397,"line":514},9,[395,516,517],{"class":412},"missing ",[395,519,323],{"class":408},[395,521,522],{"class":412}," [k ",[395,524,525],{"class":408},"for",[395,527,528],{"class":412}," k ",[395,530,531],{"class":408},"in",[395,533,534],{"class":412}," required ",[395,536,537],{"class":408},"if",[395,539,540],{"class":408}," not",[395,542,543],{"class":412}," cfg[",[395,545,546],{"class":440},"\"general\"",[395,548,549],{"class":412},"].get(k)]\n",[395,551,553,557,560,563,566,569,572],{"class":397,"line":552},10,[395,554,556],{"class":555},"sDLfK","print",[395,558,559],{"class":412},"(",[395,561,562],{"class":440},"\"Missing required keys:\"",[395,564,565],{"class":412},", missing ",[395,567,568],{"class":408},"or",[395,570,571],{"class":440}," \"none\"",[395,573,455],{"class":412},[395,575,577,579,581,584,587,589,592,595],{"class":397,"line":576},11,[395,578,556],{"class":555},[395,580,559],{"class":412},[395,582,583],{"class":440},"\"Declared version:\"",[395,585,586],{"class":412},", cfg[",[395,588,546],{"class":440},[395,590,591],{"class":412},"][",[395,593,594],{"class":440},"\"version\"",[395,596,597],{"class":412},"])\n",[14,599,600,603,604,607,608,610,611,615],{},[57,601,602],{},"Breakdown:"," Python's ",[17,605,606],{},"configparser"," reads the same INI format the repository expects, so it surfaces section and continuation errors locally. This catches an unindented ",[17,609,181],{}," paragraph or a missing required key before you waste an upload round-trip. Wire this into the test suite described in ",[22,612,614],{"href":613},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","Unit Test a QGIS Plugin with pytest"," so every commit validates the metadata automatically.",[14,617,618],{},"You can take the check one step further and assert that the version is well-formed and the booleans parse, turning the loose INI into something closer to a schema:",[76,620,622],{"className":389,"code":621,"language":391,"meta":82,"style":82},"import re\n\ngeneral = cfg[\"general\"]\n\n# version must be MAJOR.MINOR.PATCH with no leading 'v'\nassert re.fullmatch(r\"\\d+\\.\\d+\\.\\d+\", general[\"version\"]), \\\n    f\"bad version: {general['version']!r}\"\n\n# booleans must be exactly True\u002FFalse\nfor flag in (\"experimental\", \"deprecated\"):\n    if flag in general:\n        assert general[flag] in (\"True\", \"False\"), \\\n            f\"{flag} must be True or False, got {general[flag]!r}\"\n\n# qgisMinimumVersion must be dotted numeric\nassert re.fullmatch(r\"\\d+\\.\\d+(\\.\\d+)?\", general[\"qgisMinimumVersion\"])\nprint(\"metadata.txt is well-formed\")\n",[17,623,624,631,635,648,652,657,701,730,734,739,761,773,797,826,831,837,880],{"__ignoreMap":82},[395,625,626,628],{"class":397,"line":398},[395,627,409],{"class":408},[395,629,630],{"class":412}," re\n",[395,632,633],{"class":397,"line":405},[395,634,420],{"emptyLinePlaceholder":419},[395,636,637,640,642,644,646],{"class":397,"line":416},[395,638,639],{"class":412},"general ",[395,641,323],{"class":408},[395,643,543],{"class":412},[395,645,546],{"class":440},[395,647,511],{"class":412},[395,649,650],{"class":397,"line":423},[395,651,420],{"emptyLinePlaceholder":419},[395,653,654],{"class":397,"line":434},[395,655,656],{"class":401},"# version must be MAJOR.MINOR.PATCH with no leading 'v'\n",[395,658,659,662,665,668,671,674,677,681,683,685,687,689,691,693,696,698],{"class":397,"line":458},[395,660,661],{"class":408},"assert",[395,663,664],{"class":412}," re.fullmatch(",[395,666,667],{"class":408},"r",[395,669,670],{"class":440},"\"",[395,672,673],{"class":555},"\\d",[395,675,676],{"class":408},"+",[395,678,680],{"class":679},"sRjNt","\\.",[395,682,673],{"class":555},[395,684,676],{"class":408},[395,686,680],{"class":679},[395,688,673],{"class":555},[395,690,676],{"class":408},[395,692,670],{"class":440},[395,694,695],{"class":412},", general[",[395,697,594],{"class":440},[395,699,700],{"class":412},"]), \\\n",[395,702,703,706,709,712,715,718,721,724,727],{"class":397,"line":463},[395,704,705],{"class":408},"    f",[395,707,708],{"class":440},"\"bad version: ",[395,710,711],{"class":555},"{",[395,713,714],{"class":412},"general[",[395,716,717],{"class":440},"'version'",[395,719,720],{"class":412},"]",[395,722,723],{"class":408},"!r",[395,725,726],{"class":555},"}",[395,728,729],{"class":440},"\"\n",[395,731,732],{"class":397,"line":490},[395,733,420],{"emptyLinePlaceholder":419},[395,735,736],{"class":397,"line":514},[395,737,738],{"class":401},"# booleans must be exactly True\u002FFalse\n",[395,740,741,743,746,748,750,753,755,758],{"class":397,"line":552},[395,742,525],{"class":408},[395,744,745],{"class":412}," flag ",[395,747,531],{"class":408},[395,749,382],{"class":412},[395,751,752],{"class":440},"\"experimental\"",[395,754,226],{"class":412},[395,756,757],{"class":440},"\"deprecated\"",[395,759,760],{"class":412},"):\n",[395,762,763,766,768,770],{"class":397,"line":576},[395,764,765],{"class":408},"    if",[395,767,745],{"class":412},[395,769,531],{"class":408},[395,771,772],{"class":412}," general:\n",[395,774,776,779,782,784,786,789,791,794],{"class":397,"line":775},12,[395,777,778],{"class":408},"        assert",[395,780,781],{"class":412}," general[flag] ",[395,783,531],{"class":408},[395,785,382],{"class":412},[395,787,788],{"class":440},"\"True\"",[395,790,226],{"class":412},[395,792,793],{"class":440},"\"False\"",[395,795,796],{"class":412},"), \\\n",[395,798,800,803,805,807,810,812,815,817,820,822,824],{"class":397,"line":799},13,[395,801,802],{"class":408},"            f",[395,804,670],{"class":440},[395,806,711],{"class":555},[395,808,809],{"class":412},"flag",[395,811,726],{"class":555},[395,813,814],{"class":440}," must be True or False, got ",[395,816,711],{"class":555},[395,818,819],{"class":412},"general[flag]",[395,821,723],{"class":408},[395,823,726],{"class":555},[395,825,729],{"class":440},[395,827,829],{"class":397,"line":828},14,[395,830,420],{"emptyLinePlaceholder":419},[395,832,834],{"class":397,"line":833},15,[395,835,836],{"class":401},"# qgisMinimumVersion must be dotted numeric\n",[395,838,840,842,844,846,848,850,852,854,856,858,860,862,864,866,869,872,874,876,878],{"class":397,"line":839},16,[395,841,661],{"class":408},[395,843,664],{"class":412},[395,845,667],{"class":408},[395,847,670],{"class":440},[395,849,673],{"class":555},[395,851,676],{"class":408},[395,853,680],{"class":679},[395,855,673],{"class":555},[395,857,676],{"class":408},[395,859,559],{"class":555},[395,861,680],{"class":679},[395,863,673],{"class":555},[395,865,676],{"class":408},[395,867,868],{"class":555},")",[395,870,871],{"class":408},"?",[395,873,670],{"class":440},[395,875,695],{"class":412},[395,877,479],{"class":440},[395,879,597],{"class":412},[395,881,883,885,887,890],{"class":397,"line":882},17,[395,884,556],{"class":555},[395,886,559],{"class":412},[395,888,889],{"class":440},"\"metadata.txt is well-formed\"",[395,891,455],{"class":412},[14,893,894,896,897,900,901,903,904,906,907,254,909,912,913,915],{},[57,895,602],{}," ",[17,898,899],{},"re.fullmatch"," rejects the two most common formatting mistakes — a ",[17,902,381],{}," prefix on ",[17,905,153],{}," and lowercase ",[17,908,359],{},[17,910,911],{},"false"," booleans — before they reach the repository parser. Because this is plain Python, it runs in CI on every push at no cost, so a malformed ",[17,914,19],{}," fails the build instead of a release upload.",[38,917,919],{"id":918},"how-qgis-and-the-repository-read-each-field","How QGIS and the Repository Read Each Field",[14,921,922,923,925,926,226,928,931,932,934,935,937,938,940,941,226,943,226,945,226,947,226,949,363,951,953,954,956],{},"It helps to know which consumer cares about which key, because the two read ",[17,924,19],{}," for different reasons. QGIS Desktop reads it at startup to decide whether to offer the plugin (",[17,927,133],{},[17,929,930],{},"qgisMaximumVersion","), what to call it (",[17,933,117],{},"), what icon to show (",[17,936,284],{},"), and which other plugins to require (",[17,939,292],{},"). The repository reads it at upload time to build the public listing — rendering ",[17,942,181],{},[17,944,276],{},[17,946,244],{},[17,948,214],{},[17,950,206],{},[17,952,198],{},", and enforcing that ",[17,955,153],{}," is strictly greater than the live release.",[14,958,959,960,962,963,965],{},"This split explains some otherwise-confusing behavior. A plugin can be perfectly installable from a manually downloaded ZIP yet rejected by the repository, because the repository enforces fields QGIS Desktop ignores (a duplicate ",[17,961,153],{},", a missing required key). Conversely a plugin can upload fine yet stay hidden in a user's manager because ",[17,964,133],{}," exceeds their build — a constraint the repository accepts but Desktop enforces. Knowing who reads what tells you where to look when something does not behave.",[38,967,969],{"id":968},"qgis-version-compatibility","QGIS Version Compatibility",[14,971,87,972,974,975,977],{},[17,973,19],{}," format itself is stable across the 3.x line, but the values you put in ",[17,976,133],{}," should track the Python and API generation you actually target.",[979,980,981,997],"table",{},[982,983,984],"thead",{},[985,986,987,991,994],"tr",{},[988,989,990],"th",{},"QGIS line",[988,992,993],{},"Bundled Python",[988,995,996],{},"Suggested keys",[998,999,1000,1014,1028],"tbody",{},[985,1001,1002,1006,1009],{},[1003,1004,1005],"td",{},"3.28 LTR",[1003,1007,1008],{},"3.9",[1003,1010,1011],{},[17,1012,1013],{},"qgisMinimumVersion=3.28",[985,1015,1016,1019,1022],{},[1003,1017,1018],{},"3.34 LTR",[1003,1020,1021],{},"3.12",[1003,1023,1024,1027],{},[17,1025,1026],{},"qgisMinimumVersion=3.34"," (baseline for this guide)",[985,1029,1030,1033,1035],{},[1003,1031,1032],{},"3.40 \u002F 3.44",[1003,1034,1021],{},[1003,1036,1037,226,1039],{},[17,1038,1026],{},[17,1040,1041],{},"qgisMaximumVersion=3.99",[14,1043,1044,1045,1047],{},"Setting ",[17,1046,1041],{}," is a deliberate trick: it keeps your plugin visible to users on QGIS releases that did not exist when you published, instead of vanishing the moment a new version ships.",[38,1049,1051],{"id":1050},"troubleshooting","Troubleshooting",[14,1053,1054,1060,1061,53],{},[57,1055,1056,1059],{},[17,1057,1058],{},"metadata.txt not found"," on upload."," The file is not directly inside a single top-level folder in the ZIP, or that folder name contains a dash. Re-zip so the structure is ",[17,1062,1063],{},"plugin_folder\u002Fmetadata.txt",[14,1065,1066,1071,1072,1074],{},[57,1067,1068,53],{},[17,1069,1070],{},"Version X already exists"," Your ",[17,1073,153],{}," is equal to or lower than the live release. Bump the PATCH (or higher) component so it is strictly greater.",[14,1076,1077,1080,1081,1084,1085,1088],{},[57,1078,1079],{},"Plugin loads in QGIS but the repository shows no icon."," The ",[17,1082,1083],{},"icon="," path is wrong or points at a resource you did not compile. Use a path relative to the plugin folder (",[17,1086,1087],{},"icons\u002Ficon.png",") and confirm the file is actually inside the ZIP.",[14,1090,1091,896,1094,1096,1097,1100],{},[57,1092,1093],{},"Manager hides the plugin after install.",[17,1095,133],{}," is higher than the running QGIS, or ",[17,1098,1099],{},"experimental=True"," while the user has not enabled experimental plugins. Check both against your test build.",[14,1102,1103,1106,1107,1109,1110,1112],{},[57,1104,1105],{},"Continuation line parsed as an unknown key."," An ",[17,1108,181],{}," or ",[17,1111,276],{}," paragraph's second line is not indented. Add leading whitespace to every continuation line.",[14,1114,1115,1118,1119,226,1121,1124,1125,53],{},[57,1116,1117],{},"Upload rejected for invalid version string."," You used ",[17,1120,385],{},[17,1122,1123],{},"1.0",", or a date. Use bare ",[17,1126,157],{},[38,1128,1130],{"id":1129},"conclusion","Conclusion",[14,1132,1133,1135,1136,1138,1139,1141],{},[17,1134,19],{}," is small but load-bearing: it is the contract between your plugin, QGIS Desktop, and the repository. Start from the annotated template above, fill in the seven required keys correctly, set a generous ",[17,1137,930],{},", and validate locally with ",[17,1140,606],{}," before every upload. With those rules internalized, the metadata step of publishing becomes a non-event rather than a source of rejected uploads.",[38,1143,1145],{"id":1144},"frequently-asked-questions","Frequently Asked Questions",[14,1147,1148,1154,1155,1157,1158,1161,1162,53],{},[57,1149,1150,1151,1153],{},"Does ",[17,1152,117],{}," have to match my plugin's folder name?","\nNo. The folder (package) name must be a valid Python identifier — lowercase, underscores, no spaces. ",[17,1156,117],{}," is a free-text display label and can differ entirely, for example folder ",[17,1159,1160],{},"parcel_validator"," with ",[17,1163,1164],{},"name=Parcel Validator",[14,1166,1167,1170,1171,1173,1174,1176],{},[57,1168,1169],{},"Can I put comments in metadata.txt?","\nLines beginning with ",[17,1172,73],{}," are tolerated, but keep them minimal in the shipped file. The annotated comments in this guide are illustrative; a production ",[17,1175,19],{}," is cleaner without them.",[14,1178,1179,1185],{},[57,1180,1181,1182,1184],{},"Is ",[17,1183,276],{}," required?","\nNo, but it is strongly recommended. It is rendered on your plugin page and shown to users when an update is available, so omitting it leaves them guessing what changed.",[14,1187,1188,1193,1194,1197],{},[57,1189,1190,1191,871],{},"What happens if I omit ",[17,1192,930],{},"\nQGIS assumes a default upper bound tied to your minimum, which can cause the manager to hide your plugin on much newer releases. Set it explicitly to ",[17,1195,1196],{},"3.99"," to avoid premature invisibility.",[38,1199,1201],{"id":1200},"related","Related",[43,1203,1204,1208,1212],{},[46,1205,1206],{},[22,1207,25],{"href":24},[46,1209,1210],{},[22,1211,52],{"href":51},[46,1213,1214],{},[22,1215,614],{"href":613},[1217,1218,1219],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sRjNt, html code.shiki .sRjNt{--shiki-default:#85E89D;--shiki-default-font-weight:bold}",{"title":82,"searchDepth":405,"depth":405,"links":1221},[1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232],{"id":40,"depth":405,"text":41},{"id":66,"depth":405,"text":67},{"id":104,"depth":405,"text":105},{"id":185,"depth":405,"text":186},{"id":296,"depth":405,"text":297},{"id":918,"depth":405,"text":919},{"id":968,"depth":405,"text":969},{"id":1050,"depth":405,"text":1051},{"id":1129,"depth":405,"text":1130},{"id":1144,"depth":405,"text":1145},{"id":1200,"depth":405,"text":1201},"A complete annotated metadata.txt for a QGIS plugin — every required and optional key, formatting rules, and the validation errors the repository rejects.","md",{},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fwrite-metadata-txt-qgis-plugin",{"title":5,"description":1233},"qgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fwrite-metadata-txt-qgis-plugin\u002Findex","gnNmH_ypfEEYt1sjpGprg3QjX3AZ6qeYtbm2VJ4szgo",1781781223072]