emdash 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-DoNJiveC.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-DoNJiveC.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-BzltprvY.mjs → apply-UsrFuO7l.mjs} +156 -254
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.mjs +10 -2
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +5 -5
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware/request-context.mjs +4 -4
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +35 -34
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +8 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-BRICGH2l.mjs → base64-MBPo9ozB.mjs} +1 -1
- package/dist/{base64-BRICGH2l.mjs.map → base64-MBPo9ozB.mjs.map} +1 -1
- package/dist/{byline-BSaNL1w7.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-BSaNL1w7.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/{bylines-CvJ3PYz2.mjs → bylines-esI7ioa9.mjs} +5 -5
- package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-esI7ioa9.mjs.map} +1 -1
- package/dist/{cache-C6N_hhN7.mjs → cache-fTzxgMFJ.mjs} +3 -3
- package/dist/{cache-C6N_hhN7.mjs.map → cache-fTzxgMFJ.mjs.map} +1 -1
- package/dist/{chunks-NBQVDOci.mjs → chunks-Da2-b-oA.mjs} +2 -2
- package/dist/{chunks-NBQVDOci.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +251 -79
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-BI0V3ICQ.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BI0V3ICQ.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-8lOYF0pr.mjs → content-C7G4QXkK.mjs} +14 -3
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-WRezodiz.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-WRezodiz.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-D8ksjWhO.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-D8ksjWhO.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{error-D_-tqP-I.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-D_-tqP-I.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-BFRaVcD6.d.mts → index-DjPMOfO0.d.mts} +82 -67
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +10 -10
- package/dist/index.mjs +28 -27
- package/dist/{load-DDqMMvZL.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-DDqMMvZL.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CKLbBnhK.mjs → loader-Bx2_9-5e.mjs} +31 -6
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DqWNC3lM.mjs → manifest-schema-CXAbd1vH.mjs} +1 -1
- package/dist/{manifest-schema-DqWNC3lM.mjs.map → manifest-schema-CXAbd1vH.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +3 -3
- package/dist/{media-BW32b4gi.mjs → media-D8FbNsl0.mjs} +2 -2
- package/dist/{media-BW32b4gi.mjs.map → media-D8FbNsl0.mjs.map} +1 -1
- package/dist/{mode-ier8jbBk.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-ier8jbBk.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/{options-BVp3UsTS.mjs → options-nPxWnrya.mjs} +1 -1
- package/dist/{options-BVp3UsTS.mjs.map → options-nPxWnrya.mjs.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-BE4o_2dc.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-BE4o_2dc.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{public-url-DByxYjUw.mjs → public-url-B1AxbbbQ.mjs} +1 -1
- package/dist/{public-url-DByxYjUw.mjs.map → public-url-B1AxbbbQ.mjs.map} +1 -1
- package/dist/{query-Cg9ZKRQ0.mjs → query-Bo-msrmu.mjs} +13 -13
- package/dist/{query-Cg9ZKRQ0.mjs.map → query-Bo-msrmu.mjs.map} +1 -1
- package/dist/{redirect-BhUBKRc1.mjs → redirect-C5H7VGIX.mjs} +3 -3
- package/dist/{redirect-BhUBKRc1.mjs.map → redirect-C5H7VGIX.mjs.map} +1 -1
- package/dist/{registry-Dw70ChxB.mjs → registry-Beb7wxFc.mjs} +5 -5
- package/dist/{registry-Dw70ChxB.mjs.map → registry-Beb7wxFc.mjs.map} +1 -1
- package/dist/{request-cache-B-bmkipQ.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-B-bmkipQ.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/{runner-Bnoj7vjK.d.mts → runner-Clwe4Mme.d.mts} +2 -2
- package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Clwe4Mme.d.mts.map} +1 -1
- package/dist/{runner-C7ADox5q.mjs → runner-DMnlIkh4.mjs} +433 -138
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +3 -3
- package/dist/{search-dOGEccMa.mjs → search-DkN-BqsS.mjs} +164 -92
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/{secrets-CW3reAnU.mjs → secrets-CZ8rxLX3.mjs} +3 -3
- package/dist/{secrets-CW3reAnU.mjs.map → secrets-CZ8rxLX3.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +15 -14
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-CTtewrSQ.mjs +407 -0
- package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
- package/dist/taxonomy-DSxx2K2L.mjs +218 -0
- package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
- package/dist/{tokens-D7zMmWi2.mjs → tokens-CyRDPVW2.mjs} +2 -2
- package/dist/{tokens-D7zMmWi2.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-DNEfeMaU.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-DNEfeMaU.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-BeMCmin1.mjs → transport-xpzIjCIB.mjs} +1 -1
- package/dist/{transport-BeMCmin1.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-CRxNbK-Z.mjs → types-BIgulNsW.mjs} +2 -2
- package/dist/{types-CRxNbK-Z.mjs.map → types-BIgulNsW.mjs.map} +1 -1
- package/dist/{types-CJsYGpco.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-CJsYGpco.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-M78DQ1lx.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-M78DQ1lx.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/{types-4fVtCIm0.mjs → types-CoO6mpV3.mjs} +1 -1
- package/dist/{types-4fVtCIm0.mjs.map → types-CoO6mpV3.mjs.map} +1 -1
- package/dist/{types-BuBIptGk.d.mts → types-D19uBYWn.d.mts} +149 -4
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BSyXeCFW.d.mts → types-Dl1fgFjn.d.mts} +1 -1
- package/dist/{types-BSyXeCFW.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CrtWgIvl.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-CIOg5AR8.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-CIOg5AR8.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CDbKp7ND.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CDbKp7ND.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-Baqf0slj.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-BfQh_C_y.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-BfEI7tNe.mjs → validation-B1NYiEos.mjs} +5 -5
- package/dist/{validation-BfEI7tNe.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-DoxrVdYf.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-BNJDQBSZ.mjs} +8 -3
- package/dist/zod-generator-BNJDQBSZ.mjs.map +1 -0
- package/package.json +6 -6
- package/src/api/handlers/content.ts +11 -0
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/taxonomies.ts +273 -97
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- package/src/astro/routes/api/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -6
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +11 -0
- package/src/database/repositories/taxonomy.ts +193 -89
- package/src/database/types.ts +10 -2
- package/src/i18n/resolve.ts +37 -0
- package/src/loader.ts +49 -2
- package/src/mcp/server.ts +77 -18
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/schema/zod-generator.ts +12 -2
- package/src/seed/apply.ts +140 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/taxonomies/index.ts +230 -213
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-BzltprvY.mjs.map +0 -1
- package/dist/content-8lOYF0pr.mjs.map +0 -1
- package/dist/index-BFRaVcD6.d.mts.map +0 -1
- package/dist/loader-CKLbBnhK.mjs.map +0 -1
- package/dist/runner-C7ADox5q.mjs.map +0 -1
- package/dist/search-dOGEccMa.mjs.map +0 -1
- package/dist/taxonomies-ZlRtD6AG.mjs +0 -315
- package/dist/taxonomies-ZlRtD6AG.mjs.map +0 -1
- package/dist/types-BuBIptGk.d.mts.map +0 -1
- package/dist/types-CrtWgIvl.d.mts.map +0 -1
- package/dist/validate-Baqf0slj.mjs.map +0 -1
- package/dist/validate-BfQh_C_y.d.mts.map +0 -1
- package/dist/version-DoxrVdYf.mjs +0 -7
- package/dist/zod-generator-CC0xNe_K.mjs.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -1667,16 +1667,19 @@ export function createMcpServer(): McpServer {
|
|
|
1667
1667
|
description:
|
|
1668
1668
|
"List all taxonomy definitions (e.g. categories, tags). Taxonomies are " +
|
|
1669
1669
|
"classification systems applied to content. Each has a name, label, and " +
|
|
1670
|
-
"can be hierarchical (categories) or flat (tags)."
|
|
1671
|
-
|
|
1670
|
+
"can be hierarchical (categories) or flat (tags). Optionally filter by " +
|
|
1671
|
+
"locale.",
|
|
1672
|
+
inputSchema: z.object({
|
|
1673
|
+
locale: z.string().optional().describe("Filter by locale (omit for all)"),
|
|
1674
|
+
}),
|
|
1672
1675
|
annotations: { readOnlyHint: true },
|
|
1673
1676
|
},
|
|
1674
|
-
async (
|
|
1677
|
+
async (args, extra) => {
|
|
1675
1678
|
requireScope(extra, "content:read");
|
|
1676
1679
|
const ec = getEmDash(extra);
|
|
1677
1680
|
try {
|
|
1678
1681
|
const { handleTaxonomyList } = await import("../api/handlers/taxonomies.js");
|
|
1679
|
-
return unwrap(await handleTaxonomyList(ec.db));
|
|
1682
|
+
return unwrap(await handleTaxonomyList(ec.db, { locale: args.locale }));
|
|
1680
1683
|
} catch (error) {
|
|
1681
1684
|
return respondHandlerError(error, "TAXONOMY_LIST_ERROR");
|
|
1682
1685
|
}
|
|
@@ -1695,6 +1698,7 @@ export function createMcpServer(): McpServer {
|
|
|
1695
1698
|
taxonomy: z.string().describe("Taxonomy name (e.g. 'categories', 'tags')"),
|
|
1696
1699
|
limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
|
|
1697
1700
|
cursor: z.string().min(1).max(2048).optional().describe("Pagination cursor"),
|
|
1701
|
+
locale: z.string().optional().describe("Filter by locale (omit for all)"),
|
|
1698
1702
|
}),
|
|
1699
1703
|
annotations: { readOnlyHint: true },
|
|
1700
1704
|
},
|
|
@@ -1702,9 +1706,8 @@ export function createMcpServer(): McpServer {
|
|
|
1702
1706
|
requireScope(extra, "content:read");
|
|
1703
1707
|
const ec = getEmDash(extra);
|
|
1704
1708
|
try {
|
|
1705
|
-
// Verify taxonomy exists via handler layer
|
|
1706
1709
|
const { handleTaxonomyList } = await import("../api/handlers/taxonomies.js");
|
|
1707
|
-
const listResult = await handleTaxonomyList(ec.db);
|
|
1710
|
+
const listResult = await handleTaxonomyList(ec.db, { locale: args.locale });
|
|
1708
1711
|
if (!listResult.success) return unwrap(listResult);
|
|
1709
1712
|
|
|
1710
1713
|
const taxonomies = (listResult.data as { taxonomies: Array<{ name: string; id?: string }> })
|
|
@@ -1712,13 +1715,12 @@ export function createMcpServer(): McpServer {
|
|
|
1712
1715
|
const taxonomy = taxonomies.find((t: { name: string }) => t.name === args.taxonomy);
|
|
1713
1716
|
if (!taxonomy) return respondError("NOT_FOUND", `Taxonomy '${args.taxonomy}' not found`);
|
|
1714
1717
|
|
|
1715
|
-
// Paginated term query via repository (avoids N+1 of handleTermList)
|
|
1716
1718
|
const { TaxonomyRepository } = await import("../database/repositories/taxonomy.js");
|
|
1717
1719
|
const { decodeCursor, encodeCursor, InvalidCursorError } =
|
|
1718
1720
|
await import("../database/repositories/types.js");
|
|
1719
1721
|
const repo = new TaxonomyRepository(ec.db);
|
|
1720
1722
|
const limit = Math.min(args.limit ?? 50, 100);
|
|
1721
|
-
const terms = await repo.findByName(args.taxonomy);
|
|
1723
|
+
const terms = await repo.findByName(args.taxonomy, { locale: args.locale });
|
|
1722
1724
|
|
|
1723
1725
|
// Manual keyset pagination over the sorted-by-label results.
|
|
1724
1726
|
// Using a base64-encoded `(label, id)` cursor matches the
|
|
@@ -1760,6 +1762,8 @@ export function createMcpServer(): McpServer {
|
|
|
1760
1762
|
label: t.label,
|
|
1761
1763
|
parentId: t.parentId,
|
|
1762
1764
|
description: typeof t.data?.description === "string" ? t.data.description : undefined,
|
|
1765
|
+
locale: t.locale,
|
|
1766
|
+
translationGroup: t.translationGroup,
|
|
1763
1767
|
})),
|
|
1764
1768
|
nextCursor,
|
|
1765
1769
|
});
|
|
@@ -1785,6 +1789,11 @@ export function createMcpServer(): McpServer {
|
|
|
1785
1789
|
label: z.string().describe("Display name"),
|
|
1786
1790
|
parentId: z.string().optional().describe("Parent term ID for hierarchical taxonomies"),
|
|
1787
1791
|
description: z.string().optional().describe("Description of the term"),
|
|
1792
|
+
locale: z.string().optional().describe("Locale for the new term (e.g. 'es')"),
|
|
1793
|
+
translationOf: z
|
|
1794
|
+
.string()
|
|
1795
|
+
.optional()
|
|
1796
|
+
.describe("Term id to join as a translation (same translation_group)"),
|
|
1788
1797
|
}),
|
|
1789
1798
|
},
|
|
1790
1799
|
async (args, extra) => {
|
|
@@ -1799,6 +1808,8 @@ export function createMcpServer(): McpServer {
|
|
|
1799
1808
|
label: args.label,
|
|
1800
1809
|
parentId: args.parentId,
|
|
1801
1810
|
description: args.description,
|
|
1811
|
+
locale: args.locale,
|
|
1812
|
+
translationOf: args.translationOf,
|
|
1802
1813
|
}),
|
|
1803
1814
|
);
|
|
1804
1815
|
} catch (error) {
|
|
@@ -1875,6 +1886,29 @@ export function createMcpServer(): McpServer {
|
|
|
1875
1886
|
},
|
|
1876
1887
|
);
|
|
1877
1888
|
|
|
1889
|
+
server.registerTool(
|
|
1890
|
+
"taxonomy_term_translations",
|
|
1891
|
+
{
|
|
1892
|
+
title: "List Term Translations",
|
|
1893
|
+
description:
|
|
1894
|
+
"Return every locale variant of a taxonomy term, identified via its shared translation_group.",
|
|
1895
|
+
inputSchema: z.object({
|
|
1896
|
+
id: z.string().describe("Term id (or translation_group)"),
|
|
1897
|
+
}),
|
|
1898
|
+
annotations: { readOnlyHint: true },
|
|
1899
|
+
},
|
|
1900
|
+
async (args, extra) => {
|
|
1901
|
+
requireScope(extra, "content:read");
|
|
1902
|
+
const ec = getEmDash(extra);
|
|
1903
|
+
try {
|
|
1904
|
+
const { handleTermTranslations } = await import("../api/handlers/taxonomies.js");
|
|
1905
|
+
return unwrap(await handleTermTranslations(ec.db, args.id));
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
return respondHandlerError(error, "TERM_TRANSLATIONS_ERROR");
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
);
|
|
1911
|
+
|
|
1878
1912
|
// =====================================================================
|
|
1879
1913
|
// Menu tools
|
|
1880
1914
|
// =====================================================================
|
|
@@ -1884,18 +1918,20 @@ export function createMcpServer(): McpServer {
|
|
|
1884
1918
|
{
|
|
1885
1919
|
title: "List Menus",
|
|
1886
1920
|
description:
|
|
1887
|
-
"List
|
|
1888
|
-
"
|
|
1889
|
-
"
|
|
1890
|
-
inputSchema: z.object({
|
|
1921
|
+
"List navigation menus. Menus are per-locale: filter by `locale` to " +
|
|
1922
|
+
"get just one locale's worth, or omit to list every row (one per " +
|
|
1923
|
+
"locale per menu name).",
|
|
1924
|
+
inputSchema: z.object({
|
|
1925
|
+
locale: z.string().optional().describe("Filter by locale (omit for all)"),
|
|
1926
|
+
}),
|
|
1891
1927
|
annotations: { readOnlyHint: true },
|
|
1892
1928
|
},
|
|
1893
|
-
async (
|
|
1929
|
+
async (args, extra) => {
|
|
1894
1930
|
requireScope(extra, "content:read");
|
|
1895
1931
|
const ec = getEmDash(extra);
|
|
1896
1932
|
try {
|
|
1897
1933
|
const { handleMenuList } = await import("../api/handlers/menus.js");
|
|
1898
|
-
return unwrap(await handleMenuList(ec.db));
|
|
1934
|
+
return unwrap(await handleMenuList(ec.db, { locale: args.locale }));
|
|
1899
1935
|
} catch (error) {
|
|
1900
1936
|
return respondHandlerError(error, "MENU_LIST_ERROR");
|
|
1901
1937
|
}
|
|
@@ -1907,11 +1943,11 @@ export function createMcpServer(): McpServer {
|
|
|
1907
1943
|
{
|
|
1908
1944
|
title: "Get Menu with Items",
|
|
1909
1945
|
description:
|
|
1910
|
-
"Get a menu by name including
|
|
1911
|
-
"
|
|
1912
|
-
"for nesting.",
|
|
1946
|
+
"Get a menu by name, including its items. When multiple locales exist, " +
|
|
1947
|
+
"pass `locale` to pick the right one.",
|
|
1913
1948
|
inputSchema: z.object({
|
|
1914
1949
|
name: z.string().describe("Menu name (e.g. 'main', 'footer')"),
|
|
1950
|
+
locale: z.string().optional().describe("Locale to resolve the menu for"),
|
|
1915
1951
|
}),
|
|
1916
1952
|
annotations: { readOnlyHint: true },
|
|
1917
1953
|
},
|
|
@@ -1920,13 +1956,36 @@ export function createMcpServer(): McpServer {
|
|
|
1920
1956
|
const ec = getEmDash(extra);
|
|
1921
1957
|
try {
|
|
1922
1958
|
const { handleMenuGet } = await import("../api/handlers/menus.js");
|
|
1923
|
-
return unwrap(await handleMenuGet(ec.db, args.name));
|
|
1959
|
+
return unwrap(await handleMenuGet(ec.db, args.name, { locale: args.locale }));
|
|
1924
1960
|
} catch (error) {
|
|
1925
1961
|
return respondHandlerError(error, "MENU_GET_ERROR");
|
|
1926
1962
|
}
|
|
1927
1963
|
},
|
|
1928
1964
|
);
|
|
1929
1965
|
|
|
1966
|
+
server.registerTool(
|
|
1967
|
+
"menu_translations",
|
|
1968
|
+
{
|
|
1969
|
+
title: "List Menu Translations",
|
|
1970
|
+
description:
|
|
1971
|
+
"Return every locale variant of a menu, identified via the shared translation_group.",
|
|
1972
|
+
inputSchema: z.object({
|
|
1973
|
+
id: z.string().describe("Menu id (or translation_group)"),
|
|
1974
|
+
}),
|
|
1975
|
+
annotations: { readOnlyHint: true },
|
|
1976
|
+
},
|
|
1977
|
+
async (args, extra) => {
|
|
1978
|
+
requireScope(extra, "content:read");
|
|
1979
|
+
const ec = getEmDash(extra);
|
|
1980
|
+
try {
|
|
1981
|
+
const { handleMenuTranslations } = await import("../api/handlers/menus.js");
|
|
1982
|
+
return unwrap(await handleMenuTranslations(ec.db, args.id));
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
return respondHandlerError(error, "MENU_TRANSLATIONS_ERROR");
|
|
1985
|
+
}
|
|
1986
|
+
},
|
|
1987
|
+
);
|
|
1988
|
+
|
|
1930
1989
|
server.registerTool(
|
|
1931
1990
|
"menu_create",
|
|
1932
1991
|
{
|
package/src/menus/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Navigation menu runtime functions
|
|
2
|
+
* Navigation menu runtime functions.
|
|
3
3
|
*
|
|
4
|
-
* These are called from templates to query menus and resolve URLs.
|
|
4
|
+
* These are called from templates to query menus and resolve URLs. All queries
|
|
5
|
+
* are locale-aware: when a locale is configured (or passed explicitly) items
|
|
6
|
+
* are filtered to that locale, and menu item references resolve against the
|
|
7
|
+
* referenced content's translation_group so the URL points at the right
|
|
8
|
+
* per-locale row.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import type { Kysely } from "kysely";
|
|
@@ -9,50 +13,61 @@ import { sql } from "kysely";
|
|
|
9
13
|
|
|
10
14
|
import type { Database } from "../database/types.js";
|
|
11
15
|
import { validateIdentifier } from "../database/validate.js";
|
|
16
|
+
import { resolveLocale, resolveLocaleChain } from "../i18n/resolve.js";
|
|
12
17
|
import { getDb } from "../loader.js";
|
|
13
18
|
import { requestCached } from "../request-cache.js";
|
|
14
19
|
import { sanitizeHref } from "../utils/url.js";
|
|
15
20
|
import type { Menu, MenuItem, MenuItemRow } from "./types.js";
|
|
16
21
|
|
|
22
|
+
export interface MenuQueryOptions {
|
|
23
|
+
/** Override the locale used for the lookup. When omitted, the locale comes
|
|
24
|
+
* from the request context or the configured defaultLocale. */
|
|
25
|
+
locale?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
|
-
* Get menu by name with resolved URLs
|
|
29
|
+
* Get a menu by name with resolved URLs.
|
|
19
30
|
*
|
|
20
31
|
* @example
|
|
21
32
|
* ```ts
|
|
22
|
-
* import { getMenu } from "emdash";
|
|
23
|
-
*
|
|
24
33
|
* const menu = await getMenu("primary");
|
|
25
|
-
*
|
|
26
|
-
* console.log(menu.items); // Array of MenuItem with resolved URLs
|
|
27
|
-
* }
|
|
34
|
+
* const menuEs = await getMenu("primary", { locale: "es" });
|
|
28
35
|
* ```
|
|
29
36
|
*/
|
|
30
|
-
export function getMenu(name: string): Promise<Menu | null> {
|
|
31
|
-
|
|
37
|
+
export function getMenu(name: string, options: MenuQueryOptions = {}): Promise<Menu | null> {
|
|
38
|
+
const locale = resolveLocale(options.locale);
|
|
39
|
+
return requestCached(`menu:${name}:${locale ?? "*"}`, async () => {
|
|
32
40
|
const db = await getDb();
|
|
33
|
-
return getMenuWithDb(name, db);
|
|
41
|
+
return getMenuWithDb(name, db, { locale });
|
|
34
42
|
});
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
|
-
* Get menu by name with resolved URLs (with explicit db)
|
|
39
|
-
*
|
|
40
|
-
* @internal Use `getMenu()` in templates. This variant is for admin routes
|
|
41
|
-
* that already have a database handle.
|
|
46
|
+
* Get menu by name with resolved URLs (with explicit db). Internal helper for
|
|
47
|
+
* admin routes that already have a database handle.
|
|
42
48
|
*/
|
|
43
|
-
export async function getMenuWithDb(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
export async function getMenuWithDb(
|
|
50
|
+
name: string,
|
|
51
|
+
db: Kysely<Database>,
|
|
52
|
+
options: MenuQueryOptions = {},
|
|
53
|
+
): Promise<Menu | null> {
|
|
54
|
+
const chain = resolveLocaleChain(options.locale);
|
|
55
|
+
|
|
56
|
+
const selectMenu = () => db.selectFrom("_emdash_menus").selectAll().where("name", "=", name);
|
|
57
|
+
|
|
58
|
+
let menuRow: Awaited<ReturnType<ReturnType<typeof selectMenu>["executeTakeFirst"]>>;
|
|
59
|
+
if (chain.length === 0) {
|
|
60
|
+
menuRow = await selectMenu().orderBy("locale", "asc").executeTakeFirst();
|
|
61
|
+
} else {
|
|
62
|
+
menuRow = undefined;
|
|
63
|
+
for (const locale of chain) {
|
|
64
|
+
menuRow = await selectMenu().where("locale", "=", locale).executeTakeFirst();
|
|
65
|
+
if (menuRow) break;
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
|
|
55
|
-
|
|
69
|
+
if (!menuRow) return null;
|
|
70
|
+
|
|
56
71
|
const itemRows = await db
|
|
57
72
|
.selectFrom("_emdash_menu_items")
|
|
58
73
|
.selectAll()
|
|
@@ -61,31 +76,27 @@ export async function getMenuWithDb(name: string, db: Kysely<Database>): Promise
|
|
|
61
76
|
.orderBy("sort_order", "asc")
|
|
62
77
|
.execute();
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
const items = await buildMenuTree(itemRows, db);
|
|
79
|
+
const items = await buildMenuTree(itemRows, db, menuRow.locale);
|
|
66
80
|
|
|
67
81
|
return {
|
|
68
82
|
id: menuRow.id,
|
|
69
83
|
name: menuRow.name,
|
|
70
84
|
label: menuRow.label,
|
|
71
85
|
items,
|
|
86
|
+
locale: menuRow.locale,
|
|
87
|
+
translationGroup: menuRow.translation_group,
|
|
72
88
|
};
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
/**
|
|
76
|
-
* Get all menus (without items - for admin list
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```ts
|
|
80
|
-
* import { getMenus } from "emdash";
|
|
81
|
-
*
|
|
82
|
-
* const menus = await getMenus();
|
|
83
|
-
* console.log(menus); // [{ id, name, label }]
|
|
84
|
-
* ```
|
|
92
|
+
* Get all menus (without items, locale-filtered — for admin list / site nav
|
|
93
|
+
* summaries). When no locale is configured, returns menus across all locales.
|
|
85
94
|
*/
|
|
86
|
-
export async function getMenus(
|
|
95
|
+
export async function getMenus(
|
|
96
|
+
options: MenuQueryOptions = {},
|
|
97
|
+
): Promise<Array<{ id: string; name: string; label: string; locale: string }>> {
|
|
87
98
|
const db = await getDb();
|
|
88
|
-
return getMenusWithDb(db);
|
|
99
|
+
return getMenusWithDb(db, options);
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
/**
|
|
@@ -96,26 +107,30 @@ export async function getMenus(): Promise<Array<{ id: string; name: string; labe
|
|
|
96
107
|
*/
|
|
97
108
|
export async function getMenusWithDb(
|
|
98
109
|
db: Kysely<Database>,
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
options: MenuQueryOptions = {},
|
|
111
|
+
): Promise<Array<{ id: string; name: string; label: string; locale: string }>> {
|
|
112
|
+
const locale = resolveLocale(options.locale);
|
|
113
|
+
let query = db
|
|
101
114
|
.selectFrom("_emdash_menus")
|
|
102
|
-
.select(["id", "name", "label"])
|
|
103
|
-
.orderBy("name", "asc")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return rows;
|
|
115
|
+
.select(["id", "name", "label", "locale"])
|
|
116
|
+
.orderBy("name", "asc");
|
|
117
|
+
if (locale !== undefined) query = query.where("locale", "=", locale);
|
|
118
|
+
return query.execute();
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
/**
|
|
110
|
-
* Build hierarchical menu tree from flat
|
|
122
|
+
* Build a hierarchical menu tree from a flat list of items. Items are
|
|
123
|
+
* resolved against the given `locale` so references land on the right
|
|
124
|
+
* per-locale content rows.
|
|
111
125
|
*/
|
|
112
|
-
async function buildMenuTree(
|
|
113
|
-
|
|
126
|
+
async function buildMenuTree(
|
|
127
|
+
items: MenuItemRow[],
|
|
128
|
+
db: Kysely<Database>,
|
|
129
|
+
locale: string,
|
|
130
|
+
): Promise<MenuItem[]> {
|
|
114
131
|
const collectionSlugs = new Set<string>();
|
|
115
132
|
for (const item of items) {
|
|
116
|
-
if (item.reference_collection)
|
|
117
|
-
collectionSlugs.add(item.reference_collection);
|
|
118
|
-
}
|
|
133
|
+
if (item.reference_collection) collectionSlugs.add(item.reference_collection);
|
|
119
134
|
if (item.type === "page" || item.type === "post") {
|
|
120
135
|
collectionSlugs.add(item.reference_collection || `${item.type}s`);
|
|
121
136
|
}
|
|
@@ -128,41 +143,28 @@ async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promis
|
|
|
128
143
|
.select(["slug", "url_pattern"])
|
|
129
144
|
.where("slug", "in", [...collectionSlugs])
|
|
130
145
|
.execute();
|
|
131
|
-
for (const row of rows)
|
|
132
|
-
urlPatterns.set(row.slug, row.url_pattern);
|
|
133
|
-
}
|
|
146
|
+
for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
// Resolve all URLs first
|
|
137
149
|
const resolvedItems = await Promise.all(
|
|
138
|
-
items.map((item) => resolveMenuItem(item, db, urlPatterns)),
|
|
150
|
+
items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)),
|
|
139
151
|
);
|
|
152
|
+
const validItems = resolvedItems.filter((item): item is MenuItem => item !== null);
|
|
140
153
|
|
|
141
|
-
// Filter out items that couldn't be resolved (e.g., deleted content)
|
|
142
|
-
const validItems = resolvedItems.filter((item) => item !== null);
|
|
143
|
-
|
|
144
|
-
// Build tree structure
|
|
145
154
|
const itemMap = new Map<string, MenuItem & { children: MenuItem[] }>();
|
|
146
155
|
const rootItems: MenuItem[] = [];
|
|
147
156
|
|
|
148
|
-
// First pass: create all items
|
|
149
157
|
for (const item of validItems) {
|
|
150
158
|
itemMap.set(item.id, { ...item, children: [] });
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
// Second pass: build parent-child relationships
|
|
154
161
|
for (const item of items) {
|
|
155
162
|
const menuItem = itemMap.get(item.id);
|
|
156
163
|
if (!menuItem) continue;
|
|
157
|
-
|
|
158
164
|
if (item.parent_id) {
|
|
159
165
|
const parent = itemMap.get(item.parent_id);
|
|
160
|
-
if (parent)
|
|
161
|
-
|
|
162
|
-
} else {
|
|
163
|
-
// Parent not found, treat as root
|
|
164
|
-
rootItems.push(menuItem);
|
|
165
|
-
}
|
|
166
|
+
if (parent) parent.children.push(menuItem);
|
|
167
|
+
else rootItems.push(menuItem);
|
|
166
168
|
} else {
|
|
167
169
|
rootItems.push(menuItem);
|
|
168
170
|
}
|
|
@@ -172,14 +174,15 @@ async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promis
|
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
/**
|
|
175
|
-
* Resolve a single menu item's URL
|
|
176
|
-
*
|
|
177
|
-
*
|
|
177
|
+
* Resolve a single menu item's URL. `reference_id` is a translation_group
|
|
178
|
+
* (migration 036 remapped all existing references); we join it against
|
|
179
|
+
* the per-locale ec_* row or per-locale taxonomy row.
|
|
178
180
|
*/
|
|
179
181
|
async function resolveMenuItem(
|
|
180
182
|
item: MenuItemRow,
|
|
181
183
|
db: Kysely<Database>,
|
|
182
184
|
urlPatterns: Map<string, string | null>,
|
|
185
|
+
locale: string,
|
|
183
186
|
): Promise<MenuItem | null> {
|
|
184
187
|
let url: string | null;
|
|
185
188
|
|
|
@@ -192,24 +195,18 @@ async function resolveMenuItem(
|
|
|
192
195
|
case "page":
|
|
193
196
|
case "post":
|
|
194
197
|
url = await resolveContentUrl(
|
|
195
|
-
// Default to plural collection name (pages/posts) if not specified
|
|
196
198
|
item.reference_collection || `${item.type}s`,
|
|
197
199
|
item.reference_id,
|
|
198
200
|
db,
|
|
199
201
|
urlPatterns,
|
|
202
|
+
locale,
|
|
200
203
|
);
|
|
201
|
-
|
|
202
|
-
if (url === null) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
204
|
+
if (url === null) return null;
|
|
205
205
|
break;
|
|
206
206
|
|
|
207
207
|
case "taxonomy":
|
|
208
|
-
url = await resolveTaxonomyUrl(item.reference_id, db);
|
|
209
|
-
|
|
210
|
-
if (url === null) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
208
|
+
url = await resolveTaxonomyUrl(item.reference_id, db, locale);
|
|
209
|
+
if (url === null) return null;
|
|
213
210
|
break;
|
|
214
211
|
|
|
215
212
|
case "collection":
|
|
@@ -223,16 +220,14 @@ async function resolveMenuItem(
|
|
|
223
220
|
item.reference_id,
|
|
224
221
|
db,
|
|
225
222
|
urlPatterns,
|
|
223
|
+
locale,
|
|
226
224
|
);
|
|
227
|
-
if (url === null)
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
225
|
+
if (url === null) return null;
|
|
230
226
|
} else {
|
|
231
227
|
url = "#";
|
|
232
228
|
}
|
|
233
229
|
}
|
|
234
230
|
} catch (error) {
|
|
235
|
-
// If resolution fails, skip this item
|
|
236
231
|
console.error(`Failed to resolve menu item ${item.id}:`, error);
|
|
237
232
|
return null;
|
|
238
233
|
}
|
|
@@ -244,7 +239,7 @@ async function resolveMenuItem(
|
|
|
244
239
|
target: item.target || undefined,
|
|
245
240
|
titleAttr: item.title_attr || undefined,
|
|
246
241
|
cssClasses: item.css_classes || undefined,
|
|
247
|
-
children: [],
|
|
242
|
+
children: [],
|
|
248
243
|
};
|
|
249
244
|
}
|
|
250
245
|
|
|
@@ -261,72 +256,96 @@ function interpolateUrlPattern(pattern: string, slug: string, id: string): strin
|
|
|
261
256
|
}
|
|
262
257
|
|
|
263
258
|
/**
|
|
264
|
-
* Resolve URL for a content
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
259
|
+
* Resolve the URL for a content reference. `referenceGroup` is the content
|
|
260
|
+
* row's translation_group; we look up the row in the requested locale
|
|
261
|
+
* (falling back to the source if no translation exists so the menu link is
|
|
262
|
+
* still clickable).
|
|
268
263
|
*/
|
|
269
264
|
async function resolveContentUrl(
|
|
270
265
|
collection: string,
|
|
271
|
-
|
|
266
|
+
referenceGroup: string | null,
|
|
272
267
|
db: Kysely<Database>,
|
|
273
268
|
urlPatterns: Map<string, string | null>,
|
|
269
|
+
locale: string,
|
|
274
270
|
): Promise<string | null> {
|
|
275
|
-
if (!
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
271
|
+
if (!referenceGroup) return null;
|
|
278
272
|
|
|
279
273
|
try {
|
|
280
|
-
// Validate collection name before interpolating into table reference
|
|
281
274
|
validateIdentifier(collection, "menu item collection");
|
|
282
275
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
SELECT slug FROM ${sql.ref(`ec_${collection}`)}
|
|
276
|
+
// Try the requested locale first, then any locale (deterministic).
|
|
277
|
+
let result = await sql<{ id: string; slug: string }>`
|
|
278
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
279
|
+
WHERE translation_group = ${referenceGroup} AND locale = ${locale}
|
|
280
|
+
LIMIT 1
|
|
286
281
|
`.execute(db);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
282
|
+
let row = result.rows[0];
|
|
283
|
+
if (!row) {
|
|
284
|
+
result = await sql<{ id: string; slug: string }>`
|
|
285
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
286
|
+
WHERE translation_group = ${referenceGroup}
|
|
287
|
+
ORDER BY locale ASC LIMIT 1
|
|
288
|
+
`.execute(db);
|
|
289
|
+
row = result.rows[0];
|
|
295
290
|
}
|
|
291
|
+
if (!row) {
|
|
292
|
+
// Legacy rows whose reference_id still points at an id directly
|
|
293
|
+
// (defensive — migration 036 normalised these, but a row inserted
|
|
294
|
+
// between migrations could predate the remap).
|
|
295
|
+
const legacy = await sql<{ id: string; slug: string }>`
|
|
296
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
297
|
+
WHERE id = ${referenceGroup} LIMIT 1
|
|
298
|
+
`.execute(db);
|
|
299
|
+
row = legacy.rows[0];
|
|
300
|
+
}
|
|
301
|
+
if (!row) return null;
|
|
296
302
|
|
|
297
|
-
|
|
298
|
-
return
|
|
303
|
+
const pattern = urlPatterns.get(collection);
|
|
304
|
+
if (pattern) return interpolateUrlPattern(pattern, row.slug, row.id);
|
|
305
|
+
return `/${collection}/${row.slug}`;
|
|
299
306
|
} catch (error) {
|
|
300
|
-
|
|
301
|
-
console.error(`Failed to resolve content URL for ${collection}/${entryId}:`, error);
|
|
307
|
+
console.error(`Failed to resolve content URL for ${collection}/${referenceGroup}:`, error);
|
|
302
308
|
return null;
|
|
303
309
|
}
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
/**
|
|
307
|
-
* Resolve URL for a taxonomy term
|
|
308
|
-
*
|
|
309
|
-
* Returns null if taxonomy not found (item should be skipped)
|
|
313
|
+
* Resolve URL for a taxonomy term reference. `referenceGroup` is the term's
|
|
314
|
+
* translation_group; we pick the row in the active locale (or fall back).
|
|
310
315
|
*/
|
|
311
316
|
async function resolveTaxonomyUrl(
|
|
312
|
-
|
|
317
|
+
referenceGroup: string | null,
|
|
313
318
|
db: Kysely<Database>,
|
|
319
|
+
locale: string,
|
|
314
320
|
): Promise<string | null> {
|
|
315
|
-
if (!
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
321
|
+
if (!referenceGroup) return null;
|
|
318
322
|
|
|
319
|
-
|
|
323
|
+
let taxonomy = await db
|
|
320
324
|
.selectFrom("taxonomies")
|
|
321
325
|
.select(["name", "slug"])
|
|
322
|
-
.where("
|
|
326
|
+
.where("translation_group", "=", referenceGroup)
|
|
327
|
+
.where("locale", "=", locale)
|
|
323
328
|
.executeTakeFirst();
|
|
324
329
|
|
|
325
330
|
if (!taxonomy) {
|
|
326
|
-
|
|
327
|
-
|
|
331
|
+
taxonomy = await db
|
|
332
|
+
.selectFrom("taxonomies")
|
|
333
|
+
.select(["name", "slug"])
|
|
334
|
+
.where("translation_group", "=", referenceGroup)
|
|
335
|
+
.orderBy("locale", "asc")
|
|
336
|
+
.executeTakeFirst();
|
|
328
337
|
}
|
|
329
338
|
|
|
330
|
-
|
|
339
|
+
if (!taxonomy) {
|
|
340
|
+
// Legacy: id-based reference that predates the migration remap.
|
|
341
|
+
taxonomy = await db
|
|
342
|
+
.selectFrom("taxonomies")
|
|
343
|
+
.select(["name", "slug"])
|
|
344
|
+
.where("id", "=", referenceGroup)
|
|
345
|
+
.executeTakeFirst();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!taxonomy) return null;
|
|
349
|
+
|
|
331
350
|
return `/${taxonomy.name}/${taxonomy.slug}`;
|
|
332
351
|
}
|