feat: extract shop from mp/shop — initial libreshop/shop
Some checks failed
Build and publish / build (push) Failing after 19s
Some checks failed
Build and publish / build (push) Failing after 19s
Source moved verbatim from mp/shop/ on 2026-04-29; mp was the first concrete adapter consuming the libreshop toolkit. Builds and publishes git.librete.ch/libreshop/shop on every main / v* push via the standard .gitea/workflows/build.yml shared across libreshop components.
This commit is contained in:
172
server/api/__sitemap__/urls.ts
Normal file
172
server/api/__sitemap__/urls.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/ü/g, "ue")
|
||||
.replace(/ä/g, "ae")
|
||||
.replace(/ö/g, "oe")
|
||||
.replace(/ß/g, "ss");
|
||||
}
|
||||
|
||||
interface ProductData {
|
||||
id: number;
|
||||
slug?: string;
|
||||
updatedAt?: string;
|
||||
cover?: { id: number } | { data: { id: number } };
|
||||
attributes?: {
|
||||
slug: string;
|
||||
updatedAt: string;
|
||||
cover?: { data: { id: number } };
|
||||
};
|
||||
}
|
||||
|
||||
interface CoverData {
|
||||
id: number;
|
||||
name?: string;
|
||||
sort?: number;
|
||||
updatedAt?: string;
|
||||
attributes?: {
|
||||
name: string;
|
||||
sort?: number;
|
||||
updatedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T[];
|
||||
meta?: {
|
||||
pagination?: {
|
||||
total: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default defineSitemapEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig(event);
|
||||
const siteUrl = config.siteUrl || "https://muellerprints-paperwork.com";
|
||||
const urls: { loc: string; lastmod?: string; priority?: number }[] = [];
|
||||
const allPageSize = 20; // must match notebooks/index.vue pageSize
|
||||
const coverPageSize = 24; // must match notebooks/[cover].vue pageSize
|
||||
|
||||
try {
|
||||
// Fetch all products for detail page URLs
|
||||
const products = await fetchCms<ApiResponse<ProductData>>("/products", {
|
||||
query: {
|
||||
"pagination[pageSize]": "1000",
|
||||
"fields[0]": "slug",
|
||||
"fields[1]": "updatedAt",
|
||||
"populate[cover][fields][0]": "id"
|
||||
}
|
||||
});
|
||||
|
||||
// Product detail pages
|
||||
for (const product of products.data || []) {
|
||||
const slug = product.slug || product.attributes?.slug;
|
||||
const updatedAt = product.updatedAt || product.attributes?.updatedAt;
|
||||
|
||||
if (slug) {
|
||||
urls.push({
|
||||
loc: `${siteUrl}/details/${slug}`,
|
||||
lastmod: updatedAt,
|
||||
priority: 0.8
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch promo products count for pagination (matches what the page actually shows)
|
||||
const promoProducts = await fetchCms<ApiResponse<ProductData>>("/promo-products", {
|
||||
query: {
|
||||
"pagination[pageSize]": "1",
|
||||
"pagination[page]": "1"
|
||||
}
|
||||
});
|
||||
const totalPromoProducts = promoProducts.meta?.pagination?.total || 0;
|
||||
const totalNotebooksPages = Math.ceil(totalPromoProducts / allPageSize);
|
||||
|
||||
// Add /notebooks pagination pages
|
||||
for (let page = 2; page <= totalNotebooksPages; page++) {
|
||||
urls.push({
|
||||
loc: `${siteUrl}/notebooks?page=${page}`,
|
||||
priority: 0.7
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch all covers
|
||||
const covers = await fetchCms<ApiResponse<CoverData>>("/product-covers", {
|
||||
query: {
|
||||
"fields[0]": "name",
|
||||
"fields[1]": "sort",
|
||||
"fields[2]": "updatedAt"
|
||||
}
|
||||
});
|
||||
|
||||
// Sort covers by sort field
|
||||
const sortedCovers = (covers.data || []).sort((a, b) => {
|
||||
const sortA = a.sort ?? a.attributes?.sort ?? 0;
|
||||
const sortB = b.sort ?? b.attributes?.sort ?? 0;
|
||||
return sortA - sortB;
|
||||
});
|
||||
|
||||
// Cover category pages with slugs and pagination
|
||||
for (const cover of sortedCovers) {
|
||||
const name = cover.name || cover.attributes?.name;
|
||||
const updatedAt = cover.updatedAt || cover.attributes?.updatedAt;
|
||||
|
||||
if (name) {
|
||||
const slug = slugify(name);
|
||||
|
||||
// Main category page
|
||||
urls.push({
|
||||
loc: `${siteUrl}/notebooks/${slug}`,
|
||||
lastmod: updatedAt,
|
||||
priority: 0.7
|
||||
});
|
||||
|
||||
// Fetch promo product count for this cover (matches page display)
|
||||
const coverId = cover.id;
|
||||
const coverPromo = await fetchCms<ApiResponse<ProductData>>("/promo-products", {
|
||||
query: {
|
||||
"filters[cover]": String(coverId),
|
||||
"pagination[pageSize]": "1",
|
||||
"pagination[page]": "1"
|
||||
}
|
||||
});
|
||||
const coverTotal = coverPromo.meta?.pagination?.total || 0;
|
||||
const coverPageCount = Math.ceil(coverTotal / coverPageSize);
|
||||
|
||||
// Add pagination pages for this category
|
||||
for (let page = 2; page <= coverPageCount; page++) {
|
||||
urls.push({
|
||||
loc: `${siteUrl}/notebooks/${slug}?page=${page}`,
|
||||
priority: 0.6
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching sitemap data:", error);
|
||||
}
|
||||
|
||||
// Static pages
|
||||
urls.push(
|
||||
{ loc: `${siteUrl}/`, priority: 1.0 },
|
||||
{ loc: `${siteUrl}/notebooks`, priority: 0.9 },
|
||||
// Info pages
|
||||
{ loc: `${siteUrl}/about`, priority: 0.6 },
|
||||
{ loc: `${siteUrl}/kontakt`, priority: 0.5 },
|
||||
{ loc: `${siteUrl}/anfahrt`, priority: 0.4 },
|
||||
{ loc: `${siteUrl}/oeffnungszeiten`, priority: 0.4 },
|
||||
// Legal pages
|
||||
{ loc: `${siteUrl}/impressum`, priority: 0.2 },
|
||||
{ loc: `${siteUrl}/datenschutz`, priority: 0.2 },
|
||||
{ loc: `${siteUrl}/agb`, priority: 0.2 },
|
||||
{ loc: `${siteUrl}/versand`, priority: 0.3 },
|
||||
{ loc: `${siteUrl}/zahlung`, priority: 0.3 }
|
||||
);
|
||||
|
||||
return urls;
|
||||
});
|
||||
Reference in New Issue
Block a user