Add website favicon API and manual favicon profile updates.
Expose GET /tenants/{domain}/website/favicon for storefront tab icons, allow faviconMediaId on profile PATCH, and sync website API docs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
6ce6f959ac
commit
ead16080b8
@@ -30,16 +30,23 @@ You are building a **Meshkee business website (storefront)**. You must use the M
|
||||
|
||||
### Typical bootstrap sequence
|
||||
1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`)
|
||||
2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
|
||||
3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc); each portfolio may include nullable `projectUrl` (external website link). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
|
||||
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||
2. `GET /tenants/{domain}/website/favicon` → `faviconUrl` for `<link rel="icon">` / Next.js `metadata.icons` (falls back to `logoUrl` when no dedicated favicon). Also returns `logoUrl`, `logoDarkUrl`, `hasDedicatedFavicon`.
|
||||
3. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
|
||||
4. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc); each portfolio may include nullable `projectUrl` (external website link). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
|
||||
5. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||
6. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||
- For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl`
|
||||
- Response includes `payment.redirect` `{ method, url, fields }` — POST/redirect shopper to the bank
|
||||
- Meshkee registers the bank `callback_url` on the **store apex** (`https://YOUR_WEBSITE_DOMAIN/meshkee/payments/{gateway}/callback`), which nginx proxies to the API. ZarinPal/Mellat domain checks must match the store domain, not `api.meshkee.com`.
|
||||
- After verify, API redirects the browser to `returnUrl?status=success|failed&orderId=…`
|
||||
- Enabled gateways: `GET /tenants/{domain}` → `ePayment`, or `GET /businesses/{businessId}/payments/methods`
|
||||
|
||||
### Favicon + logos
|
||||
- `GET /tenants/{domain}/website/favicon` → `{ faviconUrl, logoUrl, logoDarkUrl, hasDedicatedFavicon }`
|
||||
- Use `faviconUrl` in layout metadata (Next.js: `icons: [{ url: faviconUrl, type: 'image/png' }]` when set).
|
||||
- When `hasDedicatedFavicon` is false, `faviconUrl` equals the light `logoUrl` — still safe to use as tab icon.
|
||||
- Header/footer logos: `logoUrl` on light backgrounds, `logoDarkUrl` on dark (fallback to `logoUrl` in CSS when null).
|
||||
|
||||
### User products (customer listings)
|
||||
Public marketplace listings owned by customers — not catalog `products`.
|
||||
- `GET /tenants/{domain}/user-products` — list published (`name`/`q`, `categoryId`, `cityId`, `countryId`, `condition`, `promoted`, pagination)
|
||||
|
||||
@@ -1494,6 +1494,13 @@
|
||||
"url": "{{baseUrl}}/tenants/{{domain}}/website/business-info"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get favicon + logos (website)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/tenants/{{domain}}/website/favicon"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "List category groups (website)",
|
||||
"request": {
|
||||
|
||||
@@ -92,6 +92,15 @@
|
||||
<li>Cart / orders / favorites: <code>/businesses/{businessId}/...</code> + Bearer JWT.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Branding (favicon + logos)</h2>
|
||||
<p>
|
||||
<code>GET /tenants/{domain}/website/favicon</code> returns
|
||||
<code>faviconUrl</code>, <code>logoUrl</code>, <code>logoDarkUrl</code>, and
|
||||
<code>hasDedicatedFavicon</code>. Use <code>faviconUrl</code> for the browser tab icon
|
||||
(Next.js <code>metadata.icons</code>). When no dedicated favicon is uploaded,
|
||||
<code>faviconUrl</code> falls back to <code>logoUrl</code>.
|
||||
</p>
|
||||
|
||||
<h2>User products (customer listings)</h2>
|
||||
<p>
|
||||
Marketplace-style stock listings created by customers. Public read-only under
|
||||
|
||||
@@ -176,7 +176,8 @@
|
||||
},
|
||||
"faviconUrl": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"nullable": true,
|
||||
"description": "Tab icon URL. Dedicated favicon when uploaded; otherwise same as logoUrl."
|
||||
},
|
||||
"specialProductsSource": {
|
||||
"type": "string",
|
||||
@@ -332,7 +333,99 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Business public profile"
|
||||
"description": "Business public profile",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"nameFa": { "type": "string" },
|
||||
"about": { "type": "string" },
|
||||
"vision": { "type": "string" },
|
||||
"logoUrl": { "type": "string", "nullable": true },
|
||||
"logoDarkUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Logo for dark backgrounds; null when not set."
|
||||
},
|
||||
"faviconUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Dedicated favicon when uploaded; otherwise same as logoUrl."
|
||||
},
|
||||
"emails": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"phoneNumbers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["landline", "cell"] },
|
||||
"number": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"socialMedia": { "type": "object" },
|
||||
"addresses": { "type": "array", "items": { "type": "object" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/website/favicon": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Homepage"
|
||||
],
|
||||
"summary": "Favicon and logo URLs for site chrome",
|
||||
"description": "Use `faviconUrl` for `<link rel=\"icon\">` or Next.js `metadata.icons`. When no dedicated favicon is uploaded, `faviconUrl` equals `logoUrl`. Prefer this endpoint over tenant resolve when you only need branding assets.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/domain"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Branding asset URLs",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"faviconUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Use for browser tab icon. Falls back to logoUrl when no dedicated favicon exists."
|
||||
},
|
||||
"logoUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Light-theme logo."
|
||||
},
|
||||
"logoDarkUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Dark-theme logo; null when not set."
|
||||
},
|
||||
"hasDedicatedFavicon": {
|
||||
"type": "boolean",
|
||||
"description": "True when the business uploaded a separate favicon (not auto-derived from logo)."
|
||||
}
|
||||
},
|
||||
"required": ["faviconUrl", "logoUrl", "logoDarkUrl", "hasDedicatedFavicon"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Unknown domain"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,12 @@ export class BusinessProfileService {
|
||||
await this.assertLogoMedia(businessId, BigInt(dto.logoDarkMediaId));
|
||||
}
|
||||
|
||||
if (dto.faviconMediaId !== undefined && dto.faviconMediaId !== null) {
|
||||
await this.assertLogoMedia(businessId, BigInt(dto.faviconMediaId));
|
||||
}
|
||||
|
||||
const previousFaviconMediaId = business.faviconMediaId;
|
||||
const faviconExplicitlySet = dto.faviconMediaId !== undefined;
|
||||
const logoChanged =
|
||||
dto.logoMediaId !== undefined &&
|
||||
(dto.logoMediaId === null
|
||||
@@ -210,6 +215,13 @@ export class BusinessProfileService {
|
||||
: { connect: { id: BigInt(dto.logoDarkMediaId) } };
|
||||
}
|
||||
|
||||
if (dto.faviconMediaId !== undefined) {
|
||||
data.faviconMedia =
|
||||
dto.faviconMediaId === null
|
||||
? { disconnect: true }
|
||||
: { connect: { id: BigInt(dto.faviconMediaId) } };
|
||||
}
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
await tx.business.update({
|
||||
where: { id: businessId },
|
||||
@@ -234,7 +246,7 @@ export class BusinessProfileService {
|
||||
}
|
||||
});
|
||||
|
||||
if (logoChanged) {
|
||||
if (logoChanged && !faviconExplicitlySet) {
|
||||
if (dto.logoMediaId === null) {
|
||||
await this.deleteFaviconMedia(previousFaviconMediaId);
|
||||
} else if (dto.logoMediaId != null) {
|
||||
|
||||
@@ -115,6 +115,11 @@ export class UpdateBusinessProfileDto {
|
||||
@IsInt()
|
||||
logoDarkMediaId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
faviconMediaId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Type(() => Number)
|
||||
|
||||
@@ -30,16 +30,23 @@ You are building a **Meshkee business website (storefront)**. You must use the M
|
||||
|
||||
### Typical bootstrap sequence
|
||||
1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`)
|
||||
2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
|
||||
3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc); each portfolio may include nullable `projectUrl` (external website link). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
|
||||
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||
2. `GET /tenants/{domain}/website/favicon` → `faviconUrl` for `<link rel="icon">` / Next.js `metadata.icons` (falls back to `logoUrl` when no dedicated favicon). Also returns `logoUrl`, `logoDarkUrl`, `hasDedicatedFavicon`.
|
||||
3. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
|
||||
4. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc); each portfolio may include nullable `projectUrl` (external website link). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
|
||||
5. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||
6. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||
- For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl`
|
||||
- Response includes `payment.redirect` `{ method, url, fields }` — POST/redirect shopper to the bank
|
||||
- Meshkee registers the bank `callback_url` on the **store apex** (`https://YOUR_WEBSITE_DOMAIN/meshkee/payments/{gateway}/callback`), which nginx proxies to the API. ZarinPal/Mellat domain checks must match the store domain, not `api.meshkee.com`.
|
||||
- After verify, API redirects the browser to `returnUrl?status=success|failed&orderId=…`
|
||||
- Enabled gateways: `GET /tenants/{domain}` → `ePayment`, or `GET /businesses/{businessId}/payments/methods`
|
||||
|
||||
### Favicon + logos
|
||||
- `GET /tenants/{domain}/website/favicon` → `{ faviconUrl, logoUrl, logoDarkUrl, hasDedicatedFavicon }`
|
||||
- Use `faviconUrl` in layout metadata (Next.js: `icons: [{ url: faviconUrl, type: 'image/png' }]` when set).
|
||||
- When `hasDedicatedFavicon` is false, `faviconUrl` equals the light `logoUrl` — still safe to use as tab icon.
|
||||
- Header/footer logos: `logoUrl` on light backgrounds, `logoDarkUrl` on dark (fallback to `logoUrl` in CSS when null).
|
||||
|
||||
### User products (customer listings)
|
||||
Public marketplace listings owned by customers — not catalog `products`.
|
||||
- `GET /tenants/{domain}/user-products` — list published (`name`/`q`, `categoryId`, `cityId`, `countryId`, `condition`, `promoted`, pagination)
|
||||
|
||||
@@ -1494,6 +1494,13 @@
|
||||
"url": "{{baseUrl}}/tenants/{{domain}}/website/business-info"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get favicon + logos (website)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/tenants/{{domain}}/website/favicon"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "List category groups (website)",
|
||||
"request": {
|
||||
|
||||
@@ -92,6 +92,15 @@
|
||||
<li>Cart / orders / favorites: <code>/businesses/{businessId}/...</code> + Bearer JWT.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Branding (favicon + logos)</h2>
|
||||
<p>
|
||||
<code>GET /tenants/{domain}/website/favicon</code> returns
|
||||
<code>faviconUrl</code>, <code>logoUrl</code>, <code>logoDarkUrl</code>, and
|
||||
<code>hasDedicatedFavicon</code>. Use <code>faviconUrl</code> for the browser tab icon
|
||||
(Next.js <code>metadata.icons</code>). When no dedicated favicon is uploaded,
|
||||
<code>faviconUrl</code> falls back to <code>logoUrl</code>.
|
||||
</p>
|
||||
|
||||
<h2>User products (customer listings)</h2>
|
||||
<p>
|
||||
Marketplace-style stock listings created by customers. Public read-only under
|
||||
|
||||
@@ -176,7 +176,8 @@
|
||||
},
|
||||
"faviconUrl": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"nullable": true,
|
||||
"description": "Tab icon URL. Dedicated favicon when uploaded; otherwise same as logoUrl."
|
||||
},
|
||||
"specialProductsSource": {
|
||||
"type": "string",
|
||||
@@ -332,7 +333,99 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Business public profile"
|
||||
"description": "Business public profile",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"nameFa": { "type": "string" },
|
||||
"about": { "type": "string" },
|
||||
"vision": { "type": "string" },
|
||||
"logoUrl": { "type": "string", "nullable": true },
|
||||
"logoDarkUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Logo for dark backgrounds; null when not set."
|
||||
},
|
||||
"faviconUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Dedicated favicon when uploaded; otherwise same as logoUrl."
|
||||
},
|
||||
"emails": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"phoneNumbers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["landline", "cell"] },
|
||||
"number": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"socialMedia": { "type": "object" },
|
||||
"addresses": { "type": "array", "items": { "type": "object" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/website/favicon": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Homepage"
|
||||
],
|
||||
"summary": "Favicon and logo URLs for site chrome",
|
||||
"description": "Use `faviconUrl` for `<link rel=\"icon\">` or Next.js `metadata.icons`. When no dedicated favicon is uploaded, `faviconUrl` equals `logoUrl`. Prefer this endpoint over tenant resolve when you only need branding assets.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/domain"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Branding asset URLs",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"faviconUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Use for browser tab icon. Falls back to logoUrl when no dedicated favicon exists."
|
||||
},
|
||||
"logoUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Light-theme logo."
|
||||
},
|
||||
"logoDarkUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Dark-theme logo; null when not set."
|
||||
},
|
||||
"hasDedicatedFavicon": {
|
||||
"type": "boolean",
|
||||
"description": "True when the business uploaded a separate favicon (not auto-derived from logo)."
|
||||
}
|
||||
},
|
||||
"required": ["faviconUrl", "logoUrl", "logoDarkUrl", "hasDedicatedFavicon"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Unknown domain"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { WebsiteFaviconService } from './website-favicon.service';
|
||||
|
||||
@Controller('tenants/:host/website/favicon')
|
||||
export class PublicWebsiteFaviconController {
|
||||
constructor(private readonly service: WebsiteFaviconService) {}
|
||||
|
||||
@Get()
|
||||
get(@Param('host') host: string) {
|
||||
return this.service.getPublic(host);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
|
||||
@Injectable()
|
||||
export class WebsiteFaviconService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly tenant: TenantService,
|
||||
) {}
|
||||
|
||||
async getPublic(host: string) {
|
||||
const business = await this.tenant.resolveBusinessByDomain(host);
|
||||
|
||||
const record = await this.prisma.business.findUnique({
|
||||
where: { id: business.id },
|
||||
select: {
|
||||
faviconMediaId: true,
|
||||
logoMedia: { select: { publicUrl: true } },
|
||||
logoDarkMedia: { select: { publicUrl: true } },
|
||||
faviconMedia: { select: { publicUrl: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
throw new NotFoundException('Business not found');
|
||||
}
|
||||
|
||||
const logoUrl = record.logoMedia?.publicUrl ?? null;
|
||||
const logoDarkUrl = record.logoDarkMedia?.publicUrl ?? null;
|
||||
const faviconUrl = record.faviconMedia?.publicUrl ?? logoUrl;
|
||||
|
||||
return {
|
||||
faviconUrl,
|
||||
logoUrl,
|
||||
logoDarkUrl,
|
||||
hasDedicatedFavicon: record.faviconMediaId != null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
PublicWebsiteBusinessInfoController,
|
||||
} from './website-business-info.controller';
|
||||
import { WebsiteBusinessInfoService } from './website-business-info.service';
|
||||
import { PublicWebsiteFaviconController } from './website-favicon.controller';
|
||||
import { WebsiteFaviconService } from './website-favicon.service';
|
||||
import {
|
||||
PublicWebsiteSlidersController,
|
||||
WebsiteSlidersController,
|
||||
@@ -39,6 +41,7 @@ import { WebsiteStaticImagesService } from './website-static-images.service';
|
||||
PublicWebsiteStaticImagesController,
|
||||
WebsiteStaticImagesController,
|
||||
PublicWebsiteBusinessInfoController,
|
||||
PublicWebsiteFaviconController,
|
||||
],
|
||||
providers: [
|
||||
WebsiteCategoryGroupsService,
|
||||
@@ -46,6 +49,7 @@ import { WebsiteStaticImagesService } from './website-static-images.service';
|
||||
WebsiteSlidersService,
|
||||
WebsiteStaticImagesService,
|
||||
WebsiteBusinessInfoService,
|
||||
WebsiteFaviconService,
|
||||
],
|
||||
})
|
||||
export class WebsiteModule {}
|
||||
|
||||
Reference in New Issue
Block a user