fix: build mealie permalink as /g/<group>/r/<slug>, not /recipe/<slug>

Cobb caught it. Mealie's web URL format is:
  https://recipes.sulkta.com/g/hayes-house/r/dairy-free-bread-recipe

Was emitting /recipe/<slug> which 404s.

Added _user_group_slug() helper that pulls .group.slug from /api/users/self
(handles dict-or-string-or-fallback shapes for cross-version compat),
threads the proper URL through both:
- /api/recipes/<slug>.json (used by the modal)
- /recipes/<slug> server-rendered detail (used as fallback for direct links)

Falls back to the old /recipe/<slug> path if we can't resolve a group
slug (won't break, will just 404 in Mealie if that ever happens).
This commit is contained in:
Kayos 2026-04-28 21:33:10 -07:00
parent 3c4c0c027d
commit 8e53a84121
3 changed files with 40 additions and 6 deletions

View file

@ -482,8 +482,20 @@ def create_app() -> Flask:
recipe = client.get_recipe(slug)
except Exception as e:
return jsonify({"error": str(e)}), 502
recipe["picked"] = slug in db.list_household_pick_slugs(current_household_id() or 0) if current_household_id() else False
return jsonify({"recipe": recipe, "public_url": cfg.mealie_public_url})
hid = current_household_id()
recipe["picked"] = slug in db.list_household_pick_slugs(hid) if hid else False
# Mealie's web URL: <public>/g/<group-slug>/r/<recipe-slug>
group_slug = _user_group_slug(client)
mealie_url = (
f"{cfg.mealie_public_url}/g/{group_slug}/r/{slug}"
if group_slug
else f"{cfg.mealie_public_url}/recipe/{slug}"
)
return jsonify({
"recipe": recipe,
"public_url": cfg.mealie_public_url,
"mealie_url": mealie_url,
})
@app.get("/recipes/<slug>")
@require_session
@ -497,10 +509,16 @@ def create_app() -> Flask:
except Exception as e:
return (f"recipe load failed: {e}", 502)
picked = slug in db.list_meal_pick_slugs(u["sub"])
group_slug = _user_group_slug(client)
mealie_url = (
f"{cfg.mealie_public_url}/g/{group_slug}/r/{slug}"
if group_slug else f"{cfg.mealie_public_url}/recipe/{slug}"
)
return render_template(
"recipe_detail.html",
recipe=recipe,
public_url=cfg.mealie_public_url,
mealie_url=mealie_url,
picked=picked,
active="recipes",
)
@ -534,6 +552,22 @@ def create_app() -> Flask:
return app
def _user_group_slug(client) -> str | None:
"""Mealie's recipe permalink lives at /g/<group-slug>/r/<slug>. Pull
the group slug from /api/users/self. Cheap call (Mealie is on the
same docker bridge); could cache in session if it becomes hot."""
try:
me = client.who_am_i()
except Exception:
return None
g = me.get("group")
if isinstance(g, dict):
return g.get("slug") or g.get("name")
if isinstance(g, str) and g:
return g
return me.get("groupSlug") or me.get("group_slug") or me.get("groupName")
def _sort_to_order(sort: str) -> tuple[str, str]:
"""Map our sort keys to Mealie's orderBy + direction."""
return {

View file

@ -539,13 +539,13 @@ button { font-family: inherit; }
const r = await fetch('/api/recipes/' + encodeURIComponent(slug) + '.json');
if (!r.ok) throw new Error(r.status);
const data = await r.json();
renderRecipe(data.recipe, data.public_url);
renderRecipe(data.recipe, data.mealie_url);
} catch (e) {
bodyEl.innerHTML = '<p style="color: var(--crit);">load failed.</p>';
}
}
function renderRecipe(r, publicUrl) {
function renderRecipe(r, mealieUrl) {
titleEl.textContent = r.name || '(untitled)';
const meta = [];
if (r.totalTime) meta.push('<span class="m">' + escapeHtml(r.totalTime) + '</span>');
@ -584,7 +584,7 @@ button { font-family: inherit; }
data-slug="${escapeHtml(r.slug)}" data-name="${escapeHtml(r.name||'')}">
🍄 ${isPicked ? 'pinned · unpin' : 'pin to plan'}
</button>
<a class="btn" href="${publicUrl}/recipe/${encodeURIComponent(r.slug)}" target="_blank" rel="noopener">in mealie ↗</a>
<a class="btn" href="${mealieUrl}" target="_blank" rel="noopener">in mealie ↗</a>
`;
}

View file

@ -61,7 +61,7 @@
onclick="togglePick(this)">
🍄 {% if picked %}pinned · remove{% else %}pin for ai plan{% endif %}
</button>
<a class="btn" href="{{ public_url }}/recipe/{{ recipe.slug }}" target="_blank" rel="noopener">view in mealie ↗</a>
<a class="btn" href="{{ mealie_url }}" target="_blank" rel="noopener">view in mealie ↗</a>
</div>
<script>