{"id":3227,"date":"2026-06-15T22:38:08","date_gmt":"2026-06-15T20:38:08","guid":{"rendered":"https:\/\/recette.mtk-club.jp\/fr\/archives\/"},"modified":"2026-09-30T18:37:00","modified_gmt":"2026-09-30T16:37:00","slug":"archives","status":"publish","type":"page","link":"https:\/\/recette.mtk-club.jp\/en\/archives\/","title":{"rendered":"Archives"},"content":{"rendered":"<div id=\"mtk-archives-gallery\" data-api=\"https:\/\/recette.mtk-club.jp\/wp-json\" data-category-id=\"96\">\n    <div class=\"title\">\n        <div>\n            <h1 data-i18n=\"ui.archives.title\">ARCHIVES<\/h1>\n        <\/div>\n        <div class=\"mtk-counters\">\n            <div>\n                <p id=\"mtk-car-count\">XX<\/p>\n                <p data-i18n=\"ui.archives.cars\">CARS<\/p>\n            <\/div>\n            <div>\n                <p id=\"mtk-tuner-count\">XX<\/p>\n                <p data-i18n=\"ui.archives.tuners\">TUNERS<\/p>\n            <\/div>\n        <\/div>\n    <\/div>\n    <div class=\"mtk-filters\">\n        <p data-i18n=\"ui.archives.filters\">FILTERS<\/p>\n        <div class=\"mtk-filters-inner\">\n            <label><span data-i18n=\"ui.archives.tuner\">TUNER<\/span><select id=\"mtk-tuner\"><option value=\"\" data-i18n=\"ui.archives.all_tuners\">ALL<\/option><\/select><\/label>\n            <label><span data-i18n=\"ui.archives.maker\">MAKER<\/span><select id=\"mtk-maker\"><option value=\"\" data-i18n=\"ui.archives.all_makers\">ALL<\/option><\/select><\/label>\n            <label><span data-i18n=\"ui.archives.model\">MODEL<\/span><select id=\"mtk-model\"><option value=\"\" data-i18n=\"ui.archives.all_models\">ALL<\/option><\/select><\/label>\n        <\/div>\n        <button type=\"button\" id=\"mtk-reset\" data-i18n=\"ui.archives.reset\">Reset<\/button>\n    <\/div>\n    <div id=\"mtk-status\" class=\"mtk-status\" data-i18n=\"ui.archives.loading\">Loading archive\u2026<\/div>\n    <div id=\"mtk-gallery\" class=\"mtk-gallery\"><\/div>\n    <div id=\"mtk-detail\" class=\"mtk-detail\" hidden><\/div>\n<\/div>\n\n<script>\n    \/* ---------------------------------------------------------------------------\n       HOUSE RULE FOR THIS SCRIPT: no literal \"less-than\" or \"ampersand\" character\n       may appear anywhere below, not even inside a string, a regex or a comment.\n\n       WordPress runs wptexturize over post content. It is supposed to leave script\n       elements alone, but it locates them with a naive tag scanner: any \"less-than\"\n       inside a JS string (an HTML fragment being built, a comparison operator) looks\n       like the start of a tag to it, which desynchronises the scanner and leaves the\n       rest of the script exposed to texturizing. That is what turned the logical AND\n       operator into a pair of numeric entities.\n\n       The two rules that keep this from happening:\n         - write \"less-than\" as \\x3C and ampersand as \\x26 inside strings and regexes;\n         - never use the logical AND operator or a less-than comparison in code. Use\n           fallbacks (a || {}), optional chaining, nested ifs, or a flipped comparison\n           (b > a) instead.\n\n       The baked-in string table obeys it too: build_ui_strings_json() escapes both\n       characters. tests\/test_archives_page.py fails the build if either slips in.\n    --------------------------------------------------------------------------- *\/\n    (function () {\n        \"use strict\";\n\n        let root = document.getElementById(\"mtk-archives-gallery\");\n        if (!root) return;\n\n        \/* ---------------------------------- language -------------------------------- *\/\n\n        \/\/ This page builds itself in the browser, so TranslatePress's server-side pass\n        \/\/ never sees a word of it. Its own strings are baked in per language instead.\n        const STRINGS = {\"en_US\": {\"ui.archives.title\": \"ARCHIVES\", \"ui.archives.cars\": \"CARS\", \"ui.archives.tuners\": \"TUNERS\", \"ui.archives.filters\": \"FILTERS\", \"ui.archives.tuner\": \"TUNER\", \"ui.archives.maker\": \"MAKER\", \"ui.archives.model\": \"MODEL\", \"ui.archives.all_tuners\": \"ALL\", \"ui.archives.all_makers\": \"ALL\", \"ui.archives.all_models\": \"ALL\", \"ui.archives.reset\": \"Reset\", \"ui.archives.loading\": \"Loading archive\u2026\", \"ui.archives.no_image\": \"No image\", \"ui.archives.no_match\": \"No cars match these filters.\", \"ui.archives.error\": \"Could not load the archive:\", \"ui.archives.none_published\": \"No cars have been published yet.\"}, \"fr_FR\": {\"ui.archives.title\": \"ARCHIVES\", \"ui.archives.cars\": \"VOITURES\", \"ui.archives.tuners\": \"PR\u00c9PARATEURS\", \"ui.archives.filters\": \"FILTRES\", \"ui.archives.tuner\": \"PR\u00c9PARATEUR\", \"ui.archives.maker\": \"MARQUE\", \"ui.archives.model\": \"MOD\u00c8LE\", \"ui.archives.all_tuners\": \"TOUS\", \"ui.archives.all_makers\": \"TOUTES\", \"ui.archives.all_models\": \"TOUS\", \"ui.archives.reset\": \"R\u00e9initialiser\", \"ui.archives.loading\": \"Chargement de l'archive\u2026\", \"ui.archives.no_image\": \"Pas d'image\", \"ui.archives.no_match\": \"Aucune voiture ne correspond \u00e0 ces filtres.\", \"ui.archives.error\": \"Impossible de charger l'archive :\", \"ui.archives.none_published\": \"Aucune voiture n'a encore \u00e9t\u00e9 publi\u00e9e.\"}};\n\n        \/\/ TranslatePress sets the html lang attribute per language: \"fr-FR\" for fr_FR.\n        const LANG = (function () {\n            const raw = (document.documentElement.getAttribute(\"lang\") || \"\").replace(\/-\/g, \"_\");\n            if (STRINGS[raw]) return raw;\n            const base = raw.split(\"_\")[0].toLowerCase();\n            for (const key in STRINGS) {\n                if (key.split(\"_\")[0].toLowerCase() === base) return key;\n            }\n            return \"en_US\";\n        })();\n\n        function t(key) {\n            const table = STRINGS[LANG] || {};\n            if (table[key] !== undefined) return table[key];\n            const source = STRINGS[\"en_US\"] || {};\n            return source[key] !== undefined ? source[key] : key;\n        }\n\n        \/\/ TranslatePress decides a request's language from its URL, so a fetch to the\n        \/\/ unprefixed REST root resolves in the site's default language \u2014 French on this\n        \/\/ install. Prefixing the REST root keeps the permalinks and any post body in the\n        \/\/ language of the page. The prefix is read off this page's own URL rather than\n        \/\/ derived from the locale, because TranslatePress's slug is configurable.\n        const LANG_PREFIX = (function () {\n            const m = \/^\\\/([a-z]{2}(?:-[a-z]{2})?)(?:\\\/|$)\/i.exec(window.location.pathname);\n            return m ? m[1] : \"\";\n        })();\n\n        let API = (root.getAttribute(\"data-api\") || \"\").replace(\/\\\/+$\/, \"\");\n        if (LANG_PREFIX) {\n            API = API.replace(\/\\\/wp-json\\\/?$\/, \"\/\" + LANG_PREFIX + \"\/wp-json\");\n        }\n\n        \/\/ Every REST request on this host costs several seconds of server time however\n        \/\/ small it is, so a request saved is seconds saved. With the category id on the\n        \/\/ block (data-category-id), the posts request goes out first instead of waiting\n        \/\/ for a slug lookup. The id differs per environment: when it is missing, or finds\n        \/\/ no posts, the slug lookup still runs as a fallback.\n        const CATEGORY_SLUG = \"car_archive\";\n        const CATEGORY_ID = parseInt(root.getAttribute(\"data-category-id\") || \"\", 10) || 0;\n\n        \/\/ Every static string in the markup above, in one pass. The English stays in\n        \/\/ the HTML so the first paint is never blank if this script fails.\n        Array.prototype.forEach.call(root.querySelectorAll(\"[data-i18n]\"), function (el) {\n            el.textContent = t(el.getAttribute(\"data-i18n\"));\n        });\n\n        \/\/ Per language: a card's permalink differs between \/fr\/ and \/en\/, so a grid\n        \/\/ cached on one must never paint on the other. v3: the key gained the language.\n        \/\/ v4: tag names are decoded now, and a v3 cache still holds them encoded.\n        \/\/ v5: cards carry a srcset, and the cache moved to localStorage so a returning\n        \/\/ visitor gets the grid instantly instead of only within the same tab session.\n        const CACHE_KEY = \"mtk-archives-cards-v5-\" + (LANG_PREFIX || \"default\");\n        const EAGER_CARDS = 8;      \/\/ first row or two load immediately, the rest lazily\n        const PER_PAGE = 100;       \/\/ REST maximum\n        const PAGE_BATCH = 4;       \/\/ extra pages fetched at a time\n        const CONTENT_BATCH = 60;   \/\/ posts per request in the hydration pass\n        const MAX_SRCSET_WIDTH = 1536;\n        const DIMS = [\"maker\", \"tuner\", \"model\"];\n        const META_TAGS_TO_DISPLAY = [\"tuner\", \"model\", \"maker\"]; \/\/ Below each card title\n        const PRIMARY_META_TAG = \"tuner\"; \/\/ Will be in green\n\n        \/\/ Loading skeleton: placeholder cards shown until the first data arrives. Title\n        \/\/ bar widths are percentages (one or two lines, like real titles), chip widths\n        \/\/ are pixels. Both cycle, so the placeholders do not all look identical.\n        const SKELETON_CARDS = 8;\n        const SKELETON_TITLES = [[88, 40], [70], [82], [92, 36], [66], [78, 52], [74], [84]];\n        const SKELETON_CHIPS = [[108, 62, 54], [96, 74, 54], [84, 58, 50], [112, 70, 58]];\n\n        const els = {\n            carsCounter: document.getElementById(\"mtk-car-count\"),\n            tunerCounter: document.getElementById(\"mtk-tuner-count\"),\n            maker: document.getElementById(\"mtk-maker\"),\n            tuner: document.getElementById(\"mtk-tuner\"),\n            model: document.getElementById(\"mtk-model\"),\n            reset: document.getElementById(\"mtk-reset\"),\n            status: document.getElementById(\"mtk-status\"),\n            gallery: document.getElementById(\"mtk-gallery\"),\n            detail: document.getElementById(\"mtk-detail\")\n        };\n\n        const missing = Object.keys(els).filter((key) => !els[key]);\n        if (missing.length) {\n            \/\/ The block markup was edited; stop here rather than throwing halfway through.\n            console.warn(\"mtk-archives: missing element(s): \" + missing.join(\", \"));\n            return;\n        }\n\n        \/\/ Each car: {id, title, maker, tuner, model, thumb, srcset, html, link}\n        let cars = [];\n\n        \/* --------------------------------- helpers -------------------------------- *\/\n\n        \/\/ WordPress hands back titles as rendered HTML, so an apostrophe arrives as a\n        \/\/ numeric entity. Decode once here and every consumer can use textContent. A\n        \/\/ textarea parses its content as raw text, so nothing in the string can become\n        \/\/ an element on the way through.\n        const decoder = document.createElement(\"textarea\");\n        function decodeEntities(value) {\n            decoder.innerHTML = value || \"\";\n            return decoder.value;\n        }\n\n        function firstImage(htmlStr) {\n            \/\/ Pull the src of the first image in the post body. The parsed document is\n            \/\/ inert, so nothing is downloaded by looking.\n            if (!htmlStr) return \"\";\n            const doc = new DOMParser().parseFromString(htmlStr, \"text\/html\");\n            const img = doc.querySelector(\"img\");\n            return img ? img.getAttribute(\"src\") || \"\" : \"\";\n        }\n\n        function mediaImage(post) {\n            \/\/ The featured image as a resized src plus a srcset of its other sizes, so the\n            \/\/ browser downloads the smallest file that fills a card on this screen: the\n            \/\/ 300px size is about a sixth of the 768px one, for a card under 300px wide.\n            const none = { src: \"\", srcset: \"\" };\n            const media = post._embedded?.[\"wp:featuredmedia\"]?.[0];\n            if (!media) return none;\n            if (media.code) return none;    \/\/ media came back as an error object\n            const details = media.media_details || {};\n            const sizes = details.sizes || {};\n            const pick = sizes.medium_large || sizes.large || sizes.medium || sizes.full || {};\n            return {\n                src: pick.source_url || media.source_url || \"\",\n                srcset: buildSrcset(sizes, details.width \/ details.height)\n            };\n        }\n\n        function buildSrcset(sizes, ratio) {\n            \/\/ Same rule WordPress applies to its own srcset: only sizes with the original's\n            \/\/ proportions. That leaves out the square crops (thumbnail, woocommerce_*) and\n            \/\/ the TranslatePress flag size, which would otherwise be picked as candidates.\n            if (!(ratio > 0)) return \"\";\n            const byWidth = new Map();\n            Object.keys(sizes).forEach((key) => {\n                const size = sizes[key] || {};\n                if (!size.source_url) return;\n                if (!(size.width >= 200)) return;\n                if (size.width > MAX_SRCSET_WIDTH) return;\n                const drift = Math.abs(size.width \/ size.height - ratio) \/ ratio;\n                if (drift > 0.02) return;\n                byWidth.set(size.width, size.source_url);\n            });\n            if (2 > byWidth.size) return \"\";\n            return Array.from(byWidth.keys())\n                .sort((a, b) => a - b)\n                .map((width) => byWidth.get(width) + \" \" + width + \"w\")\n                .join(\", \");\n        }\n\n        function thumbSizes() {\n            \/\/ A thumbnail is exactly one grid column wide. Read the column off the live\n            \/\/ layout, so the theme's CSS (4, 3, 2 or 1 columns) stays the only place that\n            \/\/ decides it. \"none\" (the gallery is not a grid) parses to NaN.\n            const track = parseFloat(getComputedStyle(els.gallery).gridTemplateColumns);\n            return track > 0 ? Math.ceil(track) + \"px\" : \"100vw\";\n        }\n\n        function uniqueSorted(values) {\n            return Array.from(new Set(values.filter(Boolean)))\n                .sort((a, b) => a.localeCompare(b));\n        }\n\n        function setStatus(msg) {\n            els.status.classList.remove(\"mtk-visually-hidden\");\n            els.status.textContent = msg || \"\";\n            els.status.style.display = msg ? \"\" : \"none\";\n        }\n\n        \/* ---------------------------------- REST ---------------------------------- *\/\n\n        async function fetchPage(path, page) {\n            const sep = path.indexOf(\"?\") === -1 ? \"?\" : \"\\x26\";\n            const url = API + path + sep + \"per_page=\" + PER_PAGE + \"\\x26page=\" + page;\n            const res = await fetch(url, {\n                credentials: \"same-origin\",\n                headers: { Accept: \"application\/json\" }\n            });\n            if (!res.ok) {\n                \/\/ Running past the last page returns 400; treat it as a clean end of collection.\n                if (res.status === 400) {\n                    if (page > 1) return { items: [], totalPages: page - 1 };\n                }\n                throw new Error(\"Request failed (\" + res.status + \") on \" + path);\n            }\n            const totalPages = parseInt(res.headers.get(\"X-WP-TotalPages\") || \"1\", 10);\n            return { items: await res.json(), totalPages: totalPages || 1 };\n        }\n\n        async function fetchAll(path) {\n            \/\/ Page 1 reports how many pages exist. The rest are fetched a few at a time:\n            \/\/ concurrent enough to stay fast, not so many that a shared host sees a burst.\n            const first = await fetchPage(path, 1);\n            if (2 > first.totalPages) return first.items;\n            const pages = [];\n            for (let p = 2; first.totalPages >= p; p++) pages.push(p);\n            const items = first.items.slice();\n            for (let i = 0; pages.length > i; i += PAGE_BATCH) {\n                const batch = await Promise.all(\n                    pages.slice(i, i + PAGE_BATCH).map((p) => fetchPage(path, p))\n                );\n                batch.forEach((res) => { items.push.apply(items, res.items); });\n            }\n            return items;\n        }\n\n        \/\/ Term names arrive HTML-encoded just like titles do, so a tuner called \"I and D\"\n        \/\/ written with an ampersand would reach the filter and the card as its entity.\n        \/\/ Both sources of tag names decode here, before anything compares or shows them.\n        function indexTags(tags) {\n            const byId = {};\n            (tags || []).forEach((tag) => { byId[tag.id] = decodeEntities(tag.name); });\n            return byId;\n        }\n\n        function embeddedTagNames(post) {\n            \/\/ Returns null when the post carries no embedded terms at all, so the caller\n            \/\/ can tell \"this post has no tags\" apart from \"the embed did not come back\".\n            \/\/ The live request no longer embeds terms; preview data still may.\n            const groups = post._embedded?.[\"wp:term\"];\n            if (!groups) return null;\n            const names = [];\n            groups.forEach((group) => {\n                if (!Array.isArray(group)) return;\n                group.forEach((term) => {\n                    if (term?.taxonomy === \"post_tag\") names.push(decodeEntities(term.name));\n                });\n            });\n            return names;\n        }\n\n        function postsPath(categoryId) {\n            \/\/ Only the fields a card needs. Full post content is NOT requested here: it is\n            \/\/ by far the heaviest part of the payload and nothing in the grid displays it.\n            \/\/ \"link\" is the permalink (card href); the embed supplies the featured image\n            \/\/ sizes. _links and _embedded must be listed explicitly or the _fields filter\n            \/\/ strips them and _embed returns nothing. Terms are not embedded: that cost the\n            \/\/ server two extra lookups per post (about 5 seconds for 39 cars), while one\n            \/\/ parallel request for the tag list costs nothing on the critical path.\n            return \"\/wp\/v2\/posts?categories=\" + categoryId +\n                \"\\x26_embed=wp:featuredmedia\" +\n                \"\\x26_fields=id,title,tags,link,_links,_embedded\";\n        }\n\n        async function categoryIdFromSlug() {\n            const cats = await fetchAll(\"\/wp\/v2\/categories?slug=\" + CATEGORY_SLUG + \"\\x26_fields=id\");\n            return cats.length ? cats[0].id : 0;\n        }\n\n        async function loadPosts() {\n            if (CATEGORY_ID) {\n                const posts = await fetchAll(postsPath(CATEGORY_ID));\n                if (posts.length) return posts;\n                \/\/ Nothing under the configured id: either the category is empty or the id\n                \/\/ belongs to another environment. The slug decides which.\n            }\n            const id = await categoryIdFromSlug();\n            if (!id) return [];\n            if (id === CATEGORY_ID) return [];\n            if (CATEGORY_ID) {\n                console.warn(\"mtk-archives: data-category-id is \" + CATEGORY_ID +\n                    \" but the \" + CATEGORY_SLUG + \" category is \" + id + \" here\");\n            }\n            return fetchAll(postsPath(id));\n        }\n\n        async function loadData() {\n            \/\/ Offline preview pages inject window.MTK_PRELOAD so drafts can be rendered\n            \/\/ without hitting the live REST API (which only exposes published posts).\n            if (window.MTK_PRELOAD) {\n                return {\n                    posts: window.MTK_PRELOAD.posts || [],\n                    tagName: indexTags(window.MTK_PRELOAD.tags)\n                };\n            }\n            \/\/ Posts and tag names side by side: neither needs the other, so the tag list\n            \/\/ adds no waiting on top of the posts request.\n            const results = await Promise.all([\n                loadPosts(),\n                fetchAll(\"\/wp\/v2\/tags?_fields=id,name\")\n            ]);\n            return { posts: results[0], tagName: indexTags(results[1]) };\n        }\n\n        \/* ---------------------------------- model --------------------------------- *\/\n\n        function classify(tagNames) {\n            \/\/ Tags look like \"Maker: Toyota\" \/ \"Tuner: HKS\" \/ \"Model: Supra\".\n            const found = { maker: \"\", tuner: \"\", model: \"\" };\n            tagNames.forEach((name) => {\n                const m = \/^\\s*(maker|tuner|model)\\s*:\\s*(.+?)\\s*$\/i.exec(name || \"\");\n                if (m) found[m[1].toLowerCase()] = m[2];\n            });\n            return found;\n        }\n\n        function toCar(post, tagName) {\n            const names = embeddedTagNames(post)\n                || (post.tags || []).map((id) => tagName[id] || \"\");\n            const dims = classify(names);\n            const html = post.content?.rendered || \"\";\n            const image = mediaImage(post);\n            return {\n                id: post.id,\n                title: decodeEntities(post.title?.rendered) || (\"Car #\" + post.id),\n                maker: dims.maker,\n                tuner: dims.tuner,\n                model: dims.model,\n                thumb: image.src || firstImage(html),\n                srcset: image.src ? image.srcset : \"\",\n                html: html,\n                link: post.link || \"\"\n            };\n        }\n\n        function metaLine(car) {\n            return DIMS.map((d) => car[d]).filter(Boolean).join(\" \u00b7 \");\n        }\n\n        \/* --------------------------------- filters -------------------------------- *\/\n\n        function matches(car, ignoreDim) {\n            return DIMS.every((d) => {\n                if (d === ignoreDim) return true;\n                const selected = els[d].value;\n                if (!selected) return true;\n                return car[d] === selected;\n            });\n        }\n\n        function fillSelect(sel, values) {\n            const previous = sel.value;\n            const placeholder = sel.options[0];   \/\/ the translated \"ALL\"\n            sel.textContent = \"\";\n            sel.appendChild(placeholder);\n            values.forEach((value) => {\n                const opt = document.createElement(\"option\");\n                opt.value = value;\n                opt.textContent = value;\n                sel.appendChild(opt);\n            });\n            \/\/ Preserve the selection if it is still valid.\n            sel.value = values.indexOf(previous) === -1 ? \"\" : previous;\n        }\n\n        function refreshFilters() {\n            \/\/ Each dimension's options come from the cars matching the *other* filters.\n            DIMS.forEach((d) => {\n                fillSelect(els[d], uniqueSorted(\n                    cars.filter((car) => matches(car, d)).map((car) => car[d])\n                ));\n            });\n        }\n\n        \/* --------------------------------- skeleton -------------------------------- *\/\n\n        function skeletonPart(tag, className, parent) {\n            const part = document.createElement(tag);\n            part.className = className;\n            parent.appendChild(part);\n            return part;\n        }\n\n        function skeletonCard(i) {\n            \/\/ Same element and wrappers as a real card, so the theme's card rules size it.\n            \/\/ Hidden from assistive tech: the status line announces the loading instead.\n            const card = document.createElement(\"a\");\n            card.className = \"mtk-card mtk-skeleton\";\n            card.setAttribute(\"aria-hidden\", \"true\");\n\n            const container = skeletonPart(\"div\", \"mtk-card-container\", card);\n            skeletonPart(\"span\", \"mtk-skel-thumb\", container);\n\n            const info = skeletonPart(\"div\", \"mtk-card-info-wrapper\", container);\n            const title = skeletonPart(\"div\", \"mtk-skel-title\", info);\n            SKELETON_TITLES[i % SKELETON_TITLES.length].forEach((width) => {\n                skeletonPart(\"span\", \"mtk-skel-line\", title).style.width = width + \"%\";\n            });\n\n            const meta = skeletonPart(\"div\", \"mtk-card-meta-wrapper\", info);\n            SKELETON_CHIPS[i % SKELETON_CHIPS.length].forEach((width, j) => {\n                const chip = skeletonPart(\"span\", \"mtk-skel-chip\", meta);\n                if (j === 0) chip.classList.add(\"mtk-skel-chip-primary\");\n                chip.style.width = width + \"px\";\n            });\n\n            \/\/ One highlight sweeping the whole card; staggered so the grid ripples.\n            const sweep = skeletonPart(\"span\", \"mtk-skel-sweep\", card);\n            sweep.style.animationDelay = ((i % 4) * 0.12).toFixed(2) + \"s\";\n            return card;\n        }\n\n        function showSkeleton() {\n            const frag = document.createDocumentFragment();\n            for (let i = 0; SKELETON_CARDS > i; i++) frag.appendChild(skeletonCard(i));\n            els.gallery.textContent = \"\";\n            els.gallery.appendChild(frag);\n            els.gallery.setAttribute(\"aria-busy\", \"true\");\n            els.carsCounter.classList.add(\"mtk-skel-text\");\n            els.tunerCounter.classList.add(\"mtk-skel-text\");\n            \/\/ The loading line stays for screen readers; sighted users see the skeleton.\n            els.status.textContent = t(\"ui.archives.loading\");\n            els.status.style.display = \"\";\n            els.status.classList.add(\"mtk-visually-hidden\");\n        }\n\n        function clearSkeleton() {\n            \/\/ For the paths that end without a grid (error, nothing published). A render\n            \/\/ replaces the placeholders on its own.\n            if (els.gallery.getAttribute(\"aria-busy\") === \"true\") els.gallery.textContent = \"\";\n            els.gallery.removeAttribute(\"aria-busy\");\n            els.carsCounter.classList.remove(\"mtk-skel-text\");\n            els.tunerCounter.classList.remove(\"mtk-skel-text\");\n        }\n\n        \/* --------------------------------- rendering ------------------------------- *\/\n\n        function buildCard(car, eager, sizes) {\n            \/\/ With a permalink the whole card is a link that opens the post in a new tab.\n            \/\/ Without one (offline preview via MTK_PRELOAD, drafts) it stays a card that\n            \/\/ opens the inline detail view.\n            const card = document.createElement(\"a\");\n            if (car.link) {\n                card.href = car.link;\n                card.target = \"_blank\";\n                card.rel = \"noopener noreferrer\";\n            }\n            card.className = \"mtk-card\";\n            card.dataset.id = String(car.id);\n\n            const container = document.createElement(\"div\");\n            container.className = \"mtk-card-container\";\n            card.appendChild(container);\n\n            if (car.thumb) {\n                const img = document.createElement(\"img\");\n                img.className = \"mtk-thumb\";\n                img.alt = car.title;\n                img.decoding = \"async\";\n                \/\/ Loading hints and candidates go on before src: an image settles on its\n                \/\/ priority and its source as soon as src is set.\n                img.loading = eager ? \"eager\" : \"lazy\";\n                img.setAttribute(\"fetchpriority\", eager ? \"high\" : \"low\");\n                if (car.srcset) {\n                    img.sizes = sizes;\n                    img.srcset = car.srcset;\n                }\n                img.src = car.thumb;\n                container.appendChild(img);\n            } else {\n                const empty = document.createElement(\"div\");\n                empty.className = \"mtk-thumb-empty\";\n                empty.textContent = t(\"ui.archives.no_image\");\n                container.appendChild(empty);\n            }\n\n            const bottomDiv = document.createElement(\"div\");\n            bottomDiv.className = \"mtk-card-info-wrapper\";\n            container.appendChild(bottomDiv);\n\n            const title = document.createElement(\"p\");\n            title.className = \"mtk-card-title\";\n            title.textContent = car.title;\n            bottomDiv.appendChild(title);\n\n            const metaContainer = document.createElement(\"div\");\n            metaContainer.className = \"mtk-card-meta-wrapper\";\n            bottomDiv.appendChild(metaContainer);\n\n            META_TAGS_TO_DISPLAY.forEach((tag) => {\n                const value = car[tag];\n                if (!value) return;\n                const metaTag = document.createElement(\"p\");\n                metaTag.textContent = value;\n                if (tag === PRIMARY_META_TAG) {\n                    metaTag.className = \"mtk-is-primary-tag\";\n                }\n                metaContainer.appendChild(metaTag);\n            });\n\n            return card;\n        }\n\n        function renderGallery() {\n            const list = cars.filter((car) => matches(car))\n                .sort((a, b) => a.title.localeCompare(b.title));\n\n            \/\/ Build off-document and swap in once: one layout pass instead of one per card.\n            const sizes = thumbSizes();\n            const frag = document.createDocumentFragment();\n            list.forEach((car, i) => frag.appendChild(buildCard(car, EAGER_CARDS > i, sizes)));\n            els.gallery.textContent = \"\";\n            els.gallery.appendChild(frag);\n            els.gallery.removeAttribute(\"aria-busy\");\n\n            if (!list.length) {\n                setStatus(t(\"ui.archives.no_match\"));\n                return;\n            }\n            setStatus(null);\n        }\n\n        function showCar(id) {\n            const car = id ? cars.find((c) => String(c.id) === String(id)) : null;\n            if (!car) {\n                els.detail.hidden = true;\n                els.detail.textContent = \"\";\n                return;\n            }\n            els.detail.textContent = \"\";\n\n            const heading = document.createElement(\"h2\");\n            heading.className = \"mtk-title\";\n            heading.textContent = car.title;\n            els.detail.appendChild(heading);\n\n            const meta = metaLine(car);\n            if (meta) {\n                const metaEl = document.createElement(\"div\");\n                metaEl.className = \"mtk-meta\";\n                metaEl.textContent = meta;\n                els.detail.appendChild(metaEl);\n            }\n            \/\/ Post content is already rendered and sanitised by WordPress.\n            els.detail.insertAdjacentHTML(\"beforeend\", car.html);\n\n            els.detail.hidden = false;\n            els.detail.focus({ preventScroll: true });   \/\/ keyboard users land on the article\n            els.detail.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n        }\n\n        \/* ----------------------------------- wiring -------------------------------- *\/\n\n        function onFilterChange() {\n            showCar(\"\");\n            refreshFilters();\n            renderGallery();\n        }\n\n        els.status.setAttribute(\"role\", \"status\");   \/\/ announce the match count\n        els.detail.setAttribute(\"tabindex\", \"-1\");\n\n        DIMS.forEach((d) => els[d].addEventListener(\"change\", onFilterChange));\n\n        els.gallery.addEventListener(\"click\", (ev) => {\n            const card = ev.target.closest?.(\".mtk-card\");\n            if (!card) return;\n            \/\/ Link cards navigate on their own; only fallback cards open the detail view.\n            if (card.hasAttribute(\"href\")) return;\n            showCar(card.dataset.id);\n        });\n\n        els.reset.addEventListener(\"click\", () => {\n            DIMS.forEach((d) => { els[d].value = \"\"; });\n            onFilterChange();\n        });\n\n        \/* --------- persistent cache: paint the grid instantly on repeat visits --------- *\/\n\n        \/\/ localStorage, not sessionStorage: a visit tomorrow paints straight from the last\n        \/\/ grid too. The live data is still fetched on every visit and replaces it.\n\n        function cacheable(list) {\n            \/\/ Post content is deliberately left out: it is large and not needed to draw a card.\n            return list.map((car) => ({\n                id: car.id, title: car.title, maker: car.maker, tuner: car.tuner,\n                model: car.model, thumb: car.thumb, srcset: car.srcset, link: car.link\n            }));\n        }\n\n        function signature(list) {\n            return list.map((car) => [\n                car.id, car.title, car.maker, car.tuner, car.model, car.thumb, car.srcset, car.link\n            ].join(\"|\")).join(\"~\");\n        }\n\n        function readCache() {\n            try {\n                const parsed = JSON.parse(localStorage.getItem(CACHE_KEY) || \"null\");\n                if (!Array.isArray(parsed)) return [];\n                \/\/ Ignore anything that does not look like a card, then restore the fields\n                \/\/ the cache drops or an older entry may lack.\n                return parsed\n                    .filter((entry) => entry?.id !== undefined)\n                    .map((entry) => Object.assign({ html: \"\", srcset: \"\" }, entry));\n            } catch (e) {\n                return [];\n            }\n        }\n\n        function writeCache(list) {\n            try {\n                localStorage.setItem(CACHE_KEY, JSON.stringify(cacheable(list)));\n            } catch (e) { \/* quota or private mode *\/ }\n        }\n\n        function renderCounters(list) {\n            const tuners = new Set(list.map((car) => car.tuner).filter(Boolean));\n            els.carsCounter.textContent = String(list.length).padStart(2, \"0\");\n            els.tunerCounter.textContent = String(tuners.size).padStart(2, \"0\");\n            els.carsCounter.classList.remove(\"mtk-skel-text\");\n            els.tunerCounter.classList.remove(\"mtk-skel-text\");\n        }\n\n        \/* ---------------------------------- loading --------------------------------- *\/\n\n        async function hydrateContent() {\n            \/\/ Second pass, once the grid is already on screen: fetch content only for posts\n            \/\/ that still need it \u2014 no featured image (the thumbnail then comes from the\n            \/\/ first inline image) or no permalink (inline detail view fallback).\n            if (window.MTK_PRELOAD) return;\n            const need = cars.filter((car) => {\n                if (!car.thumb) return true;\n                return !car.link;\n            });\n            if (!need.length) return;\n\n            const ids = need.map((car) => car.id);\n            const bodies = new Map();\n            for (let i = 0; ids.length > i; i += CONTENT_BATCH) {\n                const rows = await fetchAll(\n                    \"\/wp\/v2\/posts?include=\" + ids.slice(i, i + CONTENT_BATCH).join(\",\") +\n                    \"\\x26_fields=id,content\"\n                );\n                rows.forEach((row) => bodies.set(row.id, row.content?.rendered || \"\"));\n            }\n\n            let thumbsFound = false;\n            cars.forEach((car) => {\n                if (!bodies.has(car.id)) return;\n                car.html = bodies.get(car.id);\n                if (car.thumb) return;\n                const found = firstImage(car.html);\n                if (!found) return;\n                car.thumb = found;\n                thumbsFound = true;\n            });\n            \/\/ Only repaint if a card actually gained an image.\n            if (!thumbsFound) return;\n            renderGallery();\n            writeCache(cars);\n        }\n\n        (async function init() {\n            const cached = window.MTK_PRELOAD ? [] : readCache();\n            if (cached.length) {\n                cars = cached;\n                refreshFilters();\n                renderGallery();          \/\/ grid is visible before any request returns\n                renderCounters(cars);\n            } else {\n                showSkeleton();           \/\/ first visit: placeholders until the data lands\n            }\n            try {\n                const data = await loadData();\n                const fresh = data.posts.map((post) => toCar(post, data.tagName));\n\n                if (!fresh.length) {\n                    if (!cars.length) {\n                        clearSkeleton();\n                        setStatus(t(\"ui.archives.none_published\"));\n                    }\n                    return;\n                }\n                \/\/ Skip the repaint when nothing changed since the cached render, so images\n                \/\/ already on screen are not torn down and decoded again.\n                const changed = signature(fresh) !== signature(cars);\n                cars = fresh;\n                if (changed) {\n                    refreshFilters();\n                    renderGallery();\n                    renderCounters(cars);\n                }\n                writeCache(cars);\n                await hydrateContent();\n            } catch (err) {\n                \/\/ A failed refresh behind a working cached grid is not worth an error.\n                if (!cars.length) {\n                    clearSkeleton();\n                    setStatus(t(\"ui.archives.error\") + \" \" + err.message);\n                }\n            }\n        })();\n    })();\n<\/script>","protected":false},"excerpt":{"rendered":"<p>ARCHIVES XX CARS XX TUNERS FILTERS TUNERALL MAKERALL MODELALL Reset Loading archive\u2026<\/p>","protected":false},"author":4,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"wp-custom-template-archives-de-v-hicule","meta":{"inline_featured_image":false,"footnotes":""},"class_list":["post-3227","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.6 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Archives - MTK CLUB<\/title>\n<meta name=\"robots\" content=\"noindex, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Archives - MTK CLUB\" \/>\n<meta property=\"og:description\" content=\"ARCHIVES XX CARS XX TUNERS FILTERS TUNERALL MAKERALL MODELALL Reset Loading archive\u2026\" \/>\n<meta property=\"og:url\" content=\"https:\/\/recette.mtk-club.jp\/en\/archives\/\" \/>\n<meta property=\"og:site_name\" content=\"MTK CLUB\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-30T16:37:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/recette.mtk-club.jp\/wp-content\/uploads\/2026\/01\/mtk_five_stars_logo.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"922\" \/>\n\t<meta property=\"og:image:height\" content=\"273\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/recette.mtk-club.jp\/archives\/\",\"url\":\"https:\/\/recette.mtk-club.jp\/archives\/\",\"name\":\"Archives - MTK CLUB\",\"isPartOf\":{\"@id\":\"https:\/\/recette.mtk-club.jp\/#website\"},\"datePublished\":\"2026-06-15T20:38:08+00:00\",\"dateModified\":\"2026-09-30T16:37:00+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/recette.mtk-club.jp\/archives\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/recette.mtk-club.jp\/archives\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/recette.mtk-club.jp\/archives\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\/\/recette.mtk-club.jp\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Archives\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/recette.mtk-club.jp\/#website\",\"url\":\"https:\/\/recette.mtk-club.jp\/\",\"name\":\"MTK Club\",\"description\":\"MIDNIGHT T\u014cGE KIDS | Some frenchies enjoying good old t\u014dge racing.\",\"publisher\":{\"@id\":\"https:\/\/recette.mtk-club.jp\/#organization\"},\"alternateName\":\"MTK Club\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/recette.mtk-club.jp\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/recette.mtk-club.jp\/#organization\",\"name\":\"MTK Club\",\"url\":\"https:\/\/recette.mtk-club.jp\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/recette.mtk-club.jp\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/recette.mtk-club.jp\/wp-content\/uploads\/2026\/01\/mtk_five_stars_logo.jpg\",\"contentUrl\":\"https:\/\/recette.mtk-club.jp\/wp-content\/uploads\/2026\/01\/mtk_five_stars_logo.jpg\",\"width\":922,\"height\":273,\"caption\":\"MTK Club\"},\"image\":{\"@id\":\"https:\/\/recette.mtk-club.jp\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.instagram.com\/mtk.vhs\/\",\"https:\/\/www.instagram.com\/mtk_archives\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Archives - MTK CLUB","robots":{"index":"noindex","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"og_locale":"en_US","og_type":"article","og_title":"Archives - MTK CLUB","og_description":"ARCHIVES XX CARS XX TUNERS FILTERS TUNERALL MAKERALL MODELALL Reset Loading archive\u2026","og_url":"https:\/\/recette.mtk-club.jp\/en\/archives\/","og_site_name":"MTK CLUB","article_modified_time":"2026-09-30T16:37:00+00:00","og_image":[{"width":922,"height":273,"url":"https:\/\/recette.mtk-club.jp\/wp-content\/uploads\/2026\/01\/mtk_five_stars_logo.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/recette.mtk-club.jp\/archives\/","url":"https:\/\/recette.mtk-club.jp\/archives\/","name":"Archives - MTK CLUB","isPartOf":{"@id":"https:\/\/recette.mtk-club.jp\/#website"},"datePublished":"2026-06-15T20:38:08+00:00","dateModified":"2026-09-30T16:37:00+00:00","breadcrumb":{"@id":"https:\/\/recette.mtk-club.jp\/archives\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/recette.mtk-club.jp\/archives\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/recette.mtk-club.jp\/archives\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/recette.mtk-club.jp\/"},{"@type":"ListItem","position":2,"name":"Archives"}]},{"@type":"WebSite","@id":"https:\/\/recette.mtk-club.jp\/#website","url":"https:\/\/recette.mtk-club.jp\/","name":"MTK Club","description":"MIDNIGHT T\u014cGE KIDS | Some frenchies enjoying good old t\u014dge racing.","publisher":{"@id":"https:\/\/recette.mtk-club.jp\/#organization"},"alternateName":"MTK Club","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/recette.mtk-club.jp\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/recette.mtk-club.jp\/#organization","name":"MTK Club","url":"https:\/\/recette.mtk-club.jp\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/recette.mtk-club.jp\/#\/schema\/logo\/image\/","url":"https:\/\/recette.mtk-club.jp\/wp-content\/uploads\/2026\/01\/mtk_five_stars_logo.jpg","contentUrl":"https:\/\/recette.mtk-club.jp\/wp-content\/uploads\/2026\/01\/mtk_five_stars_logo.jpg","width":922,"height":273,"caption":"MTK Club"},"image":{"@id":"https:\/\/recette.mtk-club.jp\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.instagram.com\/mtk.vhs\/","https:\/\/www.instagram.com\/mtk_archives\/"]}]}},"_links":{"self":[{"href":"https:\/\/recette.mtk-club.jp\/en\/wp-json\/wp\/v2\/pages\/3227","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/recette.mtk-club.jp\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/recette.mtk-club.jp\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/recette.mtk-club.jp\/en\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/recette.mtk-club.jp\/en\/wp-json\/wp\/v2\/comments?post=3227"}],"version-history":[{"count":42,"href":"https:\/\/recette.mtk-club.jp\/en\/wp-json\/wp\/v2\/pages\/3227\/revisions"}],"predecessor-version":[{"id":6031,"href":"https:\/\/recette.mtk-club.jp\/en\/wp-json\/wp\/v2\/pages\/3227\/revisions\/6031"}],"wp:attachment":[{"href":"https:\/\/recette.mtk-club.jp\/en\/wp-json\/wp\/v2\/media?parent=3227"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}