mirror of
https://github.com/KevinMidboe/planetposen-frontend.git
synced 2025-10-29 13:10:12 +00:00
Feat: JsonLd product metadata (#2)
* Generates JSON ld structured metadata from a product & appends to head * Updated IProduct & IVariation interface * Added IProductResponse & IProductsResponse interfaces * Fixed sitemap urls having to many protocols
This commit is contained in:
@@ -15,3 +15,13 @@ export interface IOrderSummaryResponse {
|
||||
success: boolean;
|
||||
order: IOrderSummary;
|
||||
}
|
||||
|
||||
export interface IProductResponse {
|
||||
success: boolean
|
||||
product: IProduct
|
||||
}
|
||||
|
||||
export interface IProductsResponse {
|
||||
success: boolean
|
||||
products: Array<IProduct>
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
import type IProductVariation from './IProductVariation';
|
||||
|
||||
export default interface IProduct {
|
||||
export interface IProduct {
|
||||
product_no: number;
|
||||
name: string;
|
||||
subtext?: string;
|
||||
description?: string;
|
||||
image: string;
|
||||
currency: string;
|
||||
variations?: IProductVariation[];
|
||||
primary_color?: string;
|
||||
variation_count?: string;
|
||||
sum_stock?: number;
|
||||
updated?: Date;
|
||||
created?: Date;
|
||||
variations?: IVariation[];
|
||||
}
|
||||
|
||||
export interface IVariation {
|
||||
sku_id: number;
|
||||
price: number;
|
||||
size: string;
|
||||
stock: number;
|
||||
default_price: boolean;
|
||||
updated?: Date;
|
||||
created?: Date;
|
||||
}
|
||||
|
||||
38
src/lib/jsonld/product.ts
Normal file
38
src/lib/jsonld/product.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { IProduct } from '../interfaces/IProduct'
|
||||
|
||||
function structureProduct(product: IProduct) {
|
||||
const output = product?.variations?.map(variation => {
|
||||
return {
|
||||
'@context': 'https://schema.org/',
|
||||
'@type': 'Product',
|
||||
name: `${product.name} - ${variation.size}`,
|
||||
image: [product.image],
|
||||
description: product.description,
|
||||
sku: `${product.product_no}-${variation.sku_id}`,
|
||||
productID: product.product_no,
|
||||
mpn: product.product_no,
|
||||
brand: {
|
||||
'@type': 'Brand',
|
||||
name: 'Planetposen'
|
||||
},
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
url: `https://planet.schleppe.cloud/shop/${product.product_no}`,
|
||||
priceCurrency: 'NOK',
|
||||
price: variation.price,
|
||||
itemCondition: 'https://schema.org/NewCondition',
|
||||
availability: 'https://schema.org/InStock'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
|
||||
export default function generateProductJsonLd(product: IProduct): HTMLElement {
|
||||
const jsonldScript = document.createElement('script');
|
||||
jsonldScript.setAttribute('type', 'application/ld+json');
|
||||
jsonldScript.textContent = structureProduct(product);
|
||||
|
||||
return jsonldScript;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
import VippsHurtigkasse from '$lib/components/VippsHurtigkasse.svelte';
|
||||
import { cart } from '$lib/cartStore';
|
||||
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
|
||||
function postOrder(event: any) {
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { mockProducts } from '$lib/utils/mock';
|
||||
import type { PageServerData } from './$types';
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
|
||||
function subTotal(products: Array<IProduct>) {
|
||||
let total = 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type { IProductsResponse } from '$lib/interfaces/ApiResponse';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch }) => {
|
||||
@@ -9,7 +10,7 @@ export const load: PageServerLoad = async ({ fetch }) => {
|
||||
}
|
||||
|
||||
const res = await fetch(url);
|
||||
const products = await res.json();
|
||||
const products: IProductsResponse = await res.json();
|
||||
|
||||
return products;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import ProductTile from './ProductTile.svelte';
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
|
||||
export let data: PageData;
|
||||
const products = data.products as Array<IProduct>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
|
||||
export let product: IProduct;
|
||||
export let large = false;
|
||||
@@ -12,14 +12,16 @@
|
||||
product?.primary_color === '#231B1D' ? '#f3efeb' : '#37301e'
|
||||
}`}"
|
||||
>
|
||||
{#if !large}<h3>{product?.name}</h3>{/if}
|
||||
{#if !large}
|
||||
<h3>{product?.name}</h3>
|
||||
{/if}
|
||||
|
||||
<div class="{`image-frame ${large ? 'large' : null}`}">
|
||||
<img src="{product?.image}" alt="{product?.name}" />
|
||||
</div>
|
||||
|
||||
{#if !large}
|
||||
<p class="subtext">{product?.subtext}</p>
|
||||
<p class="subtext">{product?.subtext}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type { IProductResponse } from '$lib/interfaces/ApiResponse';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, params }) => {
|
||||
@@ -11,6 +12,6 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
|
||||
}
|
||||
|
||||
const res = await fetch(url);
|
||||
const product = await res.json();
|
||||
const product: IProductResponse = await res.json();
|
||||
return product;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { PageData } from './$types';
|
||||
import ProductTile from '../ProductTile.svelte';
|
||||
import ProductVariationSelect from '$lib/components/ProductVariationSelect.svelte';
|
||||
import QuantitySelect from '$lib/components/QuantitySelect.svelte';
|
||||
import SizesSection from './SizesSection.svelte';
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type IProductVariation from '$lib/interfaces/IProductVariation';
|
||||
import type { IProduct, IVariation } from '$lib/interfaces/IProduct';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import generateProductJsonLd from '$lib/jsonld/product';
|
||||
|
||||
export let data: PageData;
|
||||
const product = data.product as IProduct;
|
||||
@@ -28,12 +29,19 @@
|
||||
cooldownInputs = false;
|
||||
}
|
||||
|
||||
function defaultVariation() {
|
||||
return product.variations?.find(variation => variation.default_price)
|
||||
}
|
||||
|
||||
let jsonLd: HTMLElement;
|
||||
let cooldownInputs = false;
|
||||
let quantity = 1;
|
||||
let selectedVariation: IProductVariation;
|
||||
let selectedVariation: IVariation | undefined = defaultVariation()
|
||||
$: addProductButtonText = cooldownInputs
|
||||
? `${quantity} produkt${quantity > 1 ? 'er' : ''} lagt til`
|
||||
: `Legg til ${quantity} i handlekurven`;
|
||||
|
||||
onMount(() => document.head.appendChild(generateProductJsonLd(product)))
|
||||
</script>
|
||||
|
||||
<div class="product-container">
|
||||
|
||||
@@ -41,18 +41,16 @@ export async function GET() {
|
||||
return new Response(body, { headers });
|
||||
}
|
||||
|
||||
function buildSitemapUrl(address: string, modified: string, frequency: string) {
|
||||
function buildSitemapUrl(path: string, modified: string, frequency: string) {
|
||||
return `<url>
|
||||
<loc>https://${address}</loc>
|
||||
<loc>https://${domain}${path}</loc>
|
||||
<lastmod>${modified}</lastmod>
|
||||
<changefreq>${frequency}</changefreq>
|
||||
</url>`;
|
||||
}
|
||||
|
||||
function sitemapPages(): string {
|
||||
return pages
|
||||
.map((page) => buildSitemapUrl(`https://${domain}/${page.name}`, page.modified, 'yearly'))
|
||||
.join('\n');
|
||||
return pages.map((page) => buildSitemapUrl(`/${page.name}`, page.modified, 'yearly')).join('\n');
|
||||
}
|
||||
|
||||
async function sitemapShopPages(): Promise<string> {
|
||||
@@ -66,11 +64,7 @@ async function sitemapShopPages(): Promise<string> {
|
||||
|
||||
return products?.products
|
||||
?.map((product) =>
|
||||
buildSitemapUrl(
|
||||
`https://${domain}/shop/${product.product_no}`,
|
||||
String(product.updated),
|
||||
'daily'
|
||||
)
|
||||
buildSitemapUrl(`/shop/${product.product_no}`, String(product.updated), 'daily')
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import ProductList from './WarehouseProductList.svelte';
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
|
||||
export let products: Array<IProduct>;
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
|
||||
<td class="name-and-price">
|
||||
<p><a href="/warehouse/{product.product_no}">{product.name}</a></p>
|
||||
<p>{product.variation_count} variation(s)</p>
|
||||
<p>{product?.variations?.length} variation(s)</p>
|
||||
</td>
|
||||
|
||||
<td class="stock-column">{product.sum_stock}</td>
|
||||
<td class="stock-column">{product?.sum_stock}</td>
|
||||
|
||||
<td class="date-column"
|
||||
>{new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import DetailsSection from './DetailsSection.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import type { PageServerData } from './$types';
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
|
||||
export let data: PageServerData;
|
||||
const product = data.product as IProduct;
|
||||
@@ -20,7 +20,7 @@
|
||||
<img src="{product.image}" alt="Product" />
|
||||
<div class="name-and-price">
|
||||
<p>{product.name}</p>
|
||||
<p>NOK {product.price}</p>
|
||||
<p>NOK {product?.price}</p>
|
||||
</div>
|
||||
|
||||
<div class="edit-button">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
|
||||
export let product: IProduct;
|
||||
export let edit: boolean;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type IProductVariation from '$lib/interfaces/IProductVariation';
|
||||
import type { IProduct, IVariation } from '$lib/interfaces/IProduct';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Badge from '$lib/components/Badge.svelte';
|
||||
import BadgeType from '$lib/interfaces/BadgeType';
|
||||
@@ -10,7 +9,7 @@
|
||||
let editingVariationIndex = -1;
|
||||
|
||||
interface ISkuResponse {
|
||||
skus: IProductVariation[];
|
||||
skus: IVariation[];
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
@@ -19,7 +18,7 @@
|
||||
product.variations = response?.skus;
|
||||
};
|
||||
|
||||
function setDefault(variation: IProductVariation) {
|
||||
function setDefault(variation: IVariation) {
|
||||
if (!product.variations) return;
|
||||
|
||||
let url = `/api/product/${product.product_no}/sku/${variation.sku_id}/default_price`;
|
||||
@@ -60,7 +59,7 @@
|
||||
.then(() => (editingVariationIndex = product.variations.length - 1));
|
||||
}
|
||||
|
||||
function saveSkuVariation(variation: IProductVariation) {
|
||||
function saveSkuVariation(variation: IVariation) {
|
||||
let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`;
|
||||
if (window?.location?.href.includes('localhost')) {
|
||||
url = 'http://localhost:30010'.concat(url);
|
||||
@@ -81,7 +80,7 @@
|
||||
.then(() => resetEditingIndex());
|
||||
}
|
||||
|
||||
function deleteVariation(variation: IProductVariation) {
|
||||
function deleteVariation(variation: IVariation) {
|
||||
console.log('delete it using api', variation);
|
||||
let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`;
|
||||
if (window?.location?.href.includes('localhost')) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import type IProduct from '$lib/interfaces/IProduct';
|
||||
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||
import type { PageServerData } from './$types';
|
||||
|
||||
export let data: PageServerData;
|
||||
@@ -12,9 +12,9 @@
|
||||
<div>
|
||||
<h2>Product attributes</h2>
|
||||
<Input label="Name" value="{product.name}" required="{false}" />
|
||||
<Input label="Description" value="{product.description}" required="{false}" />
|
||||
<Input label="Subtext" value="{product.subtext}" required="{false}" />
|
||||
<Input label="Color" value="{product.primary_color}" required="{false}" />
|
||||
<Input label="Description" value="{product.description || ''}" required="{false}" />
|
||||
<Input label="Subtext" value="{product.subtext || ''}" required="{false}" />
|
||||
<Input label="Color" value="{product.primary_color || ''}" required="{false}" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user