Release 202507031117
This commit is contained in:
@@ -275,7 +275,7 @@ CREATE OR REPLACE FUNCTION handle_suggestion_status_change()
|
|||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
service_name TEXT;
|
service_name TEXT;
|
||||||
service_visibility "ServiceVisibility";
|
service_visibility "serviceVisibility";
|
||||||
is_user_admin_or_moderator BOOLEAN;
|
is_user_admin_or_moderator BOOLEAN;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Award karma for first approval
|
-- Award karma for first approval
|
||||||
@@ -283,7 +283,7 @@ BEGIN
|
|||||||
-- and ensure it wasn't already APPROVED.
|
-- and ensure it wasn't already APPROVED.
|
||||||
IF OLD.status IS DISTINCT FROM 'APPROVED' AND NEW.status = 'APPROVED' THEN
|
IF OLD.status IS DISTINCT FROM 'APPROVED' AND NEW.status = 'APPROVED' THEN
|
||||||
-- Fetch service details for the description
|
-- Fetch service details for the description
|
||||||
SELECT name, visibility INTO service_name, service_visibility FROM "Service" WHERE id = NEW."serviceId";
|
SELECT name, serviceVisibility INTO service_name, service_visibility FROM "Service" WHERE id = NEW."serviceId";
|
||||||
|
|
||||||
-- Only award karma if the service is public
|
-- Only award karma if the service is public
|
||||||
IF service_visibility = 'PUBLIC' THEN
|
IF service_visibility = 'PUBLIC' THEN
|
||||||
|
|||||||
@@ -7,186 +7,184 @@ import { makeSearchFiltersOptions } from '../../lib/searchFiltersOptions'
|
|||||||
import type { APIRoute } from 'astro'
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ site }) => {
|
export const GET: APIRoute = async ({ site }) => {
|
||||||
if (!site) {
|
if (!site) return new Response('Site URL not configured', { status: 500 })
|
||||||
return new Response('Site URL not configured', { status: 500 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchUrls = await generateSEOSitemapUrls(site.href)
|
try {
|
||||||
|
const searchUrls = await generateSEOSitemapUrls(site.href)
|
||||||
|
|
||||||
const result = `
|
const result = `
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
${searchUrls.map((url) => `<url><loc>${he.encode(url)}</loc></url>`).join('\n')}
|
${searchUrls.map((url) => `<url><loc>${he.encode(url)}</loc></url>`).join('\n')}
|
||||||
</urlset>
|
</urlset>
|
||||||
`.trim()
|
`.trim()
|
||||||
|
|
||||||
return new Response(result, {
|
return new Response(result, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/xml',
|
'Content-Type': 'application/xml',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate SEO sitemap URLs:', error)
|
||||||
|
return new Response('Failed to generate SEO sitemap URLs', { status: 500 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateSEOSitemapUrls(siteUrl: string) {
|
async function generateSEOSitemapUrls(siteUrl: string) {
|
||||||
try {
|
const [categories, attributes] = await Promise.all([
|
||||||
const [categories, attributes] = await Promise.all([
|
prisma.category.findMany({
|
||||||
prisma.category.findMany({
|
select: {
|
||||||
select: {
|
name: true,
|
||||||
name: true,
|
namePluralLong: true,
|
||||||
namePluralLong: true,
|
slug: true,
|
||||||
slug: true,
|
icon: true,
|
||||||
icon: true,
|
_count: {
|
||||||
_count: {
|
select: {
|
||||||
select: {
|
services: {
|
||||||
services: {
|
where: {
|
||||||
where: {
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.attribute.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
title: true,
|
||||||
|
category: true,
|
||||||
|
type: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
services: {
|
||||||
|
where: {
|
||||||
|
service: {
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
prisma.attribute.findMany({
|
orderBy: [{ category: 'asc' }, { type: 'asc' }, { title: 'asc' }],
|
||||||
select: {
|
}),
|
||||||
id: true,
|
])
|
||||||
slug: true,
|
|
||||||
title: true,
|
|
||||||
category: true,
|
|
||||||
type: true,
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
services: {
|
|
||||||
where: {
|
|
||||||
service: {
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: [{ category: 'asc' }, { type: 'asc' }, { title: 'asc' }],
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
const filtersOptions = makeSearchFiltersOptions({
|
const filtersOptions = makeSearchFiltersOptions({
|
||||||
filters: null,
|
filters: null,
|
||||||
categories,
|
categories,
|
||||||
attributes,
|
attributes,
|
||||||
})
|
})
|
||||||
|
|
||||||
const byCategory = filtersOptions.categories.map(
|
const byCategory = filtersOptions.categories.map(
|
||||||
(category) =>
|
(category) =>
|
||||||
|
new URLSearchParams({
|
||||||
|
categories: category.slug,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const byVerificationStatus = filtersOptions.verification.map(
|
||||||
|
(status) =>
|
||||||
|
new URLSearchParams({
|
||||||
|
verification: status.slug,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const byKycLevel = filtersOptions.kycLevels.map(
|
||||||
|
(level) =>
|
||||||
|
new URLSearchParams({
|
||||||
|
'max-kyc': level.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const byCurrency = filtersOptions.currencies.map(
|
||||||
|
(currency) =>
|
||||||
|
new URLSearchParams({
|
||||||
|
currencies: currency.slug,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const withOneAttribute = filtersOptions.attributesByCategory
|
||||||
|
.flatMap(({ attributes }) => attributes)
|
||||||
|
.map(
|
||||||
|
(attribute) =>
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
categories: category.slug,
|
[`attr-${attribute.id.toString()}`]: 'yes',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const withoutOneAttribute = filtersOptions.attributesByCategory
|
||||||
|
.flatMap(({ attributes }) => attributes)
|
||||||
|
.map(
|
||||||
|
(attribute) =>
|
||||||
|
new URLSearchParams({
|
||||||
|
[`attr-${attribute.id.toString()}`]: 'no',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const byVerificationStatus = filtersOptions.verification.map(
|
const byCategoryAndCurrency = filtersOptions.categories.flatMap((category) =>
|
||||||
(status) =>
|
filtersOptions.currencies.map(
|
||||||
new URLSearchParams({
|
|
||||||
verification: status.slug,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const byKycLevel = filtersOptions.kycLevels.map(
|
|
||||||
(level) =>
|
|
||||||
new URLSearchParams({
|
|
||||||
'max-kyc': level.id,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const byCurrency = filtersOptions.currencies.map(
|
|
||||||
(currency) =>
|
(currency) =>
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
|
categories: category.slug,
|
||||||
currencies: currency.slug,
|
currencies: currency.slug,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const withOneAttribute = filtersOptions.attributesByCategory
|
const byCategoryAndAttributes = filtersOptions.categories.flatMap((category) =>
|
||||||
|
filtersOptions.attributesByCategory
|
||||||
.flatMap(({ attributes }) => attributes)
|
.flatMap(({ attributes }) => attributes)
|
||||||
.map(
|
.flatMap((attribute) => [
|
||||||
(attribute) =>
|
new URLSearchParams({
|
||||||
new URLSearchParams({
|
categories: category.slug,
|
||||||
[`attr-${attribute.id.toString()}`]: 'yes',
|
[`attr-${attribute.id.toString()}`]: 'yes',
|
||||||
})
|
}),
|
||||||
)
|
new URLSearchParams({
|
||||||
const withoutOneAttribute = filtersOptions.attributesByCategory
|
categories: category.slug,
|
||||||
|
[`attr-${attribute.id.toString()}`]: 'no',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const relevantCurrencies = [
|
||||||
|
'xmr',
|
||||||
|
'btc',
|
||||||
|
] as const satisfies (typeof filtersOptions.currencies)[number]['slug'][]
|
||||||
|
|
||||||
|
const byCategoryAndAttributesAndRelevantCurrency = filtersOptions.categories.flatMap((category) =>
|
||||||
|
filtersOptions.attributesByCategory
|
||||||
.flatMap(({ attributes }) => attributes)
|
.flatMap(({ attributes }) => attributes)
|
||||||
.map(
|
.flatMap((attribute) =>
|
||||||
(attribute) =>
|
relevantCurrencies.map(
|
||||||
new URLSearchParams({
|
(currency) =>
|
||||||
[`attr-${attribute.id.toString()}`]: 'no',
|
new URLSearchParams({
|
||||||
})
|
categories: category.slug,
|
||||||
)
|
currencies: currency,
|
||||||
|
[`attr-${attribute.id.toString()}`]:
|
||||||
const byCategoryAndCurrency = filtersOptions.categories.flatMap((category) =>
|
attribute.type === 'GOOD' || attribute.type === 'INFO' ? 'yes' : 'no',
|
||||||
filtersOptions.currencies.map(
|
})
|
||||||
(currency) =>
|
|
||||||
new URLSearchParams({
|
|
||||||
categories: category.slug,
|
|
||||||
currencies: currency.slug,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const byCategoryAndAttributes = filtersOptions.categories.flatMap((category) =>
|
|
||||||
filtersOptions.attributesByCategory
|
|
||||||
.flatMap(({ attributes }) => attributes)
|
|
||||||
.flatMap((attribute) => [
|
|
||||||
new URLSearchParams({
|
|
||||||
categories: category.slug,
|
|
||||||
[`attr-${attribute.id.toString()}`]: 'yes',
|
|
||||||
}),
|
|
||||||
new URLSearchParams({
|
|
||||||
categories: category.slug,
|
|
||||||
[`attr-${attribute.id.toString()}`]: 'no',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
const relevantCurrencies = [
|
|
||||||
'xmr',
|
|
||||||
'btc',
|
|
||||||
] as const satisfies (typeof filtersOptions.currencies)[number]['slug'][]
|
|
||||||
|
|
||||||
const byCategoryAndAttributesAndRelevantCurrency = filtersOptions.categories.flatMap((category) =>
|
|
||||||
filtersOptions.attributesByCategory
|
|
||||||
.flatMap(({ attributes }) => attributes)
|
|
||||||
.flatMap((attribute) =>
|
|
||||||
relevantCurrencies.map(
|
|
||||||
(currency) =>
|
|
||||||
new URLSearchParams({
|
|
||||||
categories: category.slug,
|
|
||||||
currencies: currency,
|
|
||||||
[`attr-${attribute.id.toString()}`]:
|
|
||||||
attribute.type === 'GOOD' || attribute.type === 'INFO' ? 'yes' : 'no',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const allQueryParams = [
|
const allQueryParams = [
|
||||||
...byCategory,
|
...byCategory,
|
||||||
...byVerificationStatus,
|
...byVerificationStatus,
|
||||||
...byKycLevel,
|
...byKycLevel,
|
||||||
...byCurrency,
|
...byCurrency,
|
||||||
...withOneAttribute,
|
...withOneAttribute,
|
||||||
...withoutOneAttribute,
|
...withoutOneAttribute,
|
||||||
|
|
||||||
...byCategoryAndCurrency,
|
...byCategoryAndCurrency,
|
||||||
...byCategoryAndAttributes,
|
...byCategoryAndAttributes,
|
||||||
...byCategoryAndAttributesAndRelevantCurrency,
|
...byCategoryAndAttributesAndRelevantCurrency,
|
||||||
] satisfies URLSearchParams[]
|
] satisfies URLSearchParams[]
|
||||||
|
|
||||||
return allQueryParams.map((queryParams) => {
|
return allQueryParams.map((queryParams) => {
|
||||||
const url = new URL(siteUrl)
|
const url = new URL(siteUrl)
|
||||||
url.search = queryParams.toString()
|
url.search = queryParams.toString()
|
||||||
return url.href
|
return url.href
|
||||||
})
|
})
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate SEO sitemap URLs:', error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user