818,46022374097122,46022374228194,46022404931810,46022421151970,46022421250274,46022851264738,46022883148002,46022883311842,46022883377378,46022883442914,46022883475682,46022883770594,46025143189730,46025150562530,46025308045538,46025308274914,46025324757218,46025324789986,46025324822754,46025324921058,46025324953826,46025324986594,46025325019362,46025325052130,46025325084898,46025325117666,46025328951522,46025358541026,46025358573794,46025371746530,46025371910370,46025372008674,46025372041442,46025372074210,46025372205282,46025372369122,46025372401890,46025419849954,46025438560482,46025438658786,46034406998242,46034471092450,46034766266594,46034799231202,46034809454818,46034809487586,46038086680802,46038086713570,46038086746338,46038086779106,46038086811874,46038086844642,46038086877410,46038086910178,46038086942946,46038086975714,46038087008482,46038087041250,46038087074018,46038087106786,46038087139554,46038087172322,46038087205090,46038087237858,46038087270626,46038087303394,46038087336162,46038087368930,
46038087401698,46038087434466,46038087467234,46038087500002,46038087532770,46038087565538,46038087598306,46038087631074,46038087663842,46038087696610,46038087729378,46038087762146,46038087794914,46038087827682,46038087860450,46038087925986,46038087991522,46038088057058,46038088122594,46038088188130,46038088253666,46038088384738,46038088515810,46038088646882,46038088679650,46038088712418,46038088745186,46038088777954,46038088810722,46038088843490,46038088876258,46038088909026,46038088941794,46038088974562,46038089007330,46038203236578,46038203269346,46038229549282,46038229582050,46038232137954,46038232170722,46038232203490,46038232236258,46038232957154,46038232989922,46038233022690,46038233055458,46038233088226,46038233120994,46038233153762,46038233186530,46038233219298,46038245540066,46038245671138,46038245736674,46038245867746,46038245933282,46038246064354,46041328025826,46042261291234,46042261324002,46042261356770,46044399927522,46044399993058,46044400025826,46044400058594,46044411592930,46044414509282,4604k.marketId || '',
country_code: _sqTrack.countryIsoCode || '',
page_type: _sqTrack.pageType || '',
cart_token: _sqTrack.cartToken || '',
page_url: (window.location && window.location.href) || '',
properties: {
// Mirror the bundle's own preorder check (preorder.js checkInventoryProvider-
// WithCachedFallback): the live set config.preorderVariantIds (refreshed after
// page load by loadVariantInventoryData) OR the Liquid-injected
// cachedPreorderVariantIds — always present, so it covers clicks before the
// bundle loads (why this tracker is inline). ORing both (not preferring one)
// matches the bundle and is correct even if the live set is an empty array. Both
// hold integer ids; the click-time id comes off a DOM input (.value → string) or
// selected_variant_id, so coerce with Number for a like-typed compare.
is_preord
er_variant: (function () {
var vid = Number(variantId);
var has = function (a) { return Array.isArray(a) && a.indexOf(vid) !== -1; };
return has(_sqTrack.preorderVariantIds) ||
has(_sqTrack.cachedPreorderVariantIds &&
_sqTrack.cachedPreorderVariantIds.preorder_variant_ids);
})(),
// Narrow, authoritative preorder signal: enrolled in an ENABLED STOQ selling plan
// (is_preorder_variant above is the broad OOS+continue heuristic — non-STOQ too).
in_enabled_plan: !!_sqPlan,
// In-stock preorder signal: enrolled but currently IN STOCK (in the inStockVariantIds
// set, not the OOS preorderVariantIds set). Same live-OR-cached pattern as
// is_preorder_variant. is_preorder_variant OR is_instock_variant = "should sell via
// the plan" regardless of stock, so an in-stock preorder bypass is detectable too.
is_instock_variant: (function () {
var has = function (a) { return Array.isArray(a) && a.indexOf(_sqVidNum) !== -1; };
return has(_sqTrack.inStockVariantIds) ||
has(_sqTrack.cachedInStockVariantIds &&
_sqTrack.cachedInStockVariantIds.in_stock_variant_ids);
})(),
// The enrolled plan's Shopify selling-plan id + inventory_provider (stoq = STOQ tracks
// the OOS count; shopify = in-stock preorder). Tells order-debug what kind of preorder
// the Buy Now bypassed, without a downstream lookup.
enrolled_plan_id: (_sqPlan && (_sqPlan.shopify_selling_plan_id || _sqPlan.shopify_selling_plan_group_id)) || '',
enrolled_plan_provider: (_sqPlan && _sqPlan.inventory_provider) || '',
block_enabled: !!(_sqTrack.cachedSettings && _sqTrack.cachedSettings.block_accelerated_checkout),
gate_armed: !!_sqTrack.acceleratedGateArmed,
gate_active
_at_click: !!(document.documentElement &&
document.documentElement.classList.contains('stoq-buy-now-pending')),
},
});
} catch (err) { /* never break checkout */ }
}, true);
}
} catch (e) { /* fail open */ }
function applyTranslations(settings) {
try {
// Skip translation logic entirely if multi-language is not enabled
if (!settings || !settings.multi_language_enabled) {
return settings;
}
if (!settings.translations) {
console.debug('STOQ - No translations found, skipping translation');
return settings;
}
const normalizedLocale = window._RestockRocketConfig.normalizedLocale;
const translations = settings.translations;
if (!normalizedLocale) {
// No matching locale has translations; drop payload to save memory
console.debug('STOQ - No matching locale for translations. Available:', Object.keys(translations || {}));
delete settings.translations;
return settings;
}
console.debug(`STOQ - Applying translations for normalized locale: ${normalizedLocale} (original: ${window._RestockRocketConfig.locale})`);
const translatedFields = translations[normalizedLocale];
if (translatedFields && typeof translatedFields === 'object') {
Object.keys(translatedFields).forEach(function(key) {
const value = translatedFields[key];
if (value !== null && value !== undefined && value !== '') {
settings[key] = value;
}
});
} else {
console.debug('STOQ - No translated fields found for locale:', normalizedLocale);
}
delete settings.translations;
return settings;
} catch (e) {
console.debug('STOQ - error applying translations:', e);
return settings;
}
}
// Setup event listener for cart selling plan updates
// This must be called before any scripts are loaded to avoid race conditions
function setupCartSellingPlanUpdater(settings) {
// Setup listener regardless - updateCartSellingPlans has its own guards
// This ensures cleanup happens even when preorders are disabled globally
// Listen for stoq:inventory-data-loaded event dispatched by api.js
window.addEventListener('stoq:inventory-data-loaded', function(event) {
console.debug('STOQ - Inventory data loaded, updating cart selling plans');
if (window._RestockRocket && window._RestockRocket.updateCartSellingPlans) {
window._RestockRocket.updateCartSellingPlans()
.then(hasUpdates => {
if (hasUpdates) {
console.debug('STOQ - cart selling plans updated successfully');
} else {
console.debug('STOQ - no cart selling plan updates needed');
}
})
.catch(error => {
console.error('STOQ - error updating cart selling plans:', error);
});
}
});
}
// ---- Live settings via the Storefront API (additive, does not gate init) ----
// The inlined `cachedSettings` comes from the app-embed Liquid render, which
// Shopify edge-caches and can serve stale. Reading the same `settings` metafield
// back over the Storefront API is a POST to /api/
<
ion run() {
const query = 'query StoqSettings($namespace: String!) { shop { metafield(namespace: $namespace, key: "settings") { value } } }';
fetch(
`https://${cfg.shop}/api/2025-07/graphql.json`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': cfg.storefrontAccessToken,
},
body: JSON.stringify({ query: query, variables: { namespace: cfg.metafieldNamespace } }),
}
)
.then(function(response) {
if (!response.ok) { throw new Error('Storefront API HTTP ' + response.status); }
return response.json();
})
.then(function(body) {
if (body && body.errors && body.errors.length) {
throw new Error('Storefront API errors: ' + body.errors.map(function(e) { return e && e.message; }).join(', '));
}
const value = body && body.data && body.data.shop && body.data.shop.metafield && body.data.shop.metafield
gh
// to the same Liquid-cached fallback as a fetch failure.
console.error('STOQ - apply failed for ' + endpoint + ':', applyError);
throw applyError;
}
})
.catch(function(error) {
console.debug(`STOQ - using cached ${endpoint}:`, error.message);
});
}
function initializeScripts(settings) {
settings = applyTranslations(settings);
window._RestockRocketConfig.settings = settings;
console.debug(`STOQ - settings configured for ${window._RestockRocketConfig.pageType}`);
// Stale-Liquid resilience (default-on, per-shop opt-out via the
// `disable_refresh_on_stale_liquid` Toggle, surfaced as the negative
// `disable_refresh_on_stale_liquid` flag in settings.json so that
// `undefined` -- in CDN-cached metafield payloads that predate this
// key -- reads as `!undefined === true` and gets default-on behavior
// immediately, no metafield rewrite required).
// When the Liquid CDN cache is older than LIQUID_CACHE_MAX_AGE the in-
ans = data.plans;
window._RestockRocketConfig.disabledSellingPlanIds = data.disabled_plan_ids || [];
}
}),
fetchEmbedConfig('integrations', function(data) {
if (Array.isArray(data)) {
window._RestockRocketConfig.integrations = data;
}
})
]),
new Promise(function(resolve) { setTimeout(resolve, 1000); })
]).then(function() { loadScripts(settings); });
return;
}
loadScripts(settings);
}
function loadScripts(settings) {
// Setup cart selling plan updater BEFORE loading any scripts to avoid race conditions
setupCartSellingPlanUpdater(settings);
if(settings.enable_app) {
const hijackIntegration = window._RestockRocketConfig.integrations.find(function(integration) {
return integration.type === 'hijack' && integration.enabled && integration.page_types.includes(window._RestockRocketConfig.pageType);
})
// STOQ-1520: serve the lean back-in-stock-
only build (no preorder/hijack code)
// only to shops with NO preorder plans. Use the full build if preorder is on,
// an enabled offer exists, or a disabled-but-kept plan id remains (cart sweep
// must still strip those). Rationale in the PR.
const hasEnabledOffer = Array.isArray(window._RestockRocketConfig.sellingPlans)
&& window._RestockRocketConfig.sellingPlans.some(function(plan) { return plan && plan.enabled; });
const hasDisabledPlanIds = Array.isArray(window._RestockRocketConfig.disabledSellingPlanIds)
&& window._RestockRocketConfig.disabledSellingPlanIds.length > 0;
const usePreorderBuild = settings.preorder_enabled || hasEnabledOffer || hasDisabledPlanIds;
const collectionScriptUrl = usePreorderBuild
? window._RestockRocketConfig.scriptUrlCollection
: window._RestockRocketConfig.scriptUrlCollectionBis;
const productScriptUrl = usePreorderBuild
? window._RestockRocketConfig.scriptUrlProduct
: window._Rest
ockRocketConfig.scriptUrlProductBis;
const pageType = window._RestockRocketConfig.pageType;
const collectionPageTypes = ['collection', 'index', 'search', 'page'];
if(collectionPageTypes.indexOf(pageType) !== -1 && (settings[`show_button_on_${pageType}`] || settings[`preorder_${pageType}_enabled`])) {
createRestockRocketScript(collectionScriptUrl);
} else if(pageType === 'product') {
createRestockRocketScript(productScriptUrl);
} else if(hijackIntegration) {
createRestockRocketScript(window._RestockRocketConfig.scriptUrlCollection);
} else if(usePreorderBuild) {
// cart/article/blog/list-collections: full build so the cart sweep runs.
createRestockRocketScript(window._RestockRocketConfig.scriptUrlCollection);
} else {
console.debug(`STOQ - no scripts enabled for ${pageType}`);
}
// Dispatch custom event when app is loaded
// Cart selling plan updates will be triggered by stoq:inventory-data-loaded eve
nt
const appLoadedEvent = new CustomEvent('stoq:loaded', {
detail: {
pageType: window._RestockRocketConfig.pageType,
enabled: settings.enable_app,
settings: settings,
preorderEnabled: settings.preorder_enabled
}
});
console.debug('STOQ - dispatching app loaded event');
window.dispatchEvent(appLoadedEvent);
}
}
I'm looking to buy this product, when will it arrive and how much does postage cost?
Rear Suspension
Shop All Second-Hand
If the item is showing in stock, we aim to post the product within 24 hours. Please allow 5 working days to receive the item.
Postage is free on orders over £50. Orders under £50, our postage charge is £3.99.
// the bundle had loaded on that load).
var _stoqPageSeq = 0;
try {
_stoqPageSeq = (parseInt(window.sessionStorage.getItem('_stoq_pls') || '0', 10) || 0) + 1;
window.sessionStorage.setItem('_stoq_pls', String(_stoqPageSeq));
} catch (e) { /* sessionStorage unavailable (private mode) — leave 0 */ }
Shopify?.analytics?.publish?.('stoq_initialized', {
cart_token: _stoqInitConfig.cartToken || '',
page_url: window.location.href,
page_type: _stoqInitConfig.pageType || '',
shop_domain: _stoqInitConfig.shop || '',
market_id: _stoqInitConfig.marketId || '',
// Buyer's storefront country (localization.country.iso_code). Seeds the
// pixel's extension_details blob so it rides forward onto Shopify's native
// events too — a market can span countries, so this is the finer signal.
country_code: _stoqInitConfig.countryIsoCode || '',
detected_variant_ids: _stoqDetectedVariantIds,
selected_variant_id: _stoqSelectedVariantId ||m the
// derivation block above) — nothing to bypass otherwise. variant_id is read per-clicked-button
// from its form so it's correct on collection/index/search too. block_accelerated_checkout
// context (STOQ-1612) rides in `properties` (forwarded verbatim by the pixel, no schema
// change): a buy_now click on a preorder variant while the gate was armed is a visible
// bypass signal for order debug. Also sends page_url (top-level, like the pixel's native
// events) and two preorder signals in properties (see below).
try {
var _sqTrack = window._RestockRocketConfig;
if (_sqTrack.preordersConfigured) {
document.addEventListener('click', function (e) {
try {
var t = e.target;
if (!t || !t.closest) return;
var host = t.closest('.shopify-payment-button, shopify-payment-terms');
if (!host) return;
var channel = host.matches && host.matches('shopify-payment-terms')
? 'shop_pay_installments' : 'buy_now';
va product_id: (_sqTrack.product && _sqTrack.product.id) || '',
channel: channel,
market_id: _sqTrack.marketId || '',
country_code: _sqTrack.countryIsoCode || '',
page_type: _sqTrack.pageType || '',
cart_token: _sqTrack.cartToken || '',
page_url: (window.location && window.location.href) || '',
properties: {
// Mirror the bundle's own preorder check (preorder.js checkInventoryProvider-
// WithCachedFallback): the live set config.preorderVariantIds (refreshed after
// page load by loadVariantInventoryData) OR the Liquid-injected
// cachedPreorderVariantIds — always present, so it covers clicks before the
// bundle loads (why this tracker is inline). ORing both (not preferring one)
// matches the bundle and is correct even if the live set is an empty array. Both
// hold integer ids; the click-time id comes off a DOM input (.value sell via
// the plan" regardless of stock, so an in-stock preorder bypass is detectable too.
is_instock_variant: (function () {
var has = function (a) { return Array.isArray(a) && a.indexOf(_sqVidNum) !== -1; };
return has(_sqTrack.inStockVariantIds) ||
has(_sqTrack.cachedInStockVariantIds &&
_sqTrack.cachedInStockVariantIds.in_stock_variant_ids);
})(),
// The enrolled plan's Shopify selling-plan id + inventory_provider (stoq = STOQ tracks
// the OOS count; shopify = in-stock preorder). Tells order-debug what kind of preorder
// the Buy Now bypassed, without a downstream lookup.
enrolled_plan_id: (_sqPlan && (_sqPlan.shopify_selling_plan_id || _sqPlan.shopify_selling_plan_group_id)) || '',
enrolled_plan_provider: (_sqPlan && _sqPlan.inventory_provider) || '',
block_enabled: !!(_sqTrack.cachedSettings && _sqTrack.cachmatching locale for translations. Available:', Object.keys(translations || {}));
delete settings.translations;
return settings;
}
console.debug(`STOQ - Applying translations for normalized locale: ${normalizedLocale} (original: ${window._RestockRocketConfig.locale})`);
const translatedFields = translations[normalizedLocale];
if (translatedFields && typeof translatedFields === 'object') {
Object.keys(translatedFields).forEach(function(key) {
const value = translatedFields[key];
if (value !== null && value !== undefined && value !== '') {
settings[key] = value;
}
});
} else {
console.debug('STOQ - No translated fields found for locale:', normalizedLocale);
}
delete settings.translations;
return settings;
} catch (e) {
console.debug('STOQ - error applying translations:', e);
return settings;
}
}
// Setup event listener for cart selling plan updates
// Thi });
}
});
}
// ---- Live settings via the Storefront API (additive, does not gate init) ----
// The inlined `cachedSettings` comes from the app-embed Liquid render, which
// Shopify edge-caches and can serve stale. Reading the same `settings` metafield
// back over the Storefront API is a POST to /api/version>/graphql.json — never
// CDN-cached — so it returns the live value on every page load. We fetch it
// best-effort and stash it on `window._RestockRocketConfig.storefrontSettings`
// so individual behaviours can migrate onto the fresh value over time. This is
// PURELY ADDITIVE: it does NOT change the resolution flow below, never blocks
// init, and silently no-ops on any failure / missing token.
(function loadStorefrontSettings() {
const cfg = window._RestockRocketConfig;
if (!cfg.storefrontAccessToken || cfg.disableStorefrontApi === true) return;
// Deferred to browser idle so this best-effort read runs strictly AFTER the
// critical init flow and never competes with it for a connection — the call
// is a live, uncached Storefront round-trip and can be slow (multi-second
// TTFB observed). Nothing on the critical path waits for it.
function run() {
const query = 'query StoqSettings($namespace: String!) { shop { metafield(namespace: $namespace, key:refrontSettings = JSON.parse(value);
console.debug('STOQ - stored live settings from Storefront API on config.storefrontSettings');
})
.catch(function(e) {
console.debug('STOQ - Storefront settings fetch failed (non-fatal):', e && e.message);
});
}
if (typeof window.requestIdleCallback === 'function') {
window.requestIdleCallback(run, { timeout: 5000 });
} else {
setTimeout(run, 0);
}
})();
// First try to get settings from metafields with expiry check
const cachedSettings = window._RestockRocketConfig.cachedSettings;
const validCachedSettings = cachedSettings ? checkSettingsExpiry(cachedSettings) : null;
if (validCachedSettings) {
console.debug('STOQ - using cached settings');
initializeScripts(validCachedSettings);
} else {
console.debug('STOQ - fetching fresh settings');
const headers = {
'X-Shopify-Shop-Domain': window._RestockRocketConfig.shop || window.Shopify.shop,
'ngrok-skip-browser-warning': 'ski
TOQ - failed to load settings:', error);
}
})
.catch(function(e) {
console.error(e)
})
}
function fetchEmbedConfig(endpoint, apply) {
return fetch(
`${window._RestockRocketConfig.host}/api/v1/embed/${endpoint}.json`,
{
headers: {
'X-Shopify-Shop-Domain': window._RestockRocketConfig.shop || window.Shopify.shop,
'ngrok-skip-browser-warning': 'skip'
}
}
)
.then(function(response) {
if (!response.ok) throw new Error(`Failed to fetch ${endpoint}`);
return response.json();
})
.then(function(data) {
try {
apply(data);
} catch (applyError) {
// Apply failures are programming bugs (e.g. response shape changed
// server-side and the assignment threw). Surface them as console.error
// so they're visible in browser logs, then re-throw to fall through
// to the same Liquid-cached fallback as a fetch failure.
console.error('STOQ - apply failed for ' +
The RD40 wheelset is our game changing carbon road/cyclo cross wheelset. Combining the latest in carbon technology with our all new Pro 5 straight pull hubs these wheels were designed for uncompromised performance for a wide range of disciplines.
Now with our Tubeless valves and Tubeless tape at no extra cost
Key FeaturesTubeless
// Built with Pro 5 straight pull 24 hole hubs using Sapim CX-Ray black straight pull spokes
// Wheels are built from start to finish by hand in Barnoldswick
// Centrelock disc fitment Tools & Maintenance // Only available in 700c
// Boost or Standard
// Comes with our Tubeless Valves and Tubeless Tape
// Carbon Clincher rim, tubeless compatible
// Available with either a Shimano, SRAM or Campag freehub body
// Designed to suit disc brake sets using centrelock rotorsClothing and Protection // The Pro 5 rear hubs feature the reduced drag ratchet mechanism
// 24mm Tape, Valve and wipe included for Tubeless setupSee More // Rim dimensions: 40mm deep, 26.5mm external width and 19mm internal width
// Rim ERD is 563mm
// ETRTO 622x19
// Rim width is ideal to suit larger volume road/cross tyres.ProjektRide Merchandise // Strong monocoque, undirectional carbon fibre construction rim
// Wheels come supplied with 12mm end caps for front wheel and rear wheel comes with 142x12 end caps as standard.
// Choose from our 6 different hub colour options.Helmets // Wheels can be adapted quickly and easily to suit different frame and fork fitting using our conversion kits available separately.
Gloves
Socks
Frequently Asked Questions
Essentials
Please get in touch with a member of the team either by phone (01313745324See More) or email (
[email protected]
) where on of the team will be more than happy to help.
Maintenance
If the item is showing in stock, we aim to post the product within 24 hours. Please allow 5 working days to receive the item.
Postage is free on orders over £50. Orders under £50, our postage charge is £3.99.
We also have a physical store, if you are local please pop in -